fix(client): keep sidebar navigation available without a session
This commit is contained in:
1 parent
9e3a30f85a
commit
1628e911c8
25 files changed
+230
-121
No files matched your search
+2
-2
@@ -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,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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user