fix(client): keep sidebar navigation available without a session

This commit is contained in:
07akioni committed 2026-09-17 19:38:57 +08:00
1 parent 9e3a30f85a
commit 1628e911c8
25 files changed
+230 -121

No files matched your search

@@ -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 .agents/notes/implemented/feature/2026-09-13-macos-hidden-titlebar-vibrancy.md
2026-09-13-macos-hidden-titlebar-vibrancy.md: 1bc531953cedac8633c4baa6da45aa9bcd77a728
2026-09-13-macos-hidden-titlebar-vibrancy.zh.md: bbc593badbabdccaa037cca836a9554929d614bc
2026-09-13-macos-hidden-titlebar-vibrancy.md: 99ac240698fb229f9a0d66a8000251058b52491e
2026-09-13-macos-hidden-titlebar-vibrancy.zh.md: 26bbb616485e49de2f70a3d142ea84dfd5bd7cb9
@@ -18,7 +18,7 @@ Every macOS web-side adjustment keys off `html[data-platform='darwin']`, which o
**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, including its local static Web document) 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.
**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 single, root-scoped slot `conversation.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 resident header keeps the leading seat mounted on darwin with or without a selected Session 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.
@@ -40,10 +40,10 @@ Every macOS web-side adjustment keys off `html[data-platform='darwin']`, which o
- 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.
- `conversation.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.
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 with and without a selected Session. The ui-theme corner-shape and full-round style gates cover the new stylesheet.
@@ -18,7 +18,7 @@ Electron 主进程在 darwin 上以 `titleBarStyle: 'hiddenInset'`、`trafficLig
**原生主题同步。** 毛玻璃材质跟随 `nativeTheme.themeSource`,后者默认跟踪系统外观,会与应用自身的主题偏好背离。ui-theme 引导脚本与 ui-layout 的 `ThemePresenter` 发布 `html[data-ds-theme-source]`(`light`、`dark` 或 `system`;固定偏好——包括注册主题 id——发布其解析后的配色)。应用 preload 观察该属性并经 `dsh-desktop:native-theme-set` 转发;主进程校验取值与发送者(主窗口的 WebContents,包括其本地静态 Web 文档)后赋给 `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 上侧边栏展开时有一条 52px 的顶部条,避开红绿灯、承载收起按钮,并作为窗口拖拽区(`-webkit-app-region: drag`;按钮退出拖拽)。收起侧边栏时整列隐藏——`computeColumns` 接受显式 `collapsedWidth`,AppFrame 在 darwin 桌面传 0——而非其他平台保留的 56px rail。重新打开的入口移入会话头部:single、root 作用域的 slot `conversation.header.leading` 位于面包屑之前,ui-sidebar 向其注册 `HeaderLeadingControls`(打开侧边栏 + 新会话,复用 shell 的 inject face 与 locale)。显隐纯由 CSS 依据 AppFrame 发布的 `data-sidebar-collapsed` 属性控制;不新增收起状态管道。darwin 上常驻头部在选中或未选中 Session 时均保持 leading 座挂载,确保侧边栏隐藏时屏幕上始终有重新打开的控件。
**拖拽区。** darwin 上会话标题行是拖拽区,所有可交互后代退出。Electron 按 DOM 顺序以窗口几何计算拖拽区,不看层叠:覆盖标题带的浮层必须自行减除,否则下层区域仍会截获指针。因此右侧边栏的全屏面板对整个盒子设 `-webkit-app-region: no-drag`,再在其 tab 条空白处恢复拖拽。
@@ -40,10 +40,10 @@ Electron 主进程在 darwin 上以 `titleBarStyle: 'hiddenInset'`、`trafficLig
- macOS 窗口获得半透明侧边栏与隐藏标题栏,对其他平台零成本:所有规则限定在 `[data-platform='darwin']` 下,该属性仅由 Electron preload 设置。
- 毛玻璃材质跟随应用主题,含第三方注册主题(取其解析配色)。截图与录屏与纯 Web 的平面渲染不同。
- `conversation.session.header.leading` 是客户端 catalog 中的公开 slot;任何包都可占用该座位,ui-sidebar 的占用者假定平台匹配时随时可能渲染。
- `conversation.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 样式门覆盖新样式表。
ui-theme 引导与 ui-layout presenter 测试钉住 `data-ds-theme-source` 的发布与清除。ui-sidebar apply 测试钉住 leading 座注册(组件、locale、共享 inject face)及 teardown 移除。ui-conversation skeleton 测试钉住 选中或未选中 Session 时对 leading slot 的渲染调用。ui-theme 的 corner-shape 与 full-round 样式门覆盖新样式表。
@@ -139,9 +139,29 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
// The Desktop preload marks the platform before client plugins render.
await page.addInitScript(() => {
document.addEventListener('DOMContentLoaded', () => {
document.documentElement.setAttribute('data-platform', 'darwin')
}, { once: true })
})
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await appFrame(page).waitFor({ timeout: 30_000 })
expect(await page.locator('[data-sidebar-right-expand]').count()).toBe(0)
await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).click()
const leading = page.locator('[data-conversation-header-leading]')
await leading.getByRole('button', { name: 'Open sidebar', exact: true }).waitFor({ state: 'visible' })
expect(await leading.getByRole('button', { name: 'New session', exact: true }).isVisible()).toBe(true)
expect(await page.getByRole('tablist').count()).toBe(0)
await leading.getByRole('button', { name: 'Open sidebar', exact: true }).click()
await page.getByRole('button', { name: 'Collapse sidebar', exact: true }).waitFor({ state: 'visible' })
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
await page.close()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await appFrame(page).waitFor({ timeout: 30_000 })
await connectFreshWorkspace(page, scaffold.workspaceCwd)
}, 120_000)
+2 -2
View File
@@ -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: a9f0ee6ab2ef37065ab965a7989f5989350bca8b
slots.zh.md: da6a38eab6aac1c2670b131fa49a832ce41f711e
slots.md: 80894c0d6e5bcd8aae1e5db319849076aae38fde
slots.zh.md: a6e1c85747efacc5ea14ec2dbe8e114efc47ea47
+7 -6
View File
@@ -144,12 +144,13 @@ root
│ │ │ └─ tool.view.cordis
│ │ ├─ conversation.message.images
│ │ └─ 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
│ ├─ conversation.header
│ │ ├─ conversation.header.leading (root)
│ │ └─ conversation.session.header
│ │ ├─ conversation.session.header.lineage
│ │ ├─ conversation.session.header.actions
│ │ ├─ conversation.session.header.utilities
│ │ └─ conversation.session.header.corner
│ ├─ conversation.composer
│ │ ├─ conversation.approval.detail
│ │ └─ conversation.plan-review.actions
+7 -6
View File
@@ -144,12 +144,13 @@ root
│ │ │ └─ tool.view.cordis
│ │ ├─ conversation.message.images
│ │ └─ 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
│ ├─ conversation.header
│ │ ├─ conversation.header.leading (root)
│ │ └─ conversation.session.header
│ │ ├─ conversation.session.header.lineage
│ │ ├─ conversation.session.header.actions
│ │ ├─ conversation.session.header.utilities
│ │ └─ conversation.session.header.corner
│ ├─ conversation.composer
│ │ ├─ conversation.approval.detail
│ │ └─ conversation.plan-review.actions
@@ -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-conversation/README.md
README.md: bc3b582f9d80e97c29e27dcaa680dc064ec30deb
README.zh.md: 21881e7933cdf46cf9e27a66f076fc40c48e4b1a
README.md: e86daa8c41afcac67611cbdc5f772f355ec63c9f
README.zh.md: 2b6ab1c5c5dc9f7217f0899acb162a2f14379768
+1 -1
View File
@@ -48,7 +48,7 @@ Claimed commands retain their identity and highlight when only their arguments a
Workspace selection uses `uiWorkspace.openWorkspace` to prepare the target and commit navigation. Draft text and attachments move in its synchronous preparation callback only while that request is current; later navigation or owner disposal leaves the original draft intact.
The package occupies the root-scoped `main` key `conversation`. Its `main.conversation` shell keeps the strict Session Header outside the optional-Session `conversation.content` Component Factory. The Factory owns the shared body and Composer, reads the current Session through its standard Hook, and exposes strict-Session `views` plus root-scoped `widthControls` local positions. Its default adapter renders the existing `conversation.session` entry, while the main occurrence selects the width handles; an embedded occurrence can replace `views` and omit those handles without rendering the main Header. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop.
The package occupies the root-scoped `main` key `conversation`. Its `main.conversation` shell keeps the resident `conversation.header` outside the optional-Session `conversation.content` Component Factory. The header hosts root-scoped navigation without a selected Session; titles, actions, and View tabs remain in its strict Session child. The Factory owns the shared body and Composer, reads the current Session through its standard Hook, and exposes strict-Session `views` plus root-scoped `widthControls` local positions. Its default adapter renders the existing `conversation.session` entry, while the main occurrence selects the width handles; an embedded occurrence can replace `views` and omit those handles without rendering the main Header. `ctx.uiSession.provide()` materializes the Conversation and input sources from the same Session binding and supplies `inputActions` as a stable standard prop.
A blank Session retains the header's leading and corner controls, including the right-sidebar opener, while hiding its title, actions, utilities, and View tabs. Selecting a Workspace creates the Session needed by these controls; the first message is not required. Without a selected Session, the strict header is absent. Sidebar entries retain their own data and execution prerequisites.
+1 -1
View File
@@ -48,7 +48,7 @@ target package 通过 declaration merge 扩展 snapshot 与 Location data map,
工作区选择使用 `uiWorkspace.openWorkspace` 准备目标并提交导航。草稿文字和附件仅在该请求仍为当前请求时,通过它的同步准备回调搬移;后续导航或所有者释放会保留原草稿。
本包占据 root 作用域 `main` 中的 `conversation` key。其 `main.conversation` shell 将 strict Session Header 保留在 optional-Session `conversation.content` Component Factory 外。Factory 拥有共享正文与 Composer,通过其标准 Hook 读取当前 Session,并公开 strict-Session `views` 与 root-scoped `widthControls` 两个局部位置。默认 adapter 渲染现有 `conversation.session` entry,主 occurrence 选择宽度拖拽条;嵌入式 occurrence 可以替换 `views`、省略拖拽条,且不渲染主 Header。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。
本包占据 root 作用域 `main` 中的 `conversation` key。其 `main.conversation` 外壳将常驻的 `conversation.header` 放在可选 Session 的 `conversation.content` Component Factory 外。未选中 Session 时,头部仍承载根作用域导航;标题、操作和 View 标签保留在严格 Session 子组件中。Factory 拥有共享正文与 Composer,通过其标准 Hook 读取当前 Session,并公开 strict-Session `views` 与 root-scoped `widthControls` 两个局部位置。默认 adapter 渲染现有 `conversation.session` entry,主 occurrence 选择宽度拖拽条;嵌入式 occurrence 可以替换 `views`、省略拖拽条,且不渲染主 Header。`ctx.uiSession.provide()` 从同一个 Session binding 物化 Conversation 与 input source,并将 `inputActions` 作为稳定标准 prop 提供。
blank Session 保留 header 的 leading 与 corner 控件,包括右侧栏展开入口,同时隐藏标题、actions、utilities 和 View tabs。选择 Workspace 会创建这些控件所需的 Session,无需先发送消息。没有选中 Session 时,strict header 不挂载。侧栏各入口仍遵循自身的数据与执行环境要求。
@@ -31,6 +31,7 @@ import type { EnterBehaviorRowInjected } from './settings/EnterBehaviorRow.tsx'
import { ConversationRoot } from './skeleton/ConversationRoot.tsx'
import { ConversationContent } from './skeleton/ConversationContent.tsx'
import { ConversationPanel } from './skeleton/ConversationPanel.tsx'
import { ConversationHeader } from './skeleton/ConversationHeader.tsx'
import { ConversationSession, ConversationSessionHeader } from './skeleton/ConversationSession.tsx'
import { InputBar } from './skeleton/InputBar.tsx'
import { todoDockEntry } from './skeleton/TodoPanel.tsx'
@@ -238,7 +239,7 @@ export function apply(ctx: Context, config: Config = Config({})): void {
const registerConversationRoot = () => slots.register({
name: 'main.conversation',
children: {
'conversation.session.header': { kind: 'single', scope: 'session' },
'conversation.header': { kind: 'single', scope: 'session-maybe' },
},
}, ConversationRoot)
@@ -303,12 +304,19 @@ export function apply(ctx: Context, config: Config = Config({})): void {
}),
}, ConversationSession)
const registerHeader = () => slots.register({
name: 'conversation.header',
children: {
'conversation.header.leading': { kind: 'single', scope: 'root' },
'conversation.session.header': { kind: 'single', scope: 'session' },
},
}, ConversationHeader)
const registerConversationHeader = () => slots.register({
name: 'conversation.session.header',
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' },
@@ -419,6 +427,7 @@ export function apply(ctx: Context, config: Config = Config({})): void {
yield registerConversationRoot()
yield registerConversationContent()
yield registerConversationSession()
yield registerHeader()
yield registerConversationHeader()
yield registerComposerBar()
})
@@ -131,6 +131,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
scope: 'session'
owner: { view?: string }
}
/** Resident navigation container, including when no Session is selected. */
'conversation.header': { kind: 'single'; scope: 'session-maybe' }
/** Strict per-Session title, actions, and View navigation. */
'conversation.session.header': { kind: 'single'; scope: 'session' }
/** Optional replacement for one Session breadcrumb title. */
@@ -151,16 +153,10 @@ 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': {
/** Global navigation before the Session title, available without a Session. */
'conversation.header.leading': {
kind: 'single'
scope: 'session'
scope: 'root'
owner: ConversationHeaderLeadingOwnerProps
}
/**
@@ -275,7 +271,7 @@ export interface ConversationHeaderCornerOwnerProps {
children?: never
}
/** The leading seat's occupant derives its state from standard Session props. */
/** The leading seat exposes global navigation independently of a Session. */
export interface ConversationHeaderLeadingOwnerProps {
/** Marker field: the occupant receives no owner-specific values. */
children?: never
@@ -419,7 +415,7 @@ export interface HeroBrandMarkOwnerProps {
/** Full props of the resident optional-Session Conversation shell. */
export type ConversationSlotProps =
PropsRuntime<'main.conversation'>
& PropsRenderSlots<'conversation.session.header'>
& PropsRenderSlots<'conversation.header'>
& PropsRenderFactories
/** Inputs shared by main and embedded Conversation content occurrences. */
@@ -457,12 +453,16 @@ export type ConversationSessionSlotProps =
& PropsStore<ConversationStore>
& InjectFace<ConversationSessionInjected>
/** Full props of the resident navigation header. */
export type ConversationHeaderProps =
PropsRuntime<'conversation.header'>
& PropsRenderSlots<'conversation.header.leading' | 'conversation.session.header'>
/** Full props of the strict Session header. */
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'
@@ -0,0 +1,27 @@
/** Resident conversation navigation and Session-specific header content. */
import clsx from 'clsx'
import type { ConversationHeaderProps } from '../contract/slots.ts'
import { conversationPhase } from '../contract/snapshot.ts'
import css from './ConversationRoot.module.css'
/**
* Keeps global navigation available before a Session exists.
* @param props - Optional Session sources and authorized header slots.
* @returns The persistent header with any selected Session's title and views.
*/
export function ConversationHeader({ sessionId, useSession, useConversation, renderSlot }: ConversationHeaderProps) {
const session = useSession(s => s)
const conversation = useConversation(s => s)
const blank = session === undefined || conversation === undefined
|| (session.blank && conversationPhase(session, conversation) === 'blank')
return (
<header className={clsx(css.header, blank && css.headerBlank)}>
<div className={css.headerLeading} data-conversation-header-leading="">
{renderSlot('conversation.header.leading', {})}
</div>
{sessionId === undefined
? <div className={css.titleRow} />
: renderSlot('conversation.session.header', {})}
</header>
)
}
@@ -42,7 +42,7 @@ export function ConversationMainPanel(props: ConversationSlotProps) {
return (
<div className={css.root} data-phase={phase}>
{sessionId === undefined ? null : renderSlot('conversation.session.header', {})}
{renderSlot('conversation.header', {})}
{renderFactorySlot('conversation.content', {
variant: 'main',
phase,
@@ -13,6 +13,8 @@
title row, the tab strip's 10px margin, a 16px tab line, and its 9px bottom
inset — so no row below may grow past its figure. */
.header {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
flex: none;
box-sizing: border-box;
min-height: 76px;
@@ -46,6 +48,7 @@
}
.titleRow {
grid-column: 2;
display: flex;
align-items: center;
gap: 0;
@@ -65,6 +68,8 @@
occupant keeps the seat non-empty, so a seat-owned margin would push the
breadcrumbs while nothing shows). */
.headerLeading {
grid-column: 1;
grid-row: 1;
display: flex;
flex: none;
align-items: center;
@@ -166,6 +171,7 @@
/* figma Tab_Group 34:11441: 35px strip, gap 36, pad-left 8, tabs bottom-aligned.
Positioned above the header's own paint, so the active bar covers the rule. */
.tabs {
grid-column: 1 / -1;
position: relative;
z-index: 1;
display: flex;
@@ -68,11 +68,8 @@ export function ConversationSessionHeader({
const conversation = useConversation(s => s)
const hideChrome = session.blank && conversationPhase(session, conversation) === 'blank'
return (
<header className={clsx(css.header, hideChrome && css.headerBlank)}>
<>
<div className={css.titleRow}>
<div className={css.headerLeading} data-conversation-header-leading="">
{renderSlot('conversation.session.header.leading', {})}
</div>
{!hideChrome && (
<>
<div className={css.titleCluster}>
@@ -154,7 +151,7 @@ export function ConversationSessionHeader({
))}
</div>
)}
</header>
</>
)
}
@@ -61,6 +61,8 @@ describe('target-neutral Conversation apply wiring', () => {
.toMatchObject({ views: { scope: 'session' } })
expect(b.runtime.slots.entries('conversation.session')).toHaveLength(1)
expect(b.runtime.slots.entries('conversation.session.header')).toHaveLength(1)
expect(b.runtime.slots.spec('conversation.header')).toEqual({ kind: 'single', scope: 'session-maybe' })
expect(b.runtime.slots.spec('conversation.header.leading')).toEqual({ kind: 'single', scope: 'root' })
expect(b.runtime.slots.entries('conversation.composer.bar')).toHaveLength(1)
await b.runtime.dispose()
})
@@ -21,6 +21,7 @@ import { createConversationStore } from '../src/client/stores.ts'
import { SessionInputShell } from '../src/client/input/facade.ts'
import { en, zh } from '../src/client/locales.ts'
import { ConversationContent } from '../src/client/skeleton/ConversationContent.tsx'
import { ConversationHeader } from '../src/client/skeleton/ConversationHeader.tsx'
import { ConversationMainPanel } from '../src/client/skeleton/ConversationMainPanel.tsx'
import { ConversationSession, ConversationSessionHeader } from '../src/client/skeleton/ConversationSession.tsx'
import { conversationPhase } from '../src/client/contract/snapshot.ts'
@@ -197,6 +198,9 @@ function mount(
lineageOwners.push(owner as ConversationHeaderLineageOwnerProps)
return opts?.fallback ?? null
}
if (key === 'conversation.header') {
return <ConversationHeader {...props} __renders={undefined} renderSlot={renderSlot as never} />
}
if (key === 'conversation.session.header') {
return (
<ConversationSessionHeader
@@ -363,7 +367,7 @@ function mount(
}
const view = render(<ConversationMainPanel {...props} />)
return {
view, store, wiring, sink, retargetWorkspace, session, conversation, slotCalls, lineageOwners, seatOwners, open,
view, props, store, wiring, sink, retargetWorkspace, session, conversation, slotCalls, lineageOwners, seatOwners, open,
pickerOwner: () => pickerOwner,
rerender: () => { view.rerender(<ConversationMainPanel {...props} />) },
}
@@ -388,6 +392,21 @@ describe('Hero chrome', () => {
})
describe('ConversationRoot resident composer', () => {
it('keeps global header navigation without selecting a Session', () => {
const b = mount(sessionSnapshotOf())
b.slotCalls.length = 0
b.props.sessionId = undefined
b.props.useSession = () => undefined
b.props.useConversation = () => undefined
b.view.unmount()
const view = render(<ConversationMainPanel {...b.props} />)
expect(view.container.querySelector('header')).not.toBeNull()
expect(view.getByTestId('view-conversation.header.leading')).toBeTruthy()
expect(b.slotCalls).not.toContain('conversation.session.header')
expect(view.queryByRole('tablist')).toBeNull()
expect(view.queryByTestId('view-conversation.session.header.corner')).toBeNull()
})
it('does not redispatch composer child slots for an unrelated Session publication', () => {
const b = mount(sessionSnapshotOf())
const childKeys = new Set([
@@ -493,7 +512,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.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')
+2 -2
View File
@@ -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: a0b9ffe8ca601fade50ff86fc1921d6b41b84748
README.zh.md: 44b08c8ef76ec266c813c8199db8291f47db3d53
README.md: 513e9d3151aba64b640475c3cd8c2f993df077db
README.zh.md: 69050dc8c530bf4c794d42113f2679fe41891e5f
+1 -1
View File
@@ -45,7 +45,7 @@ On Windows Electron, `html[data-windows-titlebar]` fixes the sidebar toggle in t
### 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).
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 resident header remains mounted without a selected Session. The package registers `HeaderLeadingControls` into the conversation header's root-scoped `conversation.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
+1 -1
View File
@@ -45,7 +45,7 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将
### 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)。
在 `html[data-platform='darwin']`(仅由桌面 preload 设置)下,展开的侧边栏列顶部有一条 52px 的顶部条:避开 hiddenInset 红绿灯、承载收起按钮,并作为窗口拖拽区;收起时整列隐藏而非保留轨道。未选中会话时,常驻头部仍保持挂载。本包向会话头部的根作用域 `conversation.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)。
### 滚动条
@@ -1,4 +1,4 @@
/** macOS-desktop conversation-header controls for the fully hidden sidebar. */
/** macOS-desktop resident-header controls for the fully hidden sidebar. */
import {
IconNewChatOutline16, IconPanelLeftOutline16, isDarwinDesktop, Tooltip,
} from '@deepseek-ai/dsh-client-ui-primitives'
@@ -8,14 +8,14 @@ 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. */
/** Full props of the resident-header leading occupant. */
export type HeaderLeadingControlsProps =
PropsRuntime<'conversation.session.header.leading'>
PropsRuntime<'conversation.header.leading'>
& InjectFace<SidebarRootInjected>
& PropsLocale<'sidebar'>
/**
* Sidebar-open and New Session controls in the conversation header's leading
* Sidebar-open and New Session controls in the resident 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
@@ -89,8 +89,8 @@ export function apply(ctx: ClientContext): void {
// 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',
ctx.slots.inject('conversation.header.leading', () => ctx.slots.register({
name: 'conversation.header.leading',
locale: NS,
inject: injectProps,
}, HeaderLeadingControls))
@@ -42,7 +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' },
'conversation.header.leading': { kind: 'single', scope: 'root' },
} },
SidebarFrame,
)
@@ -72,7 +72,7 @@ describe('ui-sidebar apply', () => {
// 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')
const leading = b.slots.entries('conversation.header.leading')
expect(leading).toHaveLength(1)
expect(leading[0]!.component).toBe(HeaderLeadingControls)
expect(leading[0]!.locale).toBe('sidebar')
@@ -145,7 +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.entries('conversation.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()
@@ -406,7 +406,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:180',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:176',
},
{
key: 'conversation.composer.bar',
@@ -445,7 +445,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:198',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:194',
},
{
key: 'conversation.composer.dock',
@@ -501,7 +501,73 @@ 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:192',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:188',
},
{
key: 'conversation.header',
kind: 'single',
scope: 'session-maybe',
summary: 'Resident navigation container, including when no Session is selected.',
doc: 'Resident navigation container, including when no Session is selected.',
registerOptions: [],
ownerProps: [],
ownerPropsReferences: [],
standardProps: [
'useResource: UseResource',
'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
'usePanelInfo: UsePanelInfo',
'useSessions: UseSessions',
'useSessionStatus: UseSessionStatus',
'useSessionRetainInfo: UseSessionRetainInfo',
'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
'useConversation: MaybeSnapshotSelectorHook<ConversationSnapshot>',
'useInput: MaybeSnapshotSelectorHook<InputState>',
'inputActions: InputActions | undefined',
'useSession: MaybeSnapshotSelectorHook<SessionSnapshot>',
'sessionId: SessionId | undefined',
'useProjection: UseProjection',
],
keyDomain: '',
hookContext: '',
slotInject: '',
declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted',
occupants: [
'client-ui-conversation ConversationHeader',
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.header\', () => ctx.slots.register(\n { name: \'conversation.header\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:135',
},
{
key: 'conversation.header.leading',
kind: 'single',
scope: 'root',
summary: 'Global navigation before the Session title, available without a Session.',
doc: 'Global navigation before the Session title, available without a Session.',
registerOptions: [],
ownerProps: [
'/** The leading seat exposes global navigation independently of a Session. */\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',
'useSessionStatus: UseSessionStatus',
'useSessionRetainInfo: UseSessionRetainInfo',
'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
],
keyDomain: '',
hookContext: '',
slotInject: '',
declaredBy: 'an entry in \'conversation.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.header.leading\', () => ctx.slots.register(\n { name: \'conversation.header.leading\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:157',
},
{
key: 'conversation.hero.agentPreset',
@@ -538,7 +604,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:186',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:182',
},
{
key: 'conversation.hero.brand.mark',
@@ -567,7 +633,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:184',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:180',
},
{
key: 'conversation.hero.workspace',
@@ -600,7 +666,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:182',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:178',
},
{
key: 'conversation.hero.workspace.directoryFlow',
@@ -673,7 +739,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:200',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:196',
},
{
key: 'conversation.input.dock',
@@ -736,7 +802,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:188',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:184',
},
{
key: 'conversation.input.left',
@@ -790,7 +856,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:194',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:190',
},
{
key: 'conversation.input.model',
@@ -829,7 +895,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:210',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:206',
},
{
key: 'conversation.input.overlay',
@@ -887,7 +953,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:190',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:186',
},
{
key: 'conversation.input.permission',
@@ -926,7 +992,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:208',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:204',
},
{
key: 'conversation.input.plan',
@@ -965,7 +1031,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:206',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:202',
},
{
key: 'conversation.input.right',
@@ -1019,7 +1085,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:196',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:192',
},
{
key: 'conversation.message.images',
@@ -1188,13 +1254,13 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
keyDomain: '',
hookContext: '',
slotInject: '',
declaredBy: 'an entry in \'main.conversation\' (client-ui-conversation), so it exists while that entry is mounted',
declaredBy: 'an entry in \'conversation.header\' (client-ui-conversation), so it exists while that entry is mounted',
occupants: [
'client-ui-conversation ConversationSessionHeader',
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header\', () => ctx.slots.register(\n { name: \'conversation.session.header\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:135',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:137',
},
{
key: 'conversation.session.header.actions',
@@ -1256,7 +1322,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.actions\', () => ctx.slots.register(\n { name: \'conversation.session.header.actions\', 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:143',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:145',
},
{
key: 'conversation.session.header.corner',
@@ -1295,46 +1361,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.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:172',
},
{
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',
'useSessionStatus: UseSessionStatus',
'useSessionRetainInfo: UseSessionRetainInfo',
'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:161',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:168',
},
{
key: 'conversation.session.header.lineage',
@@ -1375,7 +1402,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.session.header.lineage\', () => ctx.slots.register(\n { name: \'conversation.session.header.lineage\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:137',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:139',
},
{
key: 'conversation.session.header.utilities',
@@ -1434,7 +1461,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.session.header.utilities\', () => ctx.slots.register(\n { name: \'conversation.session.header.utilities\', 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:149',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:151',
},
{
key: 'conversation.trajectory.images',
@@ -1538,7 +1565,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:178',
source: 'packages/client/ui-conversation/src/client/contract/slots.ts:174',
},
{
key: 'main',