From 12e47f59746e5195bb7503828bcee96e17f7b174 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:13:03 +0800 Subject: [PATCH] docs(ui-conversation): state the composer fixed-overlay contract Composer-hosted slots now document that fixed overlays must portal to the document body, because the docked composer is their containing block; the client slot catalog is regenerated from that JSDoc. The README also records that gutter tables cover a captured width-handle drag and that the composer's backdrop blur is verified only in Chromium. Local comments note the grayscale text antialiasing of the permanent composer layer, why the statistics row uses a minimum height, why the sessionless header keeps an empty title row, and that the Hero offset test assumes the default plugins. Refs #7590 --- apps/web/tests/startup-auto-selection.e2e.ts | 2 + .../client/ui-conversation/README.i18n.yaml | 8 +- packages/client/ui-conversation/README.md | 3 +- packages/client/ui-conversation/README.zh.md | 3 +- .../src/client/contract/slots.ts | 57 +++++++++++--- .../client/skeleton/ConversationHeader.tsx | 2 + .../skeleton/ConversationRoot.module.css | 4 +- .../src/client/skeleton/InputBar.module.css | 3 +- .../src/client/slot-catalog.ts | 78 +++++++++---------- 9 files changed, 102 insertions(+), 58 deletions(-) diff --git a/apps/web/tests/startup-auto-selection.e2e.ts b/apps/web/tests/startup-auto-selection.e2e.ts index 461d5902c1..eb24b80a57 100644 --- a/apps/web/tests/startup-auto-selection.e2e.ts +++ b/apps/web/tests/startup-auto-selection.e2e.ts @@ -88,6 +88,8 @@ describe('web e2e: startup auto-selection', () => { await connectFreshWorkspace(page, scaffold.workspaceCwd, 'startup-auto-selection') await page.locator('[data-slot="conversation.session.header"]').waitFor({ timeout: 15_000 }) // Binding the Workspace keeps the input card at the same height in the Hero. + // This holds for the default plugin set: `conversation.hero.options` entries + // render only with a Session and can change the Hero's height. expect(Math.abs(await heroCardOffset() - sessionlessOffset)).toBeLessThanOrEqual(0.5) expect(await page.evaluate(() => { diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index d0e1f1abfa..f17358e4b5 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -21,8 +21,8 @@ en: cc4dafd1aae02468 zh: be6b67724b7ac4a3 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: b9dd4406a104738c - zh: 4ea6eb46616901ec + en: 99ec447f0a6bda48 + zh: 922cf64666676530 /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: 59cd63057dfb6eeb zh: b2e2c86e810b7bb8 @@ -33,8 +33,8 @@ en: 9f4e0da7c8addf88 zh: 1a33d9981af75643 /deepseek-ai-dsh-client-ui-conversation/known-limitations-and-deferred-work: - en: 38d243630d0e4c38 - zh: a58cdc35b664020b + en: 1413851e9d622bd3 + zh: 84502501f8ce599b /deepseek-ai-dsh-client-ui-conversation/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 67e22c984b..14f54dc287 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -106,7 +106,7 @@ View selection is deterministic: a registered persisted selection wins, otherwis The shell reads the persisted View preference before rendering when a Session first binds or a cached Session becomes current, activates the registered preferred View or Chat fallback, and activates later tab or focus selections before committing them to the store. A blank Session still omits the `conversation.view` slot; no unselected target is activated. -Active transcripts in the main occurrence expose content-width drag handles in their uncovered side gutters; embedded occurrences omit them. A View that paints into a gutter raises only its concrete painted element above the handle; transparent full-width wrappers stay below so they do not claim empty gutter. This requires the path between that element and the Conversation body to remain outside an intermediate stacking context; the shipped Chromium behavior is pinned by the browser scenario. Chat applies the rule to table elements, while its column-bounded tool cards need no raise. Wheel motion over a handle still scrolls the transcript, while Ctrl+wheel remains a browser zoom gesture. The sticky composer owns its full footer band, which is not a resize target; a captured drag continues under the band, where the composer covers its indicator. In an active transcript the composer rests at the scrollport floor even when no View fills the column, keeps one stacking rank in every state, above transcript sticky headers and the back-to-bottom control, so menus it opens upward cover that control, and never serves as a scroll-anchoring target. It keeps its own compositor layer whatever lies beneath it, which makes it the containing block for fixed-position descendants, so fixed overlays inside the composer, such as control tooltips, render under the document body ([handle and composer styles](src/client/skeleton/ConversationRoot.module.css)). +Active transcripts in the main occurrence expose content-width drag handles in their uncovered side gutters; embedded occurrences omit them. A View that paints into a gutter raises only its concrete painted element above the handle; transparent full-width wrappers stay below so they do not claim empty gutter. This requires the path between that element and the Conversation body to remain outside an intermediate stacking context; the shipped Chromium behavior is pinned by the browser scenario. Chat applies the rule to table elements, while its column-bounded tool cards need no raise. Wheel motion over a handle still scrolls the transcript, while Ctrl+wheel remains a browser zoom gesture. The sticky composer owns its full footer band, which is not a resize target; a captured drag continues under the band and under gutter tables, which cover its indicator. In an active transcript the composer rests at the scrollport floor even when no View fills the column, keeps one stacking rank in every state, above transcript sticky headers and the back-to-bottom control, so menus it opens upward cover that control, and never serves as a scroll-anchoring target. It keeps its own compositor layer whatever lies beneath it, which makes it the containing block for fixed-position descendants: contributions to `conversation.composer`, `conversation.composer.bar`, `conversation.composer.dock`, `conversation.input.dock`, and the other `conversation.input.*` slots must portal fixed overlays to the document body, for example with `Tooltip` or `Menu` `portal` or `createPortal` ([handle and composer styles](src/client/skeleton/ConversationRoot.module.css)). The width-handle indicator follows the pointer only during a captured drag; ordinary hover leaves its position unchanged. @@ -201,6 +201,7 @@ None; Conversation assembly and browser input state do not alter provider-side p - **Only registered targets can render** — the shell deliberately has no implicit fallback target beyond the registered `chat` preference. - **Factory occurrences inherit their render-position Session** — `conversation.content` does not accept an independently addressed Session; that requires a separate Session-provider capability. +- **Composer backdrop blur is verified only in Chromium** — with `will-change: transform` on the docked composer, descendant `backdrop-filter` still samples the transcript in Chromium; WebKit behavior has not been verified. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index d3d2246380..473e15825e 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -106,7 +106,7 @@ View 选择规则固定:有效且已注册的持久化选择优先,其次是 Session 首次绑定或缓存的 Session 成为 current 时,shell 会在渲染前读取持久化 View 偏好,激活已注册的偏好 View 或 Chat fallback,并在后续 tab 或 focus 选择写入 store 前先激活对应 target。blank Session 仍不渲染 `conversation.view` slot;未选中的 target 不会激活。 -主 occurrence 的活跃 transcript 只在未被内容覆盖的两侧沟槽中提供正文宽度拖拽条;嵌入式 occurrence 省略这些拖拽条。View 如果绘制进沟槽,只将具体的可见元素提到拖拽条上方;透明的全宽包装层保持在下方,不会占用空白沟槽。该规则要求此元素与 Conversation body 之间不能引入中间堆叠上下文;浏览器场景固定了交付 Chromium 的行为。Chat 将该规则用于表格元素,其限定在阅读列内的工具卡片无需提高层级。指针位于拖拽条上时,滚轮仍会滚动 transcript,Ctrl+滚轮则保留为浏览器缩放手势。粘滞 composer 拥有完整的底部区带,该区域不是宽度调整目标;已捕获的拖拽会在该区带下方继续,此时指示线被 composer 遮住。在活跃 transcript 中,即使没有 View 撑满整列,composer 也停在滚动区底部;它在所有状态下保持同一层级,高于 transcript 的吸顶标题与“回到底部”按钮,因此它向上打开的菜单会盖住该按钮;它也从不作为滚动锚定目标。无论下方是什么内容,它都保持自己的合成层,因此成为 fixed 定位后代的包含块;composer 内的 fixed 浮层(例如控件提示)渲染到 document body 下([拖动手柄与 composer 样式](src/client/skeleton/ConversationRoot.module.css))。 +主 occurrence 的活跃 transcript 只在未被内容覆盖的两侧沟槽中提供正文宽度拖拽条;嵌入式 occurrence 省略这些拖拽条。View 如果绘制进沟槽,只将具体的可见元素提到拖拽条上方;透明的全宽包装层保持在下方,不会占用空白沟槽。该规则要求此元素与 Conversation body 之间不能引入中间堆叠上下文;浏览器场景固定了交付 Chromium 的行为。Chat 将该规则用于表格元素,其限定在阅读列内的工具卡片无需提高层级。指针位于拖拽条上时,滚轮仍会滚动 transcript,Ctrl+滚轮则保留为浏览器缩放手势。粘滞 composer 拥有完整的底部区带,该区域不是宽度调整目标;已捕获的拖拽会在该区带和沟槽中的表格下方继续,此时指示线被它们遮住。在活跃 transcript 中,即使没有 View 撑满整列,composer 也停在滚动区底部;它在所有状态下保持同一层级,高于 transcript 的吸顶标题与“回到底部”按钮,因此它向上打开的菜单会盖住该按钮;它也从不作为滚动锚定目标。无论下方是什么内容,它都保持自己的合成层,因此成为 fixed 定位后代的包含块:向 `conversation.composer`、`conversation.composer.bar`、`conversation.composer.dock`、`conversation.input.dock` 以及其他 `conversation.input.*` slot 贡献的内容,必须把 fixed 浮层 portal 到 document body,例如使用 `Tooltip` 或 `Menu` 的 `portal`,或 `createPortal`([拖动手柄与 composer 样式](src/client/skeleton/ConversationRoot.module.css))。 宽度拖拽条的指示线只在已捕获指针的拖拽期间跟随指针,普通悬停不改变其位置。 @@ -201,6 +201,7 @@ selector 必须是 owner currency 的纯函数。非 null 返回值作为 `match - **只有已注册 target 可以渲染**——除已注册的 `chat` 偏好外,shell 刻意不提供隐式 fallback target。 - **Factory occurrence 继承渲染位置的 Session**——`conversation.content` 不接受独立寻址的 Session;该能力需要单独的 Session provider。 +- **composer 内的毛玻璃只在 Chromium 中验证过**——docked composer 设置 `will-change: transform` 后,Chromium 中后代的 `backdrop-filter` 仍会采样背后的 transcript;WebKit 中的表现尚未验证。 diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index 369f562a7b..0c438e0f4c 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -276,7 +276,10 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { 'conversation.session.header.views.actions': { kind: 'list'; scope: 'root'; owner: ConversationViewActionsOwnerProps } /** Registered Conversation target Views, rendered one at a time. */ 'conversation.view': { kind: 'list'; scope: 'session'; owner: ConvViewOwnerProps } - /** Selector-routed replacements for the current Session's resident composer. */ + /** + * Selector-routed replacements for the current Session's resident composer; fixed overlays must + * portal to the document body because the docked composer contains them. + */ 'conversation.composer': { kind: 'chain'; scope: 'session'; owner: ComposerChainProps } /** Workspace picker shown by the blank-session Hero. */ 'conversation.hero.workspace': { kind: 'single'; scope: 'root'; owner: EmptyWorkspaceOwnerProps } @@ -286,34 +289,66 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { 'conversation.hero.agentPreset': { kind: 'single'; scope: 'session-maybe'; owner: HeroAgentPresetOwnerProps } /** Compact options right-aligned in the New Session row after the workspace and preset controls; rendered only with a Session. */ 'conversation.hero.options': { kind: 'list'; scope: 'session' } - /** Full-width entries above the composer card. */ + /** + * Full-width entries above the composer card; fixed overlays must portal to the document body + * because the docked composer contains them. + */ 'conversation.input.dock': { kind: 'list'; scope: 'session'; owner: InputZone } - /** Floating entries rendered inside the resident composer card. */ + /** + * Floating entries rendered inside the resident composer card; fixed overlays must portal to + * the document body because the docked composer contains them. + */ 'conversation.input.overlay': { kind: 'list'; scope: 'session' } - /** Ambient entries below the composer card. */ + /** + * Ambient entries below the composer card; fixed overlays must portal to the document body + * because the docked composer contains them. + */ 'conversation.composer.dock': { kind: 'list'; scope: 'session' } - /** Compact controls at the left of the composer tool row. */ + /** + * Compact controls at the left of the composer tool row; fixed overlays must portal to the + * document body because the docked composer contains them. + */ 'conversation.input.left': { kind: 'list'; scope: 'session' } - /** Compact controls before the composer submit action. */ + /** + * Compact controls before the composer submit action; fixed overlays must portal to the + * document body because the docked composer contains them. + */ 'conversation.input.right': { kind: 'list'; scope: 'session' } - /** Compact action after the model selector; it can expand across the toolbar while retaining the editor and submit action. */ + /** + * Compact action after the model selector; it can expand across the toolbar while retaining the + * editor and submit action, and fixed overlays must portal to the document body because the + * docked composer contains them. + */ 'conversation.input.activity': { kind: 'single'; scope: 'session'; owner: InputActivityOwnerProps } - /** Resident composer body, including the no-Session inert state. */ + /** + * Resident composer body, including the no-Session inert state; fixed overlays must portal to + * the document body because the docked composer contains them. + */ 'conversation.composer.bar': { kind: 'single'; scope: 'session-maybe'; owner: ComposerBarOwnerProps } - /** Optional draft-attachment rail and drop target. */ + /** + * Optional draft-attachment rail and drop target; fixed overlays must portal to the document + * body because the docked composer contains them. + */ 'conversation.input.attachments': { kind: 'single' scope: 'session-maybe' owner: ComposerAttachmentsOwnerProps } - /** Plan control inside the composer tool row. */ + /** + * Plan control inside the composer tool row; fixed overlays must portal to the document body + * because the docked composer contains them. + */ 'conversation.input.plan': { kind: 'single'; scope: 'session'; owner: InputControlOwnerProps } - /** Current-session permission control inside the composer tool row. */ + /** + * Current-session permission control inside the composer tool row; fixed overlays must portal + * to the document body because the docked composer contains them. + */ 'conversation.input.permission': { kind: 'single'; scope: 'session'; owner: InputControlOwnerProps } /** * Model selector inside the composer tool row. When expanded controls cannot * share a line, the row sets --dsh-composer-model-text-display to none and * --dsh-composer-model-icon-display to block for an occupant's compact display. + * Fixed overlays must portal to the document body because the docked composer contains them. */ 'conversation.input.model': { kind: 'single'; scope: 'session'; owner: InputControlOwnerProps } } diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationHeader.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationHeader.tsx index a1a46a98da..c1e4862f2f 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationHeader.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationHeader.tsx @@ -20,6 +20,8 @@ export function ConversationHeader({ sessionId, useSession, useConversation, ren {renderSlot('conversation.header.leading', {})} {sessionId === undefined + // The empty title row keeps the blank-Session header height, so the + // Hero centers its input card at the same offset before a Session exists. ?
: renderSlot('conversation.session.header', { hideChrome: blank })} diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index 4200c733e2..d69c5bbaf0 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -510,7 +510,9 @@ the transcript beneath it. A transform hint keeps descendant backdrop blur sampling the transcript, but makes the seat the containing block for fixed-position descendants, so every fixed overlay inside the composer - (tooltips, menus, panels) portals to the document body. */ + (tooltips, menus, panels) must portal to the document body. The permanent + layer has a translucent top, so Chromium renders the seat's text with + grayscale antialiasing where it would otherwise use LCD text. */ will-change: transform; /* Scroll anchoring never selects composer content as its anchor. */ overflow-anchor: none; diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index 48d3d3b52c..b3cd1dd271 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -22,7 +22,8 @@ /* The docked composer reserves one statistics row before its readings arrive, so the projections landing never change the composer height. The row height matches the readings: a 20px secondary line plus 1px block - padding (ui-chat StatsPills `.pill`, ContextMeter `.trigger`). */ + padding (ui-chat StatsPills `.pill`, ContextMeter `.trigger`). A minimum, + not a fixed height, so taller readings still size the row. */ .root:not(.hero) > .dock { min-height: calc(22px + var(--dsh-content-font-delta-secondary, 0px)); } diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 8816ef5067..58a7e83eb2 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -757,8 +757,8 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ key: 'conversation.composer', kind: 'chain', scope: 'session', - summary: 'Selector-routed replacements for the current Session\'s resident composer.', - doc: 'Selector-routed replacements for the current Session\'s resident composer.', + summary: 'Selector-routed replacements for the current Session\'s resident composer; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Selector-routed replacements for the current Session\'s resident composer; fixed overlays must\nportal to the document body because the docked composer contains them.', registerOptions: [ { name: 'select', @@ -808,14 +808,14 @@ 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:280', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:283', }, { key: 'conversation.composer.bar', kind: 'single', scope: 'session-maybe', - summary: 'Resident composer body, including the no-Session inert state.', - doc: 'Resident composer body, including the no-Session inert state.', + summary: 'Resident composer body, including the no-Session inert state; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Resident composer body, including the no-Session inert state; fixed overlays must portal to\nthe document body because the docked composer contains them.', registerOptions: [], ownerProps: [ '/** Owner share of the resident composer bar. */\nexport interface ComposerBarOwnerProps {\n /** Hero uses centered placement; composer uses the active bottom placement. */\n variant: \'hero\' | \'composer\'\n /** A feature-owned reason that makes message input inert while leaving model selection live. */\n blocked?: { readonly reason: string }\n /** Lock all message actions while preserving the resident composer surface. */\n disabled?: boolean\n /** Whether the shared Workspace picker is expanded. */\n workspacePickerOpen?: boolean\n /** Open the Workspace picker from the inert composer surface. */\n onRequestWorkspace?: () => void\n placeholder?: string\n /** Optional content rendered above the composer surface. */\n accessory?: ReactNode\n}', @@ -851,14 +851,14 @@ 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:302', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:327', }, { key: 'conversation.composer.dock', kind: 'list', scope: 'session', - summary: 'Ambient entries below the composer card.', - doc: 'Ambient entries below the composer card.', + summary: 'Ambient entries below the composer card; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Ambient entries below the composer card; fixed overlays must portal to the document body\nbecause the docked composer contains them.', registerOptions: [ { name: 'id', @@ -912,7 +912,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.composer.dock\', () => ctx.slots.register(\n { name: \'conversation.composer.dock\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:294', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:306', }, { key: 'conversation.header', @@ -1024,7 +1024,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:286', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:289', }, { key: 'conversation.hero.brand.mark', @@ -1056,7 +1056,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:284', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:287', }, { key: 'conversation.hero.options', @@ -1116,7 +1116,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.options\', () => ctx.slots.register(\n { name: \'conversation.hero.options\', 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:288', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:291', }, { key: 'conversation.hero.workspace', @@ -1152,7 +1152,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:282', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:285', }, { key: 'conversation.hero.workspace.directoryFlow', @@ -1193,8 +1193,8 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ key: 'conversation.input.activity', kind: 'single', scope: 'session', - summary: 'Compact action after the model selector; it can expand across the toolbar while retaining the editor and submit action.', - doc: 'Compact action after the model selector; it can expand across the toolbar while retaining the editor and submit action.', + summary: 'Compact action after the model selector; it can expand across the toolbar while retaining the editor and submit action, and fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Compact action after the model selector; it can expand across the toolbar while retaining the\neditor and submit action, and fixed overlays must portal to the document body because the\ndocked composer contains them.', registerOptions: [], ownerProps: [ '/** A toolbar activity hides ordinary accessory controls while expanded; its occupant must release expansion on unmount. */\nexport interface InputActivityOwnerProps extends InputControlOwnerProps {\n /** @param active - whether the occupant needs the toolbar width before the submit action. */\n onActiveChange: (active: boolean) => void\n}', @@ -1232,14 +1232,14 @@ 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.activity\', () => ctx.slots.register(\n { name: \'conversation.input.activity\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-conversation/src/client/contract/slots.ts:300', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:322', }, { key: 'conversation.input.attachments', kind: 'single', scope: 'session-maybe', - summary: 'Optional draft-attachment rail and drop target.', - doc: 'Optional draft-attachment rail and drop target.', + summary: 'Optional draft-attachment rail and drop target; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Optional draft-attachment rail and drop target; fixed overlays must portal to the document\nbody because the docked composer contains them.', registerOptions: [], ownerProps: [ '/** Input state handed to the optional attachment presentation plugin. */\nexport interface ComposerAttachmentsOwnerProps {\n /** Browser-owned draft attachments in input order. */\n attachments: readonly ComposerAttachment[]\n /** Whether a document-level file drop may add attachments now. */\n canAcceptDrop: boolean\n /**\n * Add one dropped batch through the composer\'s validation path.\n * @param files - dropped, pasted, or picked browser files in source order.\n * @param directories - members of `files` the drop source identified as directories.\n */\n onAddFiles: (files: readonly File[], directories?: ReadonlySet) => void\n /** Remove one draft attachment through the Conversation service. */\n onRemoveAttachment: (id: DraftAttachmentId) => void\n /** Current per-draft upload states for file-kind attachments. */\n uploads: DraftFileUploads\n /** Restart one failed file upload. */\n onRetryFile: (id: DraftAttachmentId) => void\n /**\n * Explain one upload the Host refused; such a draft cannot be retried and\n * stays until it is removed.\n * @param reason - the refusal `reason` the failed upload carries.\n * @returns localized text naming the limit or the way out.\n /* …truncated — full shape in source */', @@ -1277,14 +1277,14 @@ 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:304', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:332', }, { key: 'conversation.input.dock', kind: 'list', scope: 'session', - summary: 'Full-width entries above the composer card.', - doc: 'Full-width entries above the composer card.', + summary: 'Full-width entries above the composer card; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Full-width entries above the composer card; fixed overlays must portal to the document body\nbecause the docked composer contains them.', registerOptions: [ { name: 'id', @@ -1346,14 +1346,14 @@ 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:290', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:296', }, { key: 'conversation.input.left', kind: 'list', scope: 'session', - summary: 'Compact controls at the left of the composer tool row.', - doc: 'Compact controls at the left of the composer tool row.', + summary: 'Compact controls at the left of the composer tool row; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Compact controls at the left of the composer tool row; fixed overlays must portal to the\ndocument body because the docked composer contains them.', registerOptions: [ { name: 'id', @@ -1404,14 +1404,14 @@ 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:296', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:311', }, { key: 'conversation.input.model', kind: 'single', scope: 'session', summary: 'Model selector inside the composer tool row.', - doc: 'Model selector inside the composer tool row. When expanded controls cannot\nshare a line, the row sets --dsh-composer-model-text-display to none and\n--dsh-composer-model-icon-display to block for an occupant\'s compact display.', + doc: 'Model selector inside the composer tool row. When expanded controls cannot\nshare a line, the row sets --dsh-composer-model-text-display to none and\n--dsh-composer-model-icon-display to block for an occupant\'s compact display.\nFixed overlays must portal to the document body because the docked composer contains them.', registerOptions: [], ownerProps: [ '/** Owner share of the named plan, permission, and model controls. */\nexport interface InputControlOwnerProps {\n /** Whether the composer currently refuses interaction. */\n locked: boolean\n}', @@ -1448,7 +1448,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:318', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:353', }, { key: 'conversation.input.model.label', @@ -1498,8 +1498,8 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ key: 'conversation.input.overlay', kind: 'list', scope: 'session', - summary: 'Floating entries rendered inside the resident composer card.', - doc: 'Floating entries rendered inside the resident composer card.', + summary: 'Floating entries rendered inside the resident composer card; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Floating entries rendered inside the resident composer card; fixed overlays must portal to\nthe document body because the docked composer contains them.', registerOptions: [ { name: 'id', @@ -1554,14 +1554,14 @@ 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:292', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:301', }, { key: 'conversation.input.permission', kind: 'single', scope: 'session', - summary: 'Current-session permission control inside the composer tool row.', - doc: 'Current-session permission control inside the composer tool row.', + summary: 'Current-session permission control inside the composer tool row; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Current-session permission control inside the composer tool row; fixed overlays must portal\nto the document body because the docked composer contains them.', registerOptions: [], ownerProps: [ '/** Owner share of the named plan, permission, and model controls. */\nexport interface InputControlOwnerProps {\n /** Whether the composer currently refuses interaction. */\n locked: boolean\n}', @@ -1598,14 +1598,14 @@ 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:312', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:346', }, { key: 'conversation.input.plan', kind: 'single', scope: 'session', - summary: 'Plan control inside the composer tool row.', - doc: 'Plan control inside the composer tool row.', + summary: 'Plan control inside the composer tool row; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Plan control inside the composer tool row; fixed overlays must portal to the document body\nbecause the docked composer contains them.', registerOptions: [], ownerProps: [ '/** Owner share of the named plan, permission, and model controls. */\nexport interface InputControlOwnerProps {\n /** Whether the composer currently refuses interaction. */\n locked: boolean\n}', @@ -1641,14 +1641,14 @@ 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:310', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:341', }, { key: 'conversation.input.right', kind: 'list', scope: 'session', - summary: 'Compact controls before the composer submit action.', - doc: 'Compact controls before the composer submit action.', + summary: 'Compact controls before the composer submit action; fixed overlays must portal to the document body because the docked composer contains them.', + doc: 'Compact controls before the composer submit action; fixed overlays must portal to the\ndocument body because the docked composer contains them.', registerOptions: [ { name: 'id', @@ -1699,7 +1699,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:298', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:316', }, { key: 'conversation.plan-review.actions',