Преглед изворни кода

feat(desktop): macOS hidden titlebar with vibrancy sidebar

hiddenInset titlebar with traffic lights over a vibrancy sidebar, darwin-only
via data-platform set by the desktop preload: transparent chain to the window
material, native-theme sync from the app theme source, sidebar top strip +
full hide when collapsed with reopen controls in the new
conversation.session.header.leading slot, title-row drag regions, and
fullscreen right-sidebar traffic-light clearance via the dockkit strip inset.
Yif пре 5 дана
родитељ
комит
f2db686404
43 измењених фајлова са 684 додато и 78 уклоњено
  1. 6 0
      .agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.i18n.yaml
  2. 49 0
      .agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.md
  3. 49 0
      .agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.zh.md
  4. 1 0
      apps/desktop/src/ipc.ts
  5. 19 0
      apps/desktop/src/main.ts
  6. 4 0
      apps/desktop/src/preload-app.ts
  7. 14 0
      apps/desktop/src/preload-platform.ts
  8. 33 0
      apps/desktop/src/preload-theme.ts
  9. 2 0
      apps/desktop/src/preload.ts
  10. 2 2
      docs/subsystems/slots.i18n.yaml
  11. 1 0
      docs/subsystems/slots.md
  12. 1 0
      docs/subsystems/slots.zh.md
  13. 1 0
      packages/client/ui-conversation/src/client/apply.ts
  14. 19 0
      packages/client/ui-conversation/src/client/contract/slots.ts
  15. 39 0
      packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css
  16. 32 23
      packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx
  17. 1 0
      packages/client/ui-conversation/tests/skeleton.client.spec.tsx
  18. 4 1
      packages/client/ui-dockkit/src/components/dockkit.module.css
  19. 25 0
      packages/client/ui-layout/src/client/AppFrame.module.css
  20. 7 3
      packages/client/ui-layout/src/client/AppFrame.tsx
  21. 4 2
      packages/client/ui-layout/src/client/columns.ts
  22. 18 2
      packages/client/ui-layout/src/client/theme-presenter.ts
  23. 20 4
      packages/client/ui-layout/tests/theme-presenter.client.spec.ts
  24. 11 0
      packages/client/ui-primitives/src/darwin-desktop.ts
  25. 1 0
      packages/client/ui-primitives/src/index.ts
  26. 31 0
      packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css
  27. 2 2
      packages/client/ui-sidebar/README.i18n.yaml
  28. 4 0
      packages/client/ui-sidebar/README.md
  29. 4 0
      packages/client/ui-sidebar/README.zh.md
  30. 2 0
      packages/client/ui-sidebar/package.json
  31. 50 0
      packages/client/ui-sidebar/src/client/HeaderLeadingControls.module.css
  32. 53 0
      packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx
  33. 43 0
      packages/client/ui-sidebar/src/client/SidebarRoot.module.css
  34. 28 19
      packages/client/ui-sidebar/src/client/SidebarRoot.tsx
  35. 12 0
      packages/client/ui-sidebar/src/client/index.ts
  36. 9 0
      packages/client/ui-sidebar/tests/apply.client.spec.tsx
  37. 3 0
      packages/client/ui-sidebar/tsconfig.json
  38. 4 1
      packages/client/ui-theme/src/boot-theme.ts
  39. 3 0
      packages/client/ui-theme/tests/boot-theme.client.spec.ts
  40. 6 0
      packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.module.css
  41. 7 0
      packages/client/web/src/base.css
  42. 54 16
      packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
  43. 6 3
      pnpm-lock.yaml

+ 6 - 0
.agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.md
+2026-09-13-macos-hidden-titlebar-vibrancy.md: aa2723b97335cf3731a5e96878e197336b430b51
+2026-09-13-macos-hidden-titlebar-vibrancy.zh.md: 0a7c93f1314914e1fdc9270f9bd48d34755116f9

+ 49 - 0
.agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.md

@@ -0,0 +1,49 @@
+# Agent Note: macOS hidden titlebar with vibrancy sidebar
+
+Status: implemented
+
+English | [中文](2026-09-13-macos-hidden-titlebar-vibrancy.zh.md)
+
+## Problem
+
+The desktop app drew the stock macOS titlebar: an opaque bar above the web UI that repeats chrome the page already has, spends vertical space, and keeps the sidebar from reaching the window's top edge. The window looked like a browser tab rather than a macOS application, and no mechanism existed for platform-specific presentation — every pixel was identical on macOS, Windows, and the plain web.
+
+## Decision
+
+The Electron main process opens the main window on darwin with `titleBarStyle: 'hiddenInset'`, `trafficLightPosition: { x: 16, y: 18 }`, `vibrancy: 'sidebar'`, `visualEffectState: 'active'`, and a transparent `backgroundColor`. `'active'` keeps the material stable behind an unfocused window; `'followWindow'` washed the sidebar out on blur.
+
+Every web-side adjustment keys off `html[data-platform]`, which only the desktop preloads set (`document.documentElement.dataset.platform = process.platform`). The plain web and non-darwin desktop render exactly as before.
+
+**Transparency chain.** Vibrancy shows only through transparent pixels: on darwin `html`/`body` (ui-web base.css) and the AppFrame are transparent, the center column paints `--dsw-alias-bg-base` opaque, and the sidebar column paints a translucent `color-mix` tint of the sidebar fill so the material reads through it. SidebarRoot's own opaque fill moves to the frame column for the same reason.
+
+**Native theme sync.** The vibrancy material follows `nativeTheme.themeSource`, which otherwise tracks the OS appearance and diverges from the app's own theme preference. The ui-theme boot script and ui-layout `ThemePresenter` publish `html[data-ds-theme-source]` (`light`, `dark`, or `system`; a fixed preference, including registered theme ids, publishes its resolved scheme). The app preload observes the attribute and forwards it over `dsh-desktop:native-theme-set`; main validates the value and the sender (the main window's WebContents — the document is backend-served, so the `dsh-app://` sender check does not apply) and assigns `nativeTheme.themeSource`. Publishing the preference rather than the resolved scheme preserves OS-follow while the preference is `system`.
+
+**Sidebar top strip and full hide.** On darwin the sidebar opens with a 52px top strip that clears the traffic lights, carries the collapse toggle, and is a window drag region (`-webkit-app-region: drag`; the toggle opts out). Collapsing the sidebar hides the column entirely — `computeColumns` takes an explicit `collapsedWidth` and the AppFrame passes 0 on darwin desktop — instead of the 56px rail the other platforms keep. The reopen affordances move into the conversation header: a new single, session-scoped slot `conversation.session.header.leading` sits before the breadcrumbs, and ui-sidebar registers `HeaderLeadingControls` (open sidebar + New Session, reusing the shell's inject face and locale) into it. Visibility is pure CSS against the AppFrame-published `data-sidebar-collapsed` attribute; no collapse-state pipe is added. The blank-session header keeps the leading seat mounted on darwin so a hidden sidebar always has a reopen control on screen.
+
+**Drag regions.** The conversation title row is a drag region on darwin with every interactive descendant opted out. Electron computes drag regions from window geometry in DOM order, not stacking: an overlay covering the title band must subtract itself or the band underneath keeps taking the pointer. The right sidebar's fullscreen panel therefore sets `-webkit-app-region: no-drag` on its whole box and restores drag on its tab strips' blank runs.
+
+**Fullscreen traffic-light clearance.** ui-dockkit publishes the tab strip's start inset as `--dsh-dockkit-strip-inline-start` (fallback the design's own 10px). The right sidebar's fullscreen presentation on darwin sets 88px on the panel body and resets 10px for every non-first split cell's subtree, so exactly the pane touching the window's top-left corner clears the lights at any split depth.
+
+## Alternatives considered
+
+**`titleBarStyle: 'hidden'` with custom window controls.** Rebuilding the traffic lights forfeits native behavior (hover glyphs, fullscreen transitions) for no gain; `hiddenInset` keeps them native and only asks the page to route around them.
+
+**Keeping the 56px rail on darwin.** The rail under floating traffic lights doubled the chrome in the window's corner and wasted the width the collapse exists to reclaim; full hide with header-hosted reopen controls matches macOS sidebar conventions.
+
+**Mirroring the resolved theme instead of the preference.** Forwarding `light`/`dark` while the user preference is `system` would freeze the window material at the value resolved at send time; forwarding `system` lets macOS keep following the OS appearance natively.
+
+**Handling traffic-light clearance inside ui-dockkit.** The kit is host-agnostic and cannot know which host corner touches window chrome; publishing an inset variable keeps the policy in the host that owns the placement (ui-sidebar-right) and costs the kit one custom property.
+
+**A collapse-state prop pipe into the header controls.** The AppFrame already publishes `data-sidebar-collapsed`; CSS visibility against it avoids a second state path that could disagree with the frame's transition timeline.
+
+## Consequences
+
+- The macOS window gains a translucent sidebar and hidden titlebar at zero cost to other platforms: every rule is scoped to `[data-platform='darwin']`, which only the Electron preload sets.
+- The vibrancy material follows the app theme, including third-party registered themes (their resolved scheme). Screenshots and screen recordings differ from the flat web rendering.
+- `conversation.session.header.leading` is a public slot in the client catalog; any package can occupy the seat, and ui-sidebar's occupant assumes it may render into it whenever the platform matches.
+- Drag-region geometry is a window-global invariant: any future overlay that covers the title band on darwin must subtract itself with `-webkit-app-region: no-drag` or its controls become unclickable.
+- The transparent window plus vibrancy is accepted to look different in screen sharing and may flash on startup; the transparent `backgroundColor` mitigates the flash.
+
+## Testing
+
+ui-theme boot and ui-layout presenter specs pin the `data-ds-theme-source` publication and disposal. The ui-sidebar apply spec pins the leading-seat registration (component, locale, shared inject face) and its removal on teardown. The ui-conversation skeleton spec pins the leading slot's render call in the active-phase header. The ui-theme corner-shape and full-round style gates cover the new stylesheet.

+ 49 - 0
.agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.zh.md

@@ -0,0 +1,49 @@
+# Agent Note: macOS hidden titlebar with vibrancy sidebar
+
+Status: implemented
+
+[English](2026-09-13-macos-hidden-titlebar-vibrancy.md) | 中文
+
+## Problem
+
+桌面应用此前使用 macOS 原生标题栏:Web UI 上方一条不透明的横条,重复页面已有的窗口装饰、占用纵向空间,并让侧边栏无法触及窗口顶边。窗口看起来像浏览器标签页而非 macOS 应用,且不存在任何按平台差异化呈现的机制——macOS、Windows 与纯 Web 上每个像素都完全一致。
+
+## Decision
+
+Electron 主进程在 darwin 上以 `titleBarStyle: 'hiddenInset'`、`trafficLightPosition: { x: 16, y: 18 }`、`vibrancy: 'sidebar'`、`visualEffectState: 'active'` 与透明 `backgroundColor` 打开主窗口。`'active'` 让窗口失焦时材质保持稳定;`'followWindow'` 会在失焦时把侧边栏冲淡。
+
+所有 Web 侧调整均以 `html[data-platform]` 为开关,该属性仅由桌面 preload 设置(`document.documentElement.dataset.platform = process.platform`)。纯 Web 与非 darwin 桌面的渲染与之前完全一致。
+
+**透明链。** 毛玻璃只透过透明像素显现:darwin 上 `html`/`body`(ui-web base.css)与 AppFrame 透明,中间列铺不透明的 `--dsw-alias-bg-base`,侧边栏列铺侧边栏底色的半透明 `color-mix`,让材质透出。SidebarRoot 自身的不透明底色出于同一原因移到框架列。
+
+**原生主题同步。** 毛玻璃材质跟随 `nativeTheme.themeSource`,后者默认跟踪系统外观,会与应用自身的主题偏好背离。ui-theme 引导脚本与 ui-layout 的 `ThemePresenter` 发布 `html[data-ds-theme-source]`(`light`、`dark` 或 `system`;固定偏好——包括注册主题 id——发布其解析后的配色)。应用 preload 观察该属性并经 `dsh-desktop:native-theme-set` 转发;主进程校验取值与发送者(主窗口的 WebContents——文档由后端服务,`dsh-app://` 发送者检查不适用)后赋给 `nativeTheme.themeSource`。发布偏好而非解析值,可在偏好为 `system` 时保留跟随系统。
+
+**侧边栏顶部条与完全隐藏。** darwin 上侧边栏展开时有一条 52px 的顶部条,避开红绿灯、承载收起按钮,并作为窗口拖拽区(`-webkit-app-region: drag`;按钮退出拖拽)。收起侧边栏时整列隐藏——`computeColumns` 接受显式 `collapsedWidth`,AppFrame 在 darwin 桌面传 0——而非其他平台保留的 56px rail。重新打开的入口移入会话头部:新增 single、session 作用域的 slot `conversation.session.header.leading` 位于面包屑之前,ui-sidebar 向其注册 `HeaderLeadingControls`(打开侧边栏 + 新会话,复用 shell 的 inject face 与 locale)。显隐纯由 CSS 依据 AppFrame 发布的 `data-sidebar-collapsed` 属性控制;不新增收起状态管道。darwin 上空白会话的头部保持 leading 座挂载,确保侧边栏隐藏时屏幕上始终有重新打开的控件。
+
+**拖拽区。** darwin 上会话标题行是拖拽区,所有可交互后代退出。Electron 按 DOM 顺序以窗口几何计算拖拽区,不看层叠:覆盖标题带的浮层必须自行减除,否则下层区域仍会截获指针。因此右侧边栏的全屏面板对整个盒子设 `-webkit-app-region: no-drag`,再在其 tab 条空白处恢复拖拽。
+
+**全屏避开红绿灯。** ui-dockkit 把 tab 条起始内边距发布为 `--dsh-dockkit-strip-inline-start`(回退为设计自身的 10px)。右侧边栏全屏形态在 darwin 上对面板主体设 88px,并对每个非首格 split 单元的子树重置为 10px,使得任意分屏深度下恰好只有触及窗口左上角的 pane 避开红绿灯。
+
+## Alternatives considered
+
+**`titleBarStyle: 'hidden'` + 自绘窗口控件。** 重造红绿灯会失去原生行为(悬停图形、全屏过渡)且无收益;`hiddenInset` 保留原生控件,只要求页面绕行。
+
+**darwin 上保留 56px rail。** 浮动红绿灯下的 rail 让窗口角落装饰翻倍,也浪费了收起本要回收的宽度;完全隐藏 + 头部承载重开控件符合 macOS 侧边栏惯例。
+
+**同步解析后的主题而非偏好。** 偏好为 `system` 时转发 `light`/`dark` 会把窗口材质冻结在发送时刻的解析值;转发 `system` 让 macOS 原生持续跟随系统外观。
+
+**在 ui-dockkit 内部处理红绿灯避让。** kit 与宿主无关,不可能知道哪个宿主角落贴着窗口装饰;发布内边距变量把策略留在拥有布局位置的宿主(ui-sidebar-right),kit 只付出一个自定义属性。
+
+**向头部控件加收起状态 prop 管道。** AppFrame 已发布 `data-sidebar-collapsed`;用 CSS 对其判断显隐,避免了可能与框架过渡时间线不一致的第二条状态路径。
+
+## Consequences
+
+- macOS 窗口获得半透明侧边栏与隐藏标题栏,对其他平台零成本:所有规则限定在 `[data-platform='darwin']` 下,该属性仅由 Electron preload 设置。
+- 毛玻璃材质跟随应用主题,含第三方注册主题(取其解析配色)。截图与录屏与纯 Web 的平面渲染不同。
+- `conversation.session.header.leading` 是客户端 catalog 中的公开 slot;任何包都可占用该座位,ui-sidebar 的占用者假定平台匹配时随时可能渲染。
+- 拖拽区几何是窗口级全局不变量:今后任何在 darwin 上覆盖标题带的浮层必须用 `-webkit-app-region: no-drag` 自行减除,否则其控件不可点击。
+- 接受透明窗口 + 毛玻璃在屏幕共享中呈现不同、启动可能闪烁;透明 `backgroundColor` 缓解闪烁。
+
+## Testing
+
+ui-theme 引导与 ui-layout presenter 测试钉住 `data-ds-theme-source` 的发布与清除。ui-sidebar apply 测试钉住 leading 座注册(组件、locale、共享 inject face)及 teardown 移除。ui-conversation skeleton 测试钉住 active 阶段头部对 leading slot 的渲染调用。ui-theme 的 corner-shape 与 full-round 样式门覆盖新样式表。

+ 1 - 0
apps/desktop/src/ipc.ts

@@ -21,6 +21,7 @@ export const DESKTOP_IPC = {
   updatesCheck: 'dsh-desktop:updates-check',
   updatesInstall: 'dsh-desktop:updates-install',
   updatesState: 'dsh-desktop:updates-state',
+  nativeThemeSet: 'dsh-desktop:native-theme-set',
 } as const
 
 /** Desktop release update state rendered by desktop-owned UI. */

+ 19 - 0
apps/desktop/src/main.ts

@@ -9,6 +9,7 @@ import {
   dialog,
   ipcMain,
   Menu,
+  nativeTheme,
   protocol,
   shell,
   type IpcMainInvokeEvent,
@@ -100,6 +101,17 @@ function createWindow(preload: string, show = false): BrowserWindow {
     minWidth: 880,
     minHeight: 600,
     show,
+    // hiddenInset places traffic lights inside the sidebar; sidebar vibrancy
+    // needs a transparent window background to show through the page.
+    ...(process.platform === 'darwin' ? {
+      titleBarStyle: 'hiddenInset' as const,
+      trafficLightPosition: { x: 16, y: 18 },
+      vibrancy: 'sidebar' as const,
+      // 'active' keeps the vibrancy material stable when the window blurs;
+      // 'followWindow' washes the sidebar out behind an unfocused window.
+      visualEffectState: 'active' as const,
+      backgroundColor: '#00000000',
+    } : {}),
     webPreferences: {
       preload,
       nodeIntegration: false,
@@ -342,6 +354,13 @@ async function main(): Promise<void> {
     assertDesktopSender(event, ['shell'])
     return locale
   })
+  // Fire-and-forget from the application preload (backend-served document, so
+  // no dsh-app sender check); mirrors the Web UI theme source so the macOS
+  // vibrancy material follows the app theme instead of the OS appearance.
+  ipcMain.on(DESKTOP_IPC.nativeThemeSet, (event, source: unknown) => {
+    if (mainWindow === undefined || event.sender !== mainWindow.webContents) return
+    if (source === 'light' || source === 'dark' || source === 'system') nativeTheme.themeSource = source
+  })
   ipcMain.handle(DESKTOP_IPC.pluginsList, (event) => {
     assertDesktopSender(event, ['shell'])
     return manager.listPlugins()

+ 4 - 0
apps/desktop/src/preload-app.ts

@@ -2,6 +2,8 @@
 
 import { contextBridge, ipcRenderer } from 'electron'
 import { DESKTOP_IPC, type DshDesktopStartupApi } from './ipc.ts'
+import { markDocumentPlatform } from './preload-platform.ts'
+import { syncNativeTheme } from './preload-theme.ts'
 import type { DesktopBackendState } from './backend-controller.ts'
 
 const startup: DshDesktopStartupApi = {
@@ -20,5 +22,7 @@ const startup: DshDesktopStartupApi = {
   resetConfiguration: () => ipcRenderer.invoke(DESKTOP_IPC.configurationReset) as Promise<void>,
 }
 
+markDocumentPlatform()
+syncNativeTheme()
 contextBridge.exposeInMainWorld('dshDesktop', location.protocol === 'dsh-app:' && location.hostname === 'shell'
   ? startup : { protocolVersion: 1 })

+ 14 - 0
apps/desktop/src/preload-platform.ts

@@ -0,0 +1,14 @@
+/** Marks the document root with the host platform so shared Web UI CSS can scope desktop-only rules. */
+
+/**
+ * Sets `data-platform` (e.g. `darwin`) on `<html>`, deferring to DOMContentLoaded
+ * when the preload runs before the document root exists.
+ */
+export function markDocumentPlatform(): void {
+  const mark = (): void => { document.documentElement.dataset.platform = process.platform }
+  // lib.dom types documentElement non-null, but a preload runs before the
+  // document root exists.
+  const root = document.documentElement as HTMLElement | null
+  if (root === null) window.addEventListener('DOMContentLoaded', mark)
+  else mark()
+}

+ 33 - 0
apps/desktop/src/preload-theme.ts

@@ -0,0 +1,33 @@
+/** Mirrors the Web UI's theme source into the macOS native theme so window vibrancy follows the app palette. */
+
+import { ipcRenderer } from 'electron'
+import { DESKTOP_IPC } from './ipc.ts'
+
+/** Root attribute written by the Web UI's theme bootstrap and presenter (ui-theme / ui-layout). */
+const THEME_SOURCE_ATTRIBUTE = 'data-ds-theme-source'
+
+/**
+ * On macOS, watches `html[data-ds-theme-source]` and forwards each value to
+ * the main process, which sets `nativeTheme.themeSource` — the sidebar
+ * vibrancy material then follows the app's theme preference instead of the
+ * OS appearance, while `system` keeps following the OS. Other platforms
+ * never send.
+ */
+export function syncNativeTheme(): void {
+  if (process.platform !== 'darwin') return
+  let sent: string | undefined
+  const send = (): void => {
+    const value = document.documentElement.getAttribute(THEME_SOURCE_ATTRIBUTE)
+    if (value === null || value === sent) return
+    sent = value
+    ipcRenderer.send(DESKTOP_IPC.nativeThemeSet, value)
+  }
+  const observe = (): void => {
+    new MutationObserver(send).observe(document.documentElement, { attributeFilter: [THEME_SOURCE_ATTRIBUTE] })
+    send()
+  }
+  // The preload runs before the document root exists; the theme bootstrap
+  // script writes the attribute before DOMContentLoaded.
+  if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', observe)
+  else observe()
+}

+ 2 - 0
apps/desktop/src/preload.ts

@@ -2,6 +2,7 @@
 
 import { contextBridge, ipcRenderer } from 'electron'
 import { DESKTOP_IPC, type DshDesktopApi, type DesktopUpdateState } from './ipc.ts'
+import { markDocumentPlatform } from './preload-platform.ts'
 import type { DesktopBackendState } from './backend-controller.ts'
 
 const api: DshDesktopApi = {
@@ -35,4 +36,5 @@ const api: DshDesktopApi = {
   },
 }
 
+markDocumentPlatform()
 contextBridge.exposeInMainWorld('dshDesktop', api)

+ 2 - 2
docs/subsystems/slots.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/slots.md
-slots.md: f43262dc586daa5efb5881fff968fc57b9f5f1d5
-slots.zh.md: 7452454b1f6a0645047cf08091b548169e4932d4
+slots.md: e5c2b21d8738b3a5f8e36aa5a71f7a8e3240aec8
+slots.zh.md: ac4a16b9ba7d632233d18b1048bc9a84d4e25dbd

+ 1 - 0
docs/subsystems/slots.md

@@ -144,6 +144,7 @@ root
 │     │     └─ conversation.trajectory.images
 │     ├─ conversation.session.header
 │     │  ├─ conversation.session.header.lineage
+│     │  ├─ conversation.session.header.leading
 │     │  ├─ conversation.session.header.actions
 │     │  ├─ conversation.session.header.utilities
 │     │  └─ conversation.session.header.corner

+ 1 - 0
docs/subsystems/slots.zh.md

@@ -144,6 +144,7 @@ root
 │     │     └─ conversation.trajectory.images
 │     ├─ conversation.session.header
 │     │  ├─ conversation.session.header.lineage
+│     │  ├─ conversation.session.header.leading
 │     │  ├─ conversation.session.header.actions
 │     │  ├─ conversation.session.header.utilities
 │     │  └─ conversation.session.header.corner

+ 1 - 0
packages/client/ui-conversation/src/client/apply.ts

@@ -301,6 +301,7 @@ export function apply(ctx: Context, config: Config = Config({})): void {
     locale: NS,
     children: {
       'conversation.session.header.lineage': { kind: 'single', scope: 'session' },
+      'conversation.session.header.leading': { kind: 'single', scope: 'session' },
       'conversation.session.header.actions': { kind: 'list', scope: 'session' },
       'conversation.session.header.utilities': { kind: 'list', scope: 'session' },
       'conversation.session.header.corner': { kind: 'single', scope: 'session' },

+ 19 - 0
packages/client/ui-conversation/src/client/contract/slots.ts

@@ -141,6 +141,18 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
       scope: 'session'
       owner: ConversationHeaderActionOwnerProps
     }
+    /**
+     * Leading seat before the Session breadcrumbs, for window-chrome-adjacent
+     * controls (macOS desktop sidebar reopen and New Session while the sidebar
+     * is hidden). The seat is laid out only while its occupant renders
+     * something, and it stays mounted through the blank-session state so a
+     * hidden sidebar always keeps a reopen control on screen.
+     */
+    'conversation.session.header.leading': {
+      kind: 'single'
+      scope: 'session'
+      owner: ConversationHeaderLeadingOwnerProps
+    }
     /**
      * The header's far-right corner, past the utilities' edge and into the
      * header's own padding, for one control. The corner is laid out only while
@@ -230,6 +242,12 @@ export interface ConversationHeaderCornerOwnerProps {
   children?: never
 }
 
+/** The leading seat's occupant derives its state from standard Session props. */
+export interface ConversationHeaderLeadingOwnerProps {
+  /** Marker field: the occupant receives no owner-specific values. */
+  children?: never
+}
+
 /** Plain breadcrumb data handed to the optional lineage renderer. */
 export interface ConversationHeaderLineageOwnerProps {
   /** Session represented by this breadcrumb title. */
@@ -394,6 +412,7 @@ export type ConversationSessionHeaderSlotProps =
   PropsRuntime<'conversation.session.header'>
   & PropsRenderSlots<
     'conversation.session.header.lineage'
+    | 'conversation.session.header.leading'
     | 'conversation.session.header.actions'
     | 'conversation.session.header.utilities'
     | 'conversation.session.header.corner'

+ 39 - 0
packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css

@@ -53,6 +53,30 @@
   display: none;
 }
 
+/* macOS hiddenInset titlebar: the title row doubles as the window drag
+   region; interactive children opt out. */
+:global([data-platform='darwin']) .titleRow {
+  -webkit-app-region: drag;
+}
+
+:global([data-platform='darwin']) .titleRow button,
+:global([data-platform='darwin']) .titleRow a,
+:global([data-platform='darwin']) .headerLeading,
+:global([data-platform='darwin']) .headerActions,
+:global([data-platform='darwin']) .headerUtilities,
+:global([data-platform='darwin']) .headerCorner {
+  -webkit-app-region: no-drag;
+}
+
+/* Blank state on macOS desktop: the chrome rows are gone but the leading seat
+   (the hidden sidebar's reopen control) stays on screen, so the header keeps
+   its layout without the rule. */
+:global([data-platform='darwin']) .headerHidden {
+  display: block;
+  min-height: 0;
+  border-bottom: none;
+}
+
 .titleRow {
   display: flex;
   align-items: center;
@@ -68,6 +92,21 @@
   min-width: 0;
 }
 
+/* Leading seat before the breadcrumbs; laid out only while its occupant
+   renders something, and the occupant owns its own trailing gap (a CSS-hidden
+   occupant keeps the seat non-empty, so a seat-owned margin would push the
+   breadcrumbs while nothing shows). */
+.headerLeading {
+  display: flex;
+  flex: none;
+  align-items: center;
+  gap: 8px;
+}
+
+.headerLeading:empty {
+  display: none;
+}
+
 .crumbs {
   display: flex;
   align-items: center;

+ 32 - 23
packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx

@@ -1,6 +1,7 @@
 /** Strict per-session header/body content inserted into the resident conversation layout. */
 
 import clsx from 'clsx'
+import { isDarwinDesktop } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { SessionListState, SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type {
@@ -54,7 +55,8 @@ function equalBreadcrumbs(left: readonly Breadcrumb[], right: readonly Breadcrum
 /**
  * Renders Session header chrome above the resident conversation scrollport.
  * @param props - Strict Session store, view ledger, navigation, render, and locale shares.
- * @returns the hidden blank-session header or visible title and tabs.
+ * @returns the header: title and tabs when visible, and on macOS desktop the
+ *   always-mounted leading seat even while blank-session chrome hides.
  */
 export function ConversationSessionHeader({
   sessionId, useSession, useSessions, useConversation, useConversationViews, useStore,
@@ -67,15 +69,22 @@ export function ConversationSessionHeader({
   const session = useSession(s => s)
   const conversation = useConversation(s => s)
   const hideChrome = session.blank && conversationPhase(session, conversation) === 'blank'
+  // macOS desktop keeps the leading seat mounted through the blank state (the
+  // hidden sidebar's reopen control lives there), so the header may not leave
+  // the layout or the accessibility tree while its chrome hides.
+  const darwinDesktop = isDarwinDesktop()
 
   return (
     <header
       className={clsx(css.header, hideChrome && css.headerHidden)}
-      aria-hidden={hideChrome || undefined}
+      aria-hidden={(hideChrome && !darwinDesktop) || undefined}
     >
-      {!hideChrome && (
-        <>
-          <div className={css.titleRow}>
+      <div className={css.titleRow}>
+        <div className={css.headerLeading} data-conversation-header-leading="">
+          {renderSlot('conversation.session.header.leading', {})}
+        </div>
+        {!hideChrome && (
+          <>
             <div className={css.titleCluster}>
               <nav className={css.crumbs} aria-label={t('session.hierarchy')}>
                 {ancestry.map((summary, index) => {
@@ -136,24 +145,24 @@ export function ConversationSessionHeader({
             <div className={css.headerCorner} data-conversation-header-corner="">
               {renderSlot('conversation.session.header.corner', {})}
             </div>
-          </div>
-          {tabs.length > 1 && (
-            <div className={css.tabs} role="tablist">
-              {tabs.map(viewTab => (
-                <button
-                  key={viewTab.id}
-                  type="button"
-                  role="tab"
-                  aria-selected={viewTab.id === active?.id}
-                  className={clsx(css.tab, viewTab.id === active?.id && css.tabActive)}
-                  onClick={() => { selectView(viewTab.id) }}
-                >
-                  {viewTab.label}
-                </button>
-              ))}
-            </div>
-          )}
-        </>
+          </>
+        )}
+      </div>
+      {!hideChrome && tabs.length > 1 && (
+        <div className={css.tabs} role="tablist">
+          {tabs.map(viewTab => (
+            <button
+              key={viewTab.id}
+              type="button"
+              role="tab"
+              aria-selected={viewTab.id === active?.id}
+              className={clsx(css.tab, viewTab.id === active?.id && css.tabActive)}
+              onClick={() => { selectView(viewTab.id) }}
+            >
+              {viewTab.label}
+            </button>
+          ))}
+        </div>
       )}
     </header>
   )

+ 1 - 0
packages/client/ui-conversation/tests/skeleton.client.spec.tsx

@@ -447,6 +447,7 @@ describe('ConversationRoot resident composer', () => {
     expect(host?.contains(seat)).toBe(true)
     expect(seat?.contains(textarea)).toBe(true)
     expect(b.slotCalls).toContain('conversation.session.header.lineage')
+    expect(b.slotCalls).toContain('conversation.session.header.leading')
     expect(b.slotCalls).toContain('conversation.session.header.actions')
     expect(b.slotCalls).toContain('conversation.session.header.utilities')
     expect(b.slotCalls).toContain('conversation.session.header.corner')

+ 4 - 1
packages/client/ui-dockkit/src/components/dockkit.module.css

@@ -159,7 +159,10 @@
   gap: 4px;
   align-items: center;
   height: 28px;
-  padding: 10px 6px 0 10px;
+  /* The start inset is a published variable so a host that pins the surface
+     against window chrome (the right sidebar fullscreen under macOS traffic
+     lights) can widen it per pane; the fallback is the design's own 10px. */
+  padding: 10px 6px 0 var(--dsh-dockkit-strip-inline-start, 10px);
   touch-action: none;
 }
 

+ 25 - 0
packages/client/ui-layout/src/client/AppFrame.module.css

@@ -36,6 +36,31 @@
   overflow: hidden;
 }
 
+/* macOS desktop: the frame goes transparent so window vibrancy reaches the
+   sidebar column, which keeps only a translucent tint; the centre paints the
+   base background itself to stay opaque. The sidebar/centre separator moves
+   to the opaque centre: on the translucent sidebar the alpha border mixes
+   with the wallpaper and reads as a dark seam. */
+:global([data-platform='darwin']) .frame {
+  background: transparent;
+}
+
+:global([data-platform='darwin']) .sidebarCol {
+  background: color-mix(in srgb, var(--dsw-specific-sidebar-fill) 60%, transparent);
+  border-right: none;
+}
+
+:global([data-platform='darwin']) .centerCol {
+  background: var(--dsw-alias-bg-base);
+  border-left: 0.5px solid var(--dsw-alias-border-l3);
+}
+
+/* The darwin closed sidebar has no rail: the centre reaches the window edge,
+   where a separator would draw a seam against nothing. */
+:global([data-platform='darwin']) [data-sidebar-collapsed] .centerCol {
+  border-left: none;
+}
+
 /* Drag handles are frame children (columns clip overflow): an 8px hit strip
    centered on the column border via inline left, above column content. No
    handle draws a visible pill: the details column was the only one that did,

+ 7 - 3
packages/client/ui-layout/src/client/AppFrame.tsx

@@ -19,7 +19,7 @@ import type { ReactNode } from 'react'
 import type {
   PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore,
 } from '@deepseek-ai/dsh-client-ui-slots'
-import { computeColumns, RIGHTBAR_DEFAULT_RATIO, SIDEBAR_AUTO_COLLAPSE, SIDEBAR_DEFAULT } from './columns.ts'
+import { computeColumns, RIGHTBAR_DEFAULT_RATIO, SIDEBAR_AUTO_COLLAPSE, SIDEBAR_COLLAPSED, SIDEBAR_DEFAULT } from './columns.ts'
 import { DocumentTitle } from './DocumentTitle.tsx'
 import type { createLayoutStore } from './stores.ts'
 import css from './AppFrame.module.css'
@@ -163,10 +163,14 @@ export function AppFrame({
     ? 0
     : layoutInfo.sidebar === 0 ? SIDEBAR_DEFAULT : layoutInfo.sidebar
   const rightbarPreference = layoutInfo.rightbar ?? viewport * RIGHTBAR_DEFAULT_RATIO
+  // macOS desktop (data-platform from the Electron preload) hides the closed
+  // sidebar entirely: its reopen controls live in the session header instead
+  // of an icon rail.
+  const collapsedWidth = document.documentElement.dataset.platform === 'darwin' ? 0 : SIDEBAR_COLLAPSED
   // Opening on a narrow frame collapses the left sidebar. Eligibility must
   // include that space before the occupant's first shown report arrives.
-  const normal = computeColumns(viewport, !layoutInfo.rightbarShown && narrow ? 0 : sidebarPreference, rightbarPreference)
-  const cols = computeColumns(viewport, sidebarPreference, layoutInfo.rightbarTrack ? rightbarPreference : 0)
+  const normal = computeColumns(viewport, !layoutInfo.rightbarShown && narrow ? 0 : sidebarPreference, rightbarPreference, collapsedWidth)
+  const cols = computeColumns(viewport, sidebarPreference, layoutInfo.rightbarTrack ? rightbarPreference : 0, collapsedWidth)
   const colsRef = useRef(cols)
   colsRef.current = cols
   const rightbarWidth = useRef(normal.rightbar)

+ 4 - 2
packages/client/ui-layout/src/client/columns.ts

@@ -44,11 +44,13 @@ export function clampWidth(px: number, min: number, max: number): number {
  * @param viewport - available frame width in px.
  * @param sidebar - sidebar width preference in px (0 = closed).
  * @param rightbar - requested right panel width in px (0 = no track).
+ * @param collapsedWidth - track width of the closed sidebar; the default keeps
+ *   the icon rail, 0 hides the column entirely (macOS desktop).
  * @returns actual widths after shrinking or removing the right track; only
  *   without that track may the center fall below its minimum, down to zero.
  */
-export function computeColumns(viewport: number, sidebar: number, rightbar: number): Columns {
-  const s = sidebar === 0 ? SIDEBAR_COLLAPSED : clampWidth(sidebar, SIDEBAR_MIN, SIDEBAR_MAX)
+export function computeColumns(viewport: number, sidebar: number, rightbar: number, collapsedWidth = SIDEBAR_COLLAPSED): Columns {
+  const s = sidebar === 0 ? collapsedWidth : clampWidth(sidebar, SIDEBAR_MIN, SIDEBAR_MAX)
   const available = viewport - s - CENTER_MIN
   const r = rightbar === 0 || available < RIGHTBAR_MIN
     ? 0

+ 18 - 2
packages/client/ui-layout/src/client/theme-presenter.ts

@@ -3,7 +3,8 @@
  * document — `html { color-scheme }` for native UA chrome (scrollbars, form
  * controls), `body[data-ds-dark-theme]` for the token palette, the active
  * theme's alias-token overrides as inline CSS variables on body, the content
- * font-size axis (`--dsh-content-font-size`), and one presenter-owned
+ * font-size axis (`--dsh-content-font-size`), `html[data-ds-theme-source]`
+ * for native-chrome mirroring, and one presenter-owned
  * `meta[name="theme-color"]` for surrounding browser UI. Pure DOM writes, no
  * React involvement; the presenter only ever retracts what it wrote itself,
  * so foreign attributes, metadata, and inline styles survive.
@@ -13,6 +14,15 @@ import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
 /** Body attribute selecting the dark base palette in the token stylesheets. */
 export const DARK_ATTRIBUTE = 'data-ds-dark-theme'
 
+/**
+ * Root attribute publishing the theme source (`light`, `dark`, or `system`)
+ * for host shells that mirror it into native window chrome (the Electron
+ * preload forwards it to `nativeTheme.themeSource` so macOS vibrancy follows
+ * the app theme). `system` only when the preference is `system`; a fixed
+ * preference (including registered theme ids) publishes its resolved scheme.
+ */
+export const THEME_SOURCE_ATTRIBUTE = 'data-ds-theme-source'
+
 /** Body variable carrying the user's content font size in px. */
 export const CONTENT_FONT_SIZE_VARIABLE = '--dsh-content-font-size'
 
@@ -41,6 +51,8 @@ export class ThemePresenter {
   apply(snapshot: ThemeSnapshot): void {
     const scheme = snapshot.active.colorScheme
     document.documentElement.style.colorScheme = scheme
+    document.documentElement.setAttribute(THEME_SOURCE_ATTRIBUTE,
+      snapshot.preference === 'system' ? 'system' : scheme)
     const body = document.body
     if (scheme === 'dark') body.setAttribute(DARK_ATTRIBUTE, '')
     else body.removeAttribute(DARK_ATTRIBUTE)
@@ -55,9 +67,13 @@ export class ThemePresenter {
     if (!this.themeColorMeta.isConnected) document.head.append(this.themeColorMeta)
   }
 
-  /** Retract root color-scheme, the palette attribute, token variables, the font-size axis, and the owned metadata node. */
+  /**
+   * Retract root color-scheme, the theme-source attribute, the palette
+   * attribute, token variables, the font-size axis, and the owned metadata node.
+   */
   dispose(): void {
     document.documentElement.style.removeProperty('color-scheme')
+    document.documentElement.removeAttribute(THEME_SOURCE_ATTRIBUTE)
     const body = document.body
     body.removeAttribute(DARK_ATTRIBUTE)
     body.style.removeProperty(CONTENT_FONT_SIZE_VARIABLE)

+ 20 - 4
packages/client/ui-layout/tests/theme-presenter.client.spec.ts

@@ -1,16 +1,21 @@
 // @vitest-environment jsdom
 
 import { afterEach, beforeEach, describe, expect, it } from 'vitest'
-import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
-import { DARK_ATTRIBUTE, ThemePresenter } from '@deepseek-ai/dsh-client-ui-layout/src/client/theme-presenter.ts'
+import type { ThemePreference, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
+import { DARK_ATTRIBUTE, THEME_SOURCE_ATTRIBUTE, ThemePresenter } from '@deepseek-ai/dsh-client-ui-layout/src/client/theme-presenter.ts'
 
 const LIGHT_THEME_COLOR = 'rgb(255, 255, 255)'
 const DARK_THEME_COLOR = 'rgb(21, 21, 23)'
 
-function snapshot(colorScheme: 'light' | 'dark', tokens: Record<string, string> = {}, fontSize = 14): ThemeSnapshot {
+function snapshot(
+  colorScheme: 'light' | 'dark',
+  tokens: Record<string, string> = {},
+  fontSize = 14,
+  preference: ThemePreference = colorScheme,
+): ThemeSnapshot {
   // The presenter must key off colorScheme, not the id — keep them distinct.
   const active = { id: `${colorScheme}-test`, colorScheme, tokens }
-  return { preference: colorScheme, fontSize, active, themes: [active], revision: 1 }
+  return { preference, fontSize, active, themes: [active], revision: 1 }
 }
 
 function clearThemePresentation(): void {
@@ -24,6 +29,7 @@ function themeColorMeta(): HTMLMetaElement | null {
 beforeEach(() => {
   clearThemePresentation()
   document.documentElement.style.removeProperty('color-scheme')
+  document.documentElement.removeAttribute(THEME_SOURCE_ATTRIBUTE)
   document.body.removeAttribute(DARK_ATTRIBUTE)
   document.body.removeAttribute('style')
   const style = document.createElement('style')
@@ -80,6 +86,16 @@ describe('ThemePresenter', () => {
     expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('17px')
   })
 
+  it('publishes the theme source: system stays system, fixed preferences publish the resolved scheme', () => {
+    const presenter = new ThemePresenter()
+    presenter.apply(snapshot('dark', {}, 14, 'system'))
+    expect(document.documentElement.getAttribute(THEME_SOURCE_ATTRIBUTE)).toBe('system')
+    presenter.apply(snapshot('dark'))
+    expect(document.documentElement.getAttribute(THEME_SOURCE_ATTRIBUTE)).toBe('dark')
+    presenter.dispose()
+    expect(document.documentElement.hasAttribute(THEME_SOURCE_ATTRIBUTE)).toBe(false)
+  })
+
   it('dispose removes color-scheme, the attribute, the font-size axis, and every applied variable, sparing foreign inline styles', () => {
     document.body.style.setProperty('--foreign', 'kept')
     const presenter = new ThemePresenter()

+ 11 - 0
packages/client/ui-primitives/src/darwin-desktop.ts

@@ -0,0 +1,11 @@
+/** macOS desktop detection for hiddenInset-titlebar layout variants. */
+
+/**
+ * Whether the client runs in the macOS desktop shell: the Electron preload
+ * marks `<html>` with `data-platform="darwin"`; plain web never sets it.
+ * Read at render time — the mark may arrive as late as DOMContentLoaded.
+ * @returns true only inside the macOS Electron shell.
+ */
+export function isDarwinDesktop(): boolean {
+  return document.documentElement.dataset.platform === 'darwin'
+}

+ 1 - 0
packages/client/ui-primitives/src/index.ts

@@ -45,6 +45,7 @@ export { fileSizeText } from './file-size.ts'
 export { writeClipboard } from './clipboard.ts'
 export { relativeTime } from './relative-time.ts'
 export { rankByName } from './rank-by-name.ts'
+export { isDarwinDesktop } from './darwin-desktop.ts'
 export type { RelativeTime, RelativeTimeUnit } from './relative-time.ts'
 export { JsonTree } from './JsonTree.tsx'
 export type { JsonTreeProps, JsonTreeLabels } from './JsonTree.tsx'

+ 31 - 0
packages/client/ui-sidebar-right/src/client/shell/SidebarRight.module.css

@@ -51,6 +51,37 @@
   inset: 0;
   z-index: 40;
   border: none;
+  /* Electron drag regions are window geometry, not stacking: the conversation
+     title row's and the sidebar top strip's drag bands underneath keep taking
+     the pointer through this overlay, making the first strip's tabs unclickable.
+     The panel appears after both in DOM order, so this subtracts its whole box;
+     the strip rule below restores dragging on the strip's blank run. */
+  -webkit-app-region: no-drag;
+}
+
+/* macOS fullscreen: the panel reaches the window's top-left corner, where the
+   hiddenInset traffic lights (x 16..~68) float over the first pane's tab
+   strip. Widen that strip's start inset (dockkit publishes the variable);
+   panes below or right of a divider sit clear of the lights, and every
+   non-first split cell resets the inherited value for its whole subtree. */
+:global([data-platform='darwin']) .panel[data-sidebar-right-panel='fullscreen'] .panelBody {
+  /* Lights end near x 68; 88 leaves the same 20px of air the conversation
+     header's leading controls keep after them (ui-sidebar HeaderLeadingControls). */
+  --dsh-dockkit-strip-inline-start: 88px;
+}
+
+:global([data-platform='darwin']) .panel[data-sidebar-right-panel='fullscreen'] .panelBody :global([data-dockkit-cell]):not(:first-child) {
+  --dsh-dockkit-strip-inline-start: 10px;
+}
+
+/* Fullscreen keeps a window drag affordance on the tab strips' blank runs;
+   tabs and strip controls opt back out. */
+:global([data-platform='darwin']) .panel[data-sidebar-right-panel='fullscreen'] :global([data-dockkit-strip]) {
+  -webkit-app-region: drag;
+}
+
+:global([data-platform='darwin']) .panel[data-sidebar-right-panel='fullscreen'] :global([data-dockkit-strip]) > * {
+  -webkit-app-region: no-drag;
 }
 
 @media (prefers-reduced-motion: reduce) {

+ 2 - 2
packages/client/ui-sidebar/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-sidebar/README.md
-README.md: 1b520213809ca98a6819483a4ee5ea4f451841d2
-README.zh.md: ee694d0067db50eff52051d8d9f8fd3baac0fc27
+README.md: 783ef070442922ec4c0b56143b1131a4f381aa11
+README.zh.md: 4bd5e822b41a1b9a305a4605cfa6e0529962d232

+ 4 - 0
packages/client/ui-sidebar/README.md

@@ -39,6 +39,10 @@ Plugins add an icon component to the root-scoped `sidebar.panellist` list with a
 
 During a live collapse, the expanded content fades out at its current width, the upper controls share one fade and leftward translation into the 56px rail, and the layout's column slide ends the motion. A page that starts collapsed renders the rail statically, and reduced-motion mode disables both transitions. The bottom-pinned `sidebar.settings` control shares the fade timing but has no horizontal translation.
 
+### macOS desktop
+
+Under `html[data-platform='darwin']` (set only by the desktop preload) the expanded column opens with a 52px top strip that clears the hiddenInset traffic lights, carries the collapse toggle, and acts as the window drag region; collapsing hides the column entirely instead of leaving the rail. The package registers `HeaderLeadingControls` into the conversation header's `conversation.session.header.leading` seat — the open-sidebar and New Session controls shown, purely via CSS against the AppFrame-published `data-sidebar-collapsed` attribute, only while the column is hidden. Rationale and the window-integration contract: the [macOS hidden-titlebar Agent Note](../../../.agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.md).
+
 ### Scrollbars
 
 Scrollbars in the column are a pointer affordance: the shell rebinds the scrollbar indirection to `transparent` whenever the pointer is outside the column and keeps the thumb drawn for 2s after the pointer leaves, so a list nobody is pointing at carries no bar. The reservation that keeps rows from moving belongs to the scrolling region (ui-workspace), so revealing a thumb never reflows.

+ 4 - 0
packages/client/ui-sidebar/README.zh.md

@@ -39,6 +39,10 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将
 
 实时收起时,展开内容在当前宽度淡出,上方控件共用同一段透明度渐变,并向左平移进入 56px 轨道,由布局的栏滑动结束整段动画。页面初始即为收起状态时会静态渲染轨道;减少动态效果模式会禁用两段过渡。固定在底部的 `sidebar.settings` 控件共用相同的透明度渐变时序,但不发生横向位移。
 
+### macOS 桌面
+
+在 `html[data-platform='darwin']`(仅由桌面 preload 设置)下,展开的侧边栏列顶部有一条 52px 的顶部条:避开 hiddenInset 红绿灯、承载收起按钮,并作为窗口拖拽区;收起时整列隐藏而非保留轨道。本包向会话头部的 `conversation.session.header.leading` 座注册 `HeaderLeadingControls`——打开侧边栏与 New Session 两个控件,纯由 CSS 依据 AppFrame 发布的 `data-sidebar-collapsed` 属性仅在列隐藏时显示。设计依据与窗口集成约定见 [macOS 隐藏标题栏 Agent Note](../../../.agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.zh.md)。
+
 ### 滚动条
 
 栏内的滚动条是一种指针可供性:只要指针不在栏内,外壳就把滚动条间接层重新绑定为 `transparent`;指针离开后滑块再保留 2 秒,因此没人指向的列表不会带着滚动条。避免行位移的空间预留属于滚动区域本身(ui-workspace),所以显示滑块不会引起重排。

+ 2 - 0
packages/client/ui-sidebar/package.json

@@ -30,6 +30,7 @@
       "inject": [
         "@deepseek-ai/dsh-api-workspace-controller",
         "@deepseek-ai/dsh-client-ui-renderer",
+        "@deepseek-ai/dsh-client-ui-conversation",
         "@deepseek-ai/dsh-client-ui-layout",
         "@deepseek-ai/dsh-client-ui-session",
         "@deepseek-ai/dsh-client-ui-workspace",
@@ -51,6 +52,7 @@
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",

+ 50 - 0
packages/client/ui-sidebar/src/client/HeaderLeadingControls.module.css

@@ -0,0 +1,50 @@
+/* Rendered only on macOS desktop (the component returns null elsewhere) and
+   shown only while the sidebar column is fully hidden: expanded, the sidebar
+   holds its own toggle/New Session and the traffic lights sit over the
+   column. */
+.controls {
+  display: none;
+}
+
+/* Left padding clears the hiddenInset traffic lights: they span x 16..~68 in
+   window coordinates and the header already pads 20, so 68 lands the first
+   control at x 88 — a full control-width of air after the lights. Trailing
+   margin restores the leading seat's 8px gap to the breadcrumbs (the seat
+   itself carries none, so the empty expanded state adds no width). */
+:global([data-sidebar-collapsed]) .controls {
+  display: flex;
+  align-items: center;
+  /* Same 8px rhythm as the header's trailing controls (ConversationRoot
+     .headerUtilities/.headerCorner), so both ends of the title row read as one
+     control set. */
+  gap: 8px;
+  padding-left: 68px;
+  margin-right: 8px;
+}
+
+.iconButton {
+  flex: none;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+  padding: 0;
+  border: none;
+  border-radius: 50%;
+  corner-shape: round;
+  background: transparent;
+  color: var(--dsw-alias-label-secondary);
+  cursor: pointer;
+}
+
+.iconButton:hover {
+  background: var(--dsw-alias-interactive-bg-hover);
+}
+
+/* Glyphs match the right-side header controls (ui-sidebar-right ExpandButton):
+   a 15px icon in the 28px circle. */
+.iconButton svg {
+  width: 15px;
+  height: 15px;
+}

+ 53 - 0
packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx

@@ -0,0 +1,53 @@
+/** macOS-desktop conversation-header controls for the fully hidden sidebar. */
+import {
+  IconNewChatOutline16, IconPanelLeftOutline16, isDarwinDesktop, Tooltip,
+} from '@deepseek-ai/dsh-client-ui-primitives'
+import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+// Type-only: pulls the conversation header slot declarations.
+import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
+import type { SidebarRootInjected } from './contract/slots.ts'
+import css from './HeaderLeadingControls.module.css'
+
+/** Full props of the conversation-header leading occupant. */
+export type HeaderLeadingControlsProps =
+  PropsRuntime<'conversation.session.header.leading'>
+  & InjectFace<SidebarRootInjected>
+  & PropsLocale<'sidebar'>
+
+/**
+ * Sidebar-open and New Session controls in the conversation header's leading
+ * seat. On macOS desktop a collapsed sidebar hides entirely (no rail), taking
+ * both controls off screen; this occupant puts them back beside the traffic
+ * lights. Mounted whenever the platform matches; visibility rides the
+ * AppFrame-published `data-sidebar-collapsed` attribute in CSS, so no
+ * collapse-state pipe is added here.
+ * @param props - Injected sidebar actions plus the sidebar locale seat.
+ * @returns the two header controls, or null off macOS desktop.
+ */
+export function HeaderLeadingControls({ toggleSidebar, startSession, t }: HeaderLeadingControlsProps) {
+  if (!isDarwinDesktop()) return null
+  return (
+    <div className={css.controls}>
+      <Tooltip label={t('toggle.open')} delayMs={500}>
+        <button
+          type="button"
+          className={css.iconButton}
+          aria-label={t('toggle.open')}
+          onClick={() => { toggleSidebar() }}
+        >
+          <IconPanelLeftOutline16 size={16} />
+        </button>
+      </Tooltip>
+      <Tooltip label={t('session.new.label')} delayMs={500}>
+        <button
+          type="button"
+          className={css.iconButton}
+          aria-label={t('session.new.label')}
+          onClick={() => { startSession() }}
+        >
+          <IconNewChatOutline16 size={16} />
+        </button>
+      </Tooltip>
+    </div>
+  )
+}

+ 43 - 0
packages/client/ui-sidebar/src/client/SidebarRoot.module.css

@@ -22,6 +22,12 @@
   --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
 }
 
+/* macOS desktop: the AppFrame column owns the translucent sidebar tint; an
+   opaque fill here would sit on top of it and block the window vibrancy. */
+:global([data-platform='darwin']) .root {
+  background: transparent;
+}
+
 /* Rail geometry (figma rail spec): 36x36 control boxes centered in the 56px
    rail (10px side padding), 12px vertical rhythm, 18px from the rail top to
    the whale's box (24px to the 24-wide whale glyph itself). */
@@ -85,6 +91,32 @@
   from { opacity: 0; }
 }
 
+/* macOS hiddenInset titlebar: the strip spans the column's top edge (negative
+   margins cancel the root padding) so it clears the traffic lights at
+   (16, 18) and doubles as the window drag region; the toggle opts out. */
+.topStrip {
+  flex: none;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  height: 52px;
+  box-sizing: border-box;
+  margin: -6px calc(-1 * var(--dsh-sidebar-inline-padding)) 0;
+  padding: 0 12px;
+  -webkit-app-region: drag;
+}
+
+.topStrip .iconButton {
+  -webkit-app-region: no-drag;
+}
+
+/* The strip's full 52px foot leaves the wordmark too low; the content column
+   tucks 12px back under it, still clear of the toggle (28px control centered
+   in the strip ends at y 40). */
+.topStrip + .logoRow {
+  margin-top: -12px;
+}
+
 /* Logo row (figma pad (4,8,4,8)): expanded keeps the panel toggle at the
    right edge; the rail gives every control the same base left anchor before
    the shared entry translation is applied. */
@@ -272,6 +304,17 @@
   background: var(--dsw-alias-button-floating-hover);
 }
 
+/* macOS: the solid elevated fill reads as a foreign slab on the vibrancy
+   sidebar; letting a quarter of the material through keeps the card visible
+   while it sits in the glass. */
+:global([data-platform='darwin']) .newSession {
+  background: color-mix(in srgb, var(--dsw-alias-button-elevated-fill) 75%, transparent);
+}
+
+:global([data-platform='darwin']) .newSession:hover {
+  background: color-mix(in srgb, var(--dsw-alias-button-floating-hover) 75%, transparent);
+}
+
 .collapsed .newSession {
   align-self: flex-start;
   width: 36px;

+ 28 - 19
packages/client/ui-sidebar/src/client/SidebarRoot.tsx

@@ -19,7 +19,7 @@
 import { useEffect, useRef, useState } from 'react'
 import clsx from 'clsx'
 import {
-  FishLogo, IconNewChatOutline16, IconPanelLeftOutline16, Tooltip,
+  FishLogo, IconNewChatOutline16, IconPanelLeftOutline16, isDarwinDesktop, Tooltip,
 } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { InjectFace, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import type {
@@ -163,6 +163,29 @@ export function SidebarRoot({
 
   const buildVersion = localBuildVersion()
 
+  const darwinDesktop = isDarwinDesktop()
+  // Rail resting state is the whale mark; hovering swaps in the panel icon
+  // (the expand affordance, figma sidebar-hover flow). Expanded it is a plain
+  // panel icon.
+  const toggle = (
+    <Tooltip label={collapsed ? t('toggle.open') : t('toggle.collapse')} delayMs={500}>
+      <button
+        type="button"
+        className={clsx(css.iconButton, css.toggle)}
+        aria-label={collapsed ? t('toggle.open') : t('toggle.collapse')}
+        onClick={() => { toggleSidebar() }}
+      >
+        {!wide && (
+          <span className={css.railMark} aria-hidden="true">
+            {renderSlot('sidebar.brand.mark', { size: 24 }, { fallback: <FishLogo size={24} /> })}
+          </span>
+        )}
+        {/* Rail icons render at 18 (figma rail spec); expanded keeps the glyph-native sizes. */}
+        <IconPanelLeftOutline16 className={css.panelIcon} size={wide ? 16 : 18} />
+      </button>
+    </Tooltip>
+  )
+
   return (
     <div
       ref={column}
@@ -177,6 +200,9 @@ export function SidebarRoot({
       }}
       onPointerLeave={() => { armLinger() }}
     >
+      {/* macOS hiddenInset titlebar: the strip shares the row with the
+          traffic lights and keeps the toggle at the sidebar's top-right. */}
+      {darwinDesktop && <div className={css.topStrip}>{toggle}</div>}
       <div className={css.logoRow}>
         {/* Expanded, the brand doubles as a New Session shortcut; the
             collapsed rail's logo is the expand toggle below instead. */}
@@ -206,24 +232,7 @@ export function SidebarRoot({
             </span>
           </button>
         )}
-        {/* Rail resting state is the whale mark; hovering swaps in the panel
-            icon (the expand affordance, figma sidebar-hover flow). */}
-        <Tooltip label={collapsed ? t('toggle.open') : t('toggle.collapse')} delayMs={500}>
-          <button
-            type="button"
-            className={clsx(css.iconButton, css.toggle)}
-            aria-label={collapsed ? t('toggle.open') : t('toggle.collapse')}
-            onClick={() => { toggleSidebar() }}
-          >
-            {!wide && (
-              <span className={css.railMark} aria-hidden="true">
-                {renderSlot('sidebar.brand.mark', { size: 24 }, { fallback: <FishLogo size={24} /> })}
-              </span>
-            )}
-            {/* Rail icons render at 18 (figma rail spec); expanded keeps the glyph-native sizes. */}
-            <IconPanelLeftOutline16 className={css.panelIcon} size={wide ? 16 : 18} />
-          </button>
-        </Tooltip>
+        {!darwinDesktop && toggle}
       </div>
 
       {/* Expanded, the button carries its own label — tooltip only on the rail. */}

+ 12 - 0
packages/client/ui-sidebar/src/client/index.ts

@@ -9,7 +9,10 @@ import type {} from '@deepseek-ai/dsh-client-locale/client'
 import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
 // Type-only: pulls the Session root standard-props merge.
 import type {} from '@deepseek-ai/dsh-client-ui-session/client'
+// Type-only: pulls the conversation header slot declarations.
+import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
 import type { SidebarPanelMetadata, SidebarRootInjected } from './contract/slots.ts'
+import { HeaderLeadingControls } from './HeaderLeadingControls.tsx'
 import { SidebarRoot } from './SidebarRoot.tsx'
 import { en, zh, type SidebarKey } from './locales.ts'
 
@@ -81,5 +84,14 @@ export function apply(ctx: ClientContext): void {
     },
     inject: injectProps,
   }, SidebarRoot))
+  // macOS desktop hides the collapsed sidebar entirely, so the open/New
+  // Session controls move into the conversation header's leading seat; the
+  // occupant reuses the shell's injected actions and shows itself purely
+  // through CSS against the AppFrame's data-sidebar-collapsed attribute.
+  ctx.slots.inject('conversation.session.header.leading', () => ctx.slots.register({
+    name: 'conversation.session.header.leading',
+    locale: NS,
+    inject: injectProps,
+  }, HeaderLeadingControls))
   syncPanels()
 }

+ 9 - 0
packages/client/ui-sidebar/tests/apply.client.spec.tsx

@@ -7,6 +7,7 @@ import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
 import { apply, inject } from '@deepseek-ai/dsh-client-ui-sidebar/client'
 import type { SidebarRootInjected } from '@deepseek-ai/dsh-client-ui-sidebar/client'
 import type { MainPanelId } from '@deepseek-ai/dsh-client-ui-layout/client'
+import { HeaderLeadingControls } from '../src/client/HeaderLeadingControls.tsx'
 import { apply as hostApply } from '../src/index.ts'
 
 const owners = new Set<Fiber>()
@@ -41,6 +42,7 @@ async function bench(declare = true) {
       { name: 'root', children: {
         'sidebar': { kind: 'single', scope: 'root' },
         'main': { kind: 'keyed', scope: 'root' },
+        'conversation.session.header.leading': { kind: 'single', scope: 'session' },
       } },
       SidebarFrame,
     )
@@ -69,6 +71,12 @@ describe('ui-sidebar apply', () => {
     expect(b.slots.spec('sidebar.panellist')).toEqual({ kind: 'list', scope: 'root' })
     // Copy rides the standard locale seat, not the inject face.
     expect(b.slots.entries('sidebar')[0]!.locale).toBe('sidebar')
+    // The header leading occupant reuses the shell's inject face and locale.
+    const leading = b.slots.entries('conversation.session.header.leading')
+    expect(leading).toHaveLength(1)
+    expect(leading[0]!.component).toBe(HeaderLeadingControls)
+    expect(leading[0]!.locale).toBe('sidebar')
+    expect(leading[0]!.inject).toBe(b.slots.entries('sidebar')[0]!.inject)
     const injected = (b.slots.entries('sidebar')[0]!.inject as () => SidebarRootInjected)()
     expect(Object.keys(injected)).toEqual(['startSession', 'toggleSidebar', 'selectPanel', 'hooks'])
     expect(injected.hooks.panels.getSnapshot()).toEqual([])
@@ -137,6 +145,7 @@ describe('ui-sidebar apply', () => {
     await fiber.await()
     await fiber.dispose()
     expect(b.slots.entries('sidebar')).toHaveLength(0)
+    expect(b.slots.entries('conversation.session.header.leading')).toHaveLength(0)
     expect(b.slots.spec('sidebar.brand.mark')).toBeUndefined()
     expect(b.slots.spec('sidebar.brand.name')).toBeUndefined()
     expect(b.slots.spec('sidebar.workspaces')).toBeUndefined()

+ 3 - 0
packages/client/ui-sidebar/tsconfig.json

@@ -23,6 +23,9 @@
     {
       "path": "../ui-primitives"
     },
+    {
+      "path": "../ui-conversation"
+    },
     {
       "path": "../ui-renderer"
     },

+ 4 - 1
packages/client/ui-theme/src/boot-theme.ts

@@ -2,7 +2,9 @@
  * Theme bootstrap row for the browser's pre-plugin interval. Each index
  * render embeds the current durable built-in preference and content font size;
  * the browser resolves only `system`, then writes the same DOM fields
- * ui-layout's ThemePresenter owns after the client plugin tree activates.
+ * ui-layout's ThemePresenter owns after the client plugin tree activates,
+ * including `html[data-ds-theme-source]` (the unresolved preference) for
+ * host shells that mirror it into native window chrome.
  */
 
 import type { IndexInjection } from '@deepseek-ai/dsh-host-webserver'
@@ -17,6 +19,7 @@ function bootThemeScript(preference: ThemePreference, fontSize: number): string
     && matchMedia('(prefers-color-scheme: dark)').matches
   const dark = preference === 'dark' || systemDark
   document.documentElement.style.colorScheme = dark ? 'dark' : 'light'
+  document.documentElement.dataset.dsThemeSource = preference
   document.body.toggleAttribute('data-ds-dark-theme', dark)
   document.body.style.setProperty('--dsh-content-font-size', ${JSON.stringify(`${fontSize}px`)})
 })()`

+ 3 - 0
packages/client/ui-theme/tests/boot-theme.client.spec.ts

@@ -21,6 +21,7 @@ afterEach(() => {
   vi.restoreAllMocks()
   vi.unstubAllGlobals()
   document.documentElement.style.removeProperty('color-scheme')
+  delete document.documentElement.dataset.dsThemeSource
   document.body.removeAttribute(DARK_ATTRIBUTE)
   document.body.style.removeProperty('--dsh-content-font-size')
 })
@@ -32,6 +33,7 @@ describe('theme bootstrap row', () => {
     expect(row).toMatchObject({ kind: 'script', placement: 'body' })
     executeBootstrap('dark')
     expect(document.documentElement.style.colorScheme).toBe('dark')
+    expect(document.documentElement.dataset.dsThemeSource).toBe('dark')
     expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(true)
   })
 
@@ -50,6 +52,7 @@ describe('theme bootstrap row', () => {
     mockSystemDark(matches)
     executeBootstrap('system')
     expect(document.documentElement.style.colorScheme).toBe(colorScheme)
+    expect(document.documentElement.dataset.dsThemeSource).toBe('system')
     expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(dark)
   })
 

+ 6 - 0
packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.module.css

@@ -319,6 +319,12 @@
   pointer-events: none;
 }
 
+/* macOS desktop: the sidebar is translucent over window vibrancy, so the
+   opaque fill gradient would paint a solid smear instead of a fade. */
+:global([data-platform='darwin']) .fade {
+  display: none;
+}
+
 /* Wide-only content fades back in on expand remount (mirrors the shell). */
 .wide {
   animation: wide-in 200ms var(--ds-ease-in-out);

+ 7 - 0
packages/client/web/src/base.css

@@ -32,6 +32,13 @@ body {
   text-autospace: normal;
 }
 
+/* macOS desktop (the Electron preload sets data-platform): the window's
+   sidebar vibrancy shows only through a transparent page background. */
+html[data-platform='darwin'],
+html[data-platform='darwin'] body {
+  background: transparent;
+}
+
 code,
 pre,
 [data-diff],

+ 54 - 16
packages/extensions/cordis-client-runner/src/client/slot-catalog.ts

@@ -387,7 +387,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.composer\', () => ctx.slots.register(\n      { name: \'conversation.composer\', select: owner => null },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:158',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:170',
   },
   {
     key: 'conversation.composer.bar',
@@ -425,7 +425,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'shadows-shipped-ui',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.composer.bar\', () => ctx.slots.register(\n      { name: \'conversation.composer.bar\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:176',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:188',
   },
   {
     key: 'conversation.composer.dock',
@@ -480,7 +480,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.composer.dock\', () => ctx.slots.register(\n      { name: \'conversation.composer.dock\', id: \'my-entry\', order: 100, label: \'My entry\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:170',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:182',
   },
   {
     key: 'conversation.hero.agentPreset',
@@ -510,7 +510,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'shadows-shipped-ui',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.hero.agentPreset\', () => ctx.slots.register(\n      { name: \'conversation.hero.agentPreset\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:164',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:176',
   },
   {
     key: 'conversation.hero.brand.mark',
@@ -538,7 +538,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     occupants: [],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.hero.brand.mark\', () => ctx.slots.register(\n      { name: \'conversation.hero.brand.mark\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:162',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:174',
   },
   {
     key: 'conversation.hero.workspace',
@@ -570,7 +570,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'shadows-shipped-ui',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.hero.workspace\', () => ctx.slots.register(\n      { name: \'conversation.hero.workspace\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:160',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:172',
   },
   {
     key: 'conversation.hero.workspace.directoryFlow',
@@ -641,7 +641,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'shadows-shipped-ui',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.input.attachments\', () => ctx.slots.register(\n      { name: \'conversation.input.attachments\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:178',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:190',
   },
   {
     key: 'conversation.input.dock',
@@ -703,7 +703,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.input.dock\', () => ctx.slots.register(\n      { name: \'conversation.input.dock\', id: \'my-entry\', order: 100, label: \'My entry\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:166',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:178',
   },
   {
     key: 'conversation.input.left',
@@ -756,7 +756,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     occupants: [],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.input.left\', () => ctx.slots.register(\n      { name: \'conversation.input.left\', id: \'my-entry\', order: 100, label: \'My entry\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:172',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:184',
   },
   {
     key: 'conversation.input.model',
@@ -794,7 +794,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'shadows-shipped-ui',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.input.model\', () => ctx.slots.register(\n      { name: \'conversation.input.model\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:188',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:200',
   },
   {
     key: 'conversation.input.overlay',
@@ -851,7 +851,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.input.overlay\', () => ctx.slots.register(\n      { name: \'conversation.input.overlay\', id: \'my-entry\', order: 100, label: \'My entry\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:168',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:180',
   },
   {
     key: 'conversation.input.permission',
@@ -889,7 +889,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'shadows-shipped-ui',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.input.permission\', () => ctx.slots.register(\n      { name: \'conversation.input.permission\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:186',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:198',
   },
   {
     key: 'conversation.input.plan',
@@ -927,7 +927,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'shadows-shipped-ui',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.input.plan\', () => ctx.slots.register(\n      { name: \'conversation.input.plan\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:184',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:196',
   },
   {
     key: 'conversation.input.right',
@@ -980,7 +980,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     occupants: [],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.input.right\', () => ctx.slots.register(\n      { name: \'conversation.input.right\', id: \'my-entry\', order: 100, label: \'My entry\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:174',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:186',
   },
   {
     key: 'conversation.message.images',
@@ -1192,7 +1192,45 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'shadows-shipped-ui',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.session.header.corner\', () => ctx.slots.register(\n      { name: \'conversation.session.header.corner\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:150',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:162',
+  },
+  {
+    key: 'conversation.session.header.leading',
+    kind: 'single',
+    scope: 'session',
+    summary: 'Leading seat before the Session breadcrumbs, for window-chrome-adjacent controls (macOS desktop sidebar reopen and New Session while the sidebar is hidden).',
+    doc: 'Leading seat before the Session breadcrumbs, for window-chrome-adjacent\ncontrols (macOS desktop sidebar reopen and New Session while the sidebar\nis hidden). The seat is laid out only while its occupant renders\nsomething, and it stays mounted through the blank-session state so a\nhidden sidebar always keeps a reopen control on screen.',
+    registerOptions: [],
+    ownerProps: [
+      '/** The leading seat\'s occupant derives its state from standard Session props. */\nexport interface ConversationHeaderLeadingOwnerProps {\n  /** Marker field: the occupant receives no owner-specific values. */\n  children?: never\n}',
+    ],
+    ownerPropsReferences: [],
+    standardProps: [
+      'useResource: UseResource',
+      'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
+      'usePanelInfo: UsePanelInfo',
+      'useSessions: UseSessions',
+      'useSessionPendingInteraction: UseSessionPendingInteraction',
+      'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
+      'useChat: UseChat',
+      'useConversation: UseConversation',
+      'useInput: SnapshotSelectorHook<InputState>',
+      'inputActions: InputActions',
+      'useSession: SessionSnapshotSelector',
+      'sessionId: SessionId',
+      'useProjection: UseProjection',
+      'useTrajectory: UseTrajectory',
+    ],
+    keyDomain: '',
+    hookContext: '',
+    slotInject: '',
+    declaredBy: 'an entry in \'conversation.session.header\' (client-ui-conversation), so it exists while that entry is mounted',
+    occupants: [
+      'client-ui-sidebar HeaderLeadingControls',
+    ],
+    replaceRisk: 'shadows-shipped-ui',
+    example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.session.header.leading\', () => ctx.slots.register(\n      { name: \'conversation.session.header.leading\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:151',
   },
   {
     key: 'conversation.session.header.lineage',
@@ -1392,7 +1430,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     ],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'conversation.view\', () => ctx.slots.register(\n      { name: \'conversation.view\', id: \'my-entry\', order: 100, label: \'My entry\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
-    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:156',
+    source: 'packages/client/ui-conversation/src/client/contract/slots.ts:168',
   },
   {
     key: 'main',

+ 6 - 3
pnpm-lock.yaml

@@ -519,7 +519,7 @@ importers:
         version: link:../../packages/util/home-paths
       '@electron/get':
         specifier: ^5.1.0
-        version: 5.1.0
+        version: 5.1.0(supports-color@9.4.0)
       '@electron/notarize':
         specifier: 2.5.0
         version: 2.5.0
@@ -3854,6 +3854,9 @@ importers:
       '@deepseek-ai/dsh-client-test-runtime':
         specifier: workspace:^
         version: link:../../test-support/client-runtime
+      '@deepseek-ai/dsh-client-ui-conversation':
+        specifier: workspace:^
+        version: link:../ui-conversation
       '@deepseek-ai/dsh-client-ui-layout':
         specifier: workspace:^
         version: link:../ui-layout
@@ -19534,7 +19537,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@electron/get@5.1.0':
+  '@electron/get@5.1.0(supports-color@9.4.0)':
     dependencies:
       debug: 4.4.3(supports-color@9.4.0)
       env-paths: 3.0.0
@@ -22662,7 +22665,7 @@ snapshots:
   electron@44.0.0:
     dependencies:
       '@electron-internal/extract-zip': 1.0.5
-      '@electron/get': 5.1.0
+      '@electron/get': 5.1.0(supports-color@9.4.0)
       '@types/node': 24.13.3
     transitivePeerDependencies:
       - supports-color