From d70967e36b7e4b5abe154016549909c039b42024 Mon Sep 17 00:00:00 2001 From: Ziya Date: Tue, 3 Nov 2026 20:07:26 -0800 Subject: [PATCH] Merge pull request #6586 from deepseek-harness/experiment/home-code-ripple feat(web): play a pixel and code whale on blank home clicks --- ...2-slot-type-chain-implementation.i18n.yaml | 8 +- ...26-07-22-slot-type-chain-implementation.md | 3 +- ...07-22-slot-type-chain-implementation.zh.md | 3 +- .../2026-11-03-hoc-slot-kind.i18n.yaml | 25 ++ .../architecture/2026-11-03-hoc-slot-kind.md | 72 ++++ .../2026-11-03-hoc-slot-kind.zh.md | 72 ++++ apps/web/tests/home-whale.e2e.ts | 201 +++++++++++ apps/web/tsconfig.json | 1 + docs/subsystems/slots.i18n.yaml | 15 +- docs/subsystems/slots.md | 18 +- docs/subsystems/slots.zh.md | 18 +- packages/bundle/web-app/cordis.patch.yml | 2 +- .../client/ui-brand-official/README.i18n.yaml | 35 +- packages/client/ui-brand-official/README.md | 23 +- .../client/ui-brand-official/README.zh.md | 23 +- .../client/ui-brand-official/package.json | 13 +- .../src/client/assets/breach-atlas.webp | Bin 0 -> 189208 bytes .../client/home-whale/PixelWhale.module.css | 20 ++ .../src/client/home-whale/PixelWhale.tsx | 53 +++ .../src/client/home-whale/artwork.ts | 30 ++ .../src/client/home-whale/code-fill.ts | 89 +++++ .../src/client/home-whale/renderer.ts | 225 ++++++++++++ .../ui-brand-official/src/client/index.ts | 12 +- .../ui-brand-official/src/css-modules.d.ts | 4 + .../tests/browser-plugin.client.spec.tsx | 29 +- .../tests/home-whale.client.spec.tsx | 320 ++++++++++++++++++ .../tests/pixel-whale.client.spec.tsx | 100 ++++++ .../client/ui-brand-official/tsconfig.json | 3 + .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 4 +- packages/client/ui-conversation/README.zh.md | 4 +- .../ui-conversation/src/client/apply.ts | 1 + .../src/client/contract/slots.ts | 11 +- .../ui-conversation/src/client/index.ts | 2 +- .../client/skeleton/ConversationContent.tsx | 13 +- .../skeleton/ConversationRoot.module.css | 7 + .../skeleton/ConversationWidthControls.tsx | 4 +- .../tests/skeleton.client.spec.tsx | 38 ++- .../ui-renderer/src/client/scoped-slots.tsx | 270 ++++++++++++--- .../tests/hoc-slots.client.spec.tsx | 205 +++++++++++ packages/client/ui-slots/README.i18n.yaml | 4 +- packages/client/ui-slots/README.md | 2 +- packages/client/ui-slots/README.zh.md | 2 +- packages/client/ui-slots/src/index.ts | 93 +++-- .../client/ui-slots/tests/core.client.spec.ts | 17 +- .../ui-slots/tests/type-chain.client.spec.tsx | 19 ++ .../src/client/api-catalog.ts | 8 +- .../src/client/slot-catalog.ts | 82 ++++- .../references/packages.md | 2 +- pnpm-lock.yaml | 6 + scripts/gen-client-catalog.spec.ts | 6 +- scripts/gen-client-catalog.ts | 26 +- scripts/slot-walk.ts | 2 +- tsconfig.host.json | 1 + 54 files changed, 2050 insertions(+), 200 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.md create mode 100644 .agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.zh.md create mode 100644 apps/web/tests/home-whale.e2e.ts create mode 100644 packages/client/ui-brand-official/src/client/assets/breach-atlas.webp create mode 100644 packages/client/ui-brand-official/src/client/home-whale/PixelWhale.module.css create mode 100644 packages/client/ui-brand-official/src/client/home-whale/PixelWhale.tsx create mode 100644 packages/client/ui-brand-official/src/client/home-whale/artwork.ts create mode 100644 packages/client/ui-brand-official/src/client/home-whale/code-fill.ts create mode 100644 packages/client/ui-brand-official/src/client/home-whale/renderer.ts create mode 100644 packages/client/ui-brand-official/src/css-modules.d.ts create mode 100644 packages/client/ui-brand-official/tests/home-whale.client.spec.tsx create mode 100644 packages/client/ui-brand-official/tests/pixel-whale.client.spec.tsx create mode 100644 packages/client/ui-renderer/tests/hoc-slots.client.spec.tsx diff --git a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml index 2ce9443416..4d52ae67d8 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml @@ -18,11 +18,11 @@ en: ce28268021a38b89 zh: 7802540e4ef04752 /agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/component-props-four-shares-each-from-its-own-source-of-truth: - en: f3fca569fa0c5168 - zh: 647a82662f6a887d + en: c1892cb7309c0824 + zh: 21983594d0c7f24f /agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/the-chain-kind-entries-self-nominate-first-match-renders: - en: 56278be7f3688805 - zh: 16602ffbf4baef85 + en: dad15e2e1309c9a4 + zh: ee2d13b27c92a73b /agent-note-the-slot-system-standard-single-register-four-props-shares-and-the-framework-store-seat/decision/the-store-seat-framework-engine-registrant-schema: en: 2645acc9d29c20ec zh: a8a2fd293ef5da04 diff --git a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md index b08c0f4be8..2a2cee6b84 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md +++ b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md @@ -50,12 +50,13 @@ A contributor whose activation order is independent from the declaring entry use | child render | `PropsRenderSlots` | register's `children` keys | `renderSlot(key, owner)`, key statically narrowed to S; chain keys add `renderSlotChain` | | store | `PropsStore` | store factory return type | `useStore` selector hook + `actions.*` (draft-param stripped) | | business | `I` | inject return type | plain data + callbacks; a reserved `hooks` compartment of bare observables arrives bound as `use` selector hooks (`InjectFace`) | +| kind-specific | `MatchedShare` / `HocShare` | SlotMap entry's kind | chain `matched`; hoc `children`, the next inner layer or the owner content | `sessionId` is framework-supplied wherever `scope: 'session'` is declared — owner params do not carry it. The register call site is the double-lock choke point: a component whose renderSlot keys exceed the `children` declaration, or that misses a declared face, or whose store/inject shapes drift, is a compile error on that line. Delegation is ordinary props passing (hand the `renderSlot` function down, optionally behind a narrower signature) — there is no whitelist face object and no minting API. ### The chain kind: entries self-nominate, first match renders -The fourth `SlotKind`, `'chain'`, inverts routing authority relative to `keyed`: a keyed dispatch site picks its occupant by `entryKey`, while a chain entry nominates itself — the owner dispatches one common currency of owner props and never learns who takes over, so a new takeover package registers with zero owner edits. A chain registration carries a `select` pure selector (`ChainSelect`: `(owner) => matched | null`) and an optional `priority` (ascending; ties keep registration = assembly order — the deployment-controllable inject topology — under the same stable sort as list `order`); registering without `select` is one of the loud-at-load cases above. At render, the outlet runs the selectors in chain order: the first non-null return elects its entry and the returned value joins the component's props as `matched` (the component never re-derives its own match), `null` passes the turn to the next entry, and all-null renders the owner's fallback body (`ChainRenderOpts`). +A fifth kind, `'hoc'`, nests entries around owner-supplied `children` and adds a kind-specific `children` prop the same way `chain` adds `matched`; its decision lives in the [hoc slot kind note](2026-11-03-hoc-slot-kind.md). The fourth `SlotKind`, `'chain'`, inverts routing authority relative to `keyed`: a keyed dispatch site picks its occupant by `entryKey`, while a chain entry nominates itself — the owner dispatches one common currency of owner props and never learns who takes over, so a new takeover package registers with zero owner edits. A chain registration carries a `select` pure selector (`ChainSelect`: `(owner) => matched | null`) and an optional `priority` (ascending; ties keep registration = assembly order — the deployment-controllable inject topology — under the same stable sort as list `order`); registering without `select` is one of the loud-at-load cases above. At render, the outlet runs the selectors in chain order: the first non-null return elects its entry and the returned value joins the component's props as `matched` (the component never re-derives its own match), `null` passes the turn to the next entry, and all-null renders the owner's fallback body (`ChainRenderOpts`). The decline decision lives in `select`, never in a mounted component probing its own props: a component that mounts only to render null still runs its hooks and effects for nothing, and the resulting mount/unmount churn breaks memoization and React key semantics, whereas a selector is a pure function — unit-testable, zero mount side effects — the same discipline as "presentation methods are pure functions of `args`". Purity is the selector's contract: it reads no external mutable state and produces no side effects, so the routing decision is entirely a function of the owner props and safe to run on every dispatch. Selectors route; they never mint — per-dispatch object construction would churn identity every render, so wrapping a matched value in a richer face happens inside the elected component (`useMemo` keyed on `matched`). diff --git a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md index d36cce53b7..a144d51683 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md @@ -50,12 +50,13 @@ ctx.slots.register({ | 子 slot 渲染 | `PropsRenderSlots` | register 的 `children` 键集 | `renderSlot(key, owner)`,键参静态收窄到 S;chain 键另有 `renderSlotChain` | | store | `PropsStore` | store 工厂的返回类型 | `useStore` selector 钩子 + `actions.*`(剥去 draft 形参) | | 业务 | `I` | inject 的返回类型 | 普通数据+回调;保留的 `hooks` 区域内,裸 observable 经绑定后以 `use` 选择器钩子的形式到达(`InjectFace`) | +| 按 kind | `MatchedShare` / `HocShare` | SlotMap entry 的 kind | chain 的 `matched`;hoc 的 `children`,即内一层或宿主内容 | 凡声明 `scope: 'session'` 之处,`sessionId` 一律由框架供给——owner 传参不携带它。register 调用点是双重类型约束的收口:组件的 renderSlot 键集超出 `children` 声明、漏接某个已声明的面、store/inject 形状漂移,任何一条都在那一行上报编译错误。转授就是普通的 props 传递(把 `renderSlot` 函数递下去,可按需包一层更窄的签名)——不存在白名单面对象,也不存在铸面 API。 ### chain kind:entry 自荐,首个匹配项负责渲染 -第四种 `SlotKind`——`'chain'`——把路由权相对 `keyed` 反转:keyed 的分派现场以 `entryKey` 点选占用 slot 的 entry,chain 则由 entry 自荐——owner 只分派一套格式统一的 owner props,永远不知道谁来接管,新的接管包注册进来 owner 零改动。chain 注册携带一个 `select` 纯选择器(`ChainSelect`:`(owner) => matched | null`)与可选的 `priority`(升序;同值保持注册序 = 装配序——部署可控的 inject 拓扑——复用 list `order` 的同一稳定排序);注册缺 `select` 即上文装载即炸情形之一。渲染时 outlet 按链序依次执行各 select:首个非 null 返回值当选,该值以 `matched` 并入组件 props(组件绝不自行重新推导匹配);返回 `null` 则轮到下一个 entry;全 null 则渲染 owner 的 fallback 体(`ChainRenderOpts`)。 +第五种 kind `'hoc'` 把 entry 层层嵌套在 owner 提供的 `children` 外面,并像 `chain` 提供 `matched` 那样提供按 kind 的 `children` prop;其决策见 [hoc 插槽类型 Note](2026-11-03-hoc-slot-kind.zh.md)。第四种 `SlotKind`——`'chain'`——把路由权相对 `keyed` 反转:keyed 的分派现场以 `entryKey` 点选占用 slot 的 entry,chain 则由 entry 自荐——owner 只分派一套格式统一的 owner props,永远不知道谁来接管,新的接管包注册进来 owner 零改动。chain 注册携带一个 `select` 纯选择器(`ChainSelect`:`(owner) => matched | null`)与可选的 `priority`(升序;同值保持注册序 = 装配序——部署可控的 inject 拓扑——复用 list `order` 的同一稳定排序);注册缺 `select` 即上文装载即炸情形之一。渲染时 outlet 按链序依次执行各 select:首个非 null 返回值当选,该值以 `matched` 并入组件 props(组件绝不自行重新推导匹配);返回 `null` 则轮到下一个 entry;全 null 则渲染 owner 的 fallback 体(`ChainRenderOpts`)。 「不接」的判定住在 `select` 里,绝不在挂载后的组件里自探 props:组件为了渲染 null 也得先挂载,其钩子与 effect 全部白跑,随之而来的挂载/卸载抖动还会破坏 memo 化与 React key 语义;而选择器是纯函数——可单测、零挂载副作用——与「presentation methods are pure functions of `args`」是同一条纪律。纯,就是选择器的约定:不读外部可变状态、不产副作用,路由判定因此完全是 owner props 的函数,每次分派都可安全执行。选择器只做路由,绝不创建新对象——按分派逐次构造对象会让引用每次渲染都换新;把匹配值包成更丰富的面这件事,发生在当选组件内部(以 `matched` 为依赖的 `useMemo`)。 diff --git a/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.i18n.yaml b/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.i18n.yaml new file mode 100644 index 0000000000..8826b372a2 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.i18n.yaml @@ -0,0 +1,25 @@ +# Bilingual-pair consistency record for 2026-11-03-hoc-slot-kind.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.md +/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers: + en: 8292b8f9884b8802 + zh: 3a4f34ca79313c9d +/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/problem: + en: d16c3bfef49e16be + zh: 6714da1c9c984fe9 +/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/decision: + en: 3a4380324cfc1679 + zh: 44db368e091cbd46 +/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/decision/implementation: + en: 01189e457c91deeb + zh: 94f7e8865b332a07 +/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/decision/first-consumer: + en: 1afdf4ceeef7a3b3 + zh: be01cf39850db0ca +/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/alternatives-considered: + en: b4824619c502b422 + zh: 610364f2dc8cb2b7 +/agent-note-add-the-hoc-slot-kind-so-plugins-can-wrap-owner-content-in-layers/consequences: + en: 7542ee2bc2c70258 + zh: bc0dd1dc6e50de66 diff --git a/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.md b/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.md new file mode 100644 index 0000000000..ccfb0d11f9 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.md @@ -0,0 +1,72 @@ +# Agent Note: Add the `hoc` slot kind so plugins can wrap owner content in layers + +Status: implemented + +English | [中文](2026-11-03-hoc-slot-kind.zh.md) + +## Problem + +The home whale plays when blank space is clicked. Its canvas must paint below the content, and `.scrollBody` fills the whole body, so the canvas itself never receives clicks; only an ancestor of the content does. + +No existing slot kind lets a plugin become an ancestor of the content: + +| Kind | Behavior | Why it is not enough | +|---|---|---| +| `single` / `list` / `keyed` | Render the occupant at an owner-chosen position | The occupant is a sibling of the content | +| `chain` | The elected entry replaces the fallback; with `overlay` it renders beside the fallback | It does not wrap | +| Factory local slot | The caller replaces a local Component per occurrence | Only the caller chooses; plugins cannot stack | + +The previous approach had the Conversation classify blank clicks on its body and notify the background plugin through `onBlankClick(listener) => dispose`. The Conversation thereby maintained click classification and subscription forwarding that served only a decorative plugin. + +## Decision + +Add a fifth kind: `hoc`. + +| Item | Contract | +|---|---| +| Declaration | The parent entry's `children` lists `{ kind: 'hoc', scope }`; `SlotMap` declares `kind: 'hoc'` | +| Rendering | `renderSlot(key, owner, { children })`; `entryKey` / `only` / `fallback` / `hookContext` are unavailable; omitted `children` renders the layers around nothing | +| Registration | Same as `list`: required `id`, optional `order` and `priority`; the same `id` at the same `priority` throws, a different `priority` shadows | +| Nesting order | Ascending `order`, then `priority`, then registration order; lowest outermost | +| Layer props | Every layer receives the same owner props plus the framework-supplied `children` (the next inner layer; the innermost is the owner content); `children` is not an owner prop, `HocShare` types it, and owner props must not use that name | +| Layer obligation | Render `children` exactly once without creating a box of its own; do not change the props passed inward | +| No entry | `children` renders unchanged | +| Layer crash | Each layer has its own error boundary; a crashed layer renders its `children` and abdicates, so the next render omits it | +| Content error | An error thrown by the wrapped content passes every layer as its original error and is reported against no layer | +| Remounting | Adding or removing a layer remounts the content inside it; a `session` or `session-maybe` layer also remounts it when its Session binding changes | + +### Implementation + +| Package | Realization | +|---|---| +| `ui-slots` | `SlotKind` includes `'hoc'`; `HocRenderOpts`, `HocKeysOf`, `HocShare`; `RenderSlotFn` has a hoc-key overload taking optional `{ children }`; `KindOptions` has a hoc branch; `SlotCore.register` / `entriesOfSlot` key hoc cells by `id` | +| `ui-renderer` | `HocOutlet` calls `composeHocLayers` when the slot's ledger version or declaration changes (`useMemo` on the outlet instance) and nests the layers into one render function. Each layer is one Component that fixes its scope-specific Entry (`StrictSessionEntry` / `SessionMaybeEntry` / `RootEntry`), entry, slot inject, and crash reporting at composition, and receives only `owner` and the next inner layer when rendering. With no layer the outlet returns the owner content directly. `HocContentBoundary` wraps each layer's inner content and rethrows its errors as `HocContentError`, which layer boundaries pass through and `HocOutletBoundary` unwraps to the original error. The Entry chain passes `children` to the registered Component as JSX children; non-hoc entries receive `undefined`. `SlotErrorBoundary` takes a `crashed` replacement node | +| `scripts/gen-client-catalog.ts` | Register options, a `children`-rendering example, and the `wraps-owner-content` replace risk for hoc slots | + +### First consumer + +| Location | Realization | +|---|---| +| `ui-conversation` | `conversation.hero.frame` (hoc, root) with owner props `{ hero: boolean }` wraps `.scrollBody` in every phase, so the composer does not remount when the home ends. On the home (`.root[data-phase='hero']`), the Conversation root, which paints the background, sets `isolation: isolate` as part of the frame contract, so layer decorations at negative `z-index` paint above that background and below all Conversation content. The Conversation performs no click classification for its layers | +| `ui-brand-official` | `PixelWhale` is a frame layer: a React `onClick` on a `display: contents` frame plays the whale for primary clicks on blank space, excluding portaled overlays (`currentTarget.contains(target)`), controls (a selector), and clicks that end a text selection inside the frame. While `hero` is true it paints a canvas at negative `z-index`. The frame atlas loads through the package asset route; the renderer returns `{ play, dispose }` | + +## Alternatives considered + +- `onBlankClick` subscription forwarding: the Conversation owns click classification that serves only a decorative plugin. +- Canvas below with `pointer-events: none` on `.scrollBody`: blank space cannot scroll when the home overflows. +- A canvas with no `z-index`: it needs no stacking context, but it paints over every unpositioned Conversation content outside the `z-index: 1` home composer stack, such as composer takeovers, and depends on that unrelated rule. +- A layer box that establishes the stacking context itself: the layer has to mirror the body's flex column layout. +- A separate `renderNestedSlot` or a JSX `` component: one more set of bindings; `hookContext` already shows that `renderSlot` can constrain its third argument per key. +- Precompiling every slot kind per ledger version: no measurable end-to-end time difference, see #7838. + +## Consequences + +- Adding or removing a layer remounts everything inside it, including the composer on the home. +- Every non-hoc registered Component receives `children: undefined`, which overrides any owner prop of that name. +- Each layer adds one Component and two error boundaries to the tree; each hoc slot with layers adds one outlet boundary. +- The outlet's `[data-slot]` element sits between the owner and its content, so owner selectors cannot assume the content is a direct child. +- React 18 recollects the component stack where an error is rethrown, so a content error reaching boundaries above a hoc slot with layers carries a component stack that starts at `HocOutletBoundary`. +- A layer that wraps its `children` in its own error boundary catches the internal `HocContentError` rather than the original content error. +- The frame contract holds only on the home: other phases leave the Conversation root out of the stacking order, so unportaled Conversation popovers still rise above AppFrame chrome that floats over the column, while on the home they stack only within the root. +- `HocOutlet` keeps its layer Components through `useMemo`, as Factory local Components keep theirs; if React ever discards memoized values, both need an explicit cache, because these Components are the remount boundary of the content inside them. +- [The slot system standard](2026-07-22-slot-type-chain-implementation.md) remains authoritative for the other kinds; this note adds the fifth. diff --git a/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.zh.md b/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.zh.md new file mode 100644 index 0000000000..f09a37fede --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-11-03-hoc-slot-kind.zh.md @@ -0,0 +1,72 @@ +# Agent Note: 新增 `hoc` 插槽类型,让插件层层包裹宿主内容 + +Status: implemented + +[English](2026-11-03-hoc-slot-kind.md) | 中文 + +## 问题 + +首页鲸鱼需要在空白处被点击时播放。canvas 必须画在内容下面,`.scrollBody` 铺满整个 body,所以 canvas 自己收不到点击;能收到点击的只有内容的祖先节点。 + +现有插槽类型都不能让插件成为内容的祖先: + +| 类型 | 行为 | 为什么不够 | +|---|---|---| +| `single` / `list` / `keyed` | 在宿主指定位置渲染 occupant | occupant 和内容是兄弟节点 | +| `chain` | 选中的 entry 替换 fallback;`overlay` 时与 fallback 并排 | 不包裹 | +| Factory 局部 slot | 调用方按次替换局部组件 | 只有调用方能选,插件无法叠加 | + +此前的做法是由 Conversation 在 body 上判定空白点击,再通过 `onBlankClick(listener) => dispose` 通知背景插件。这让 Conversation 维护一套只服务于装饰插件的点击分类和订阅转发。 + +## Decision + +新增第五种 kind:`hoc`。 + +| 项 | 约定 | +|---|---| +| 声明 | 父 entry 的 `children` 写 `{ kind: 'hoc', scope }`;`SlotMap` 中 `kind: 'hoc'` | +| 渲染 | `renderSlot(key, owner, { children })`;`entryKey` / `only` / `fallback` / `hookContext` 不可用;省略 `children` 时各层包裹空内容 | +| 注册 | 与 `list` 相同:必填 `id`,可选 `order`、`priority`;同 `id` 同 `priority` 抛错,不同 `priority` 遮蔽 | +| 嵌套顺序 | 依次按 `order`、`priority`、注册顺序升序,最小者在最外层 | +| 层的 props | 每层收到同一份 owner props,加上框架注入的 `children`(内一层,最内层是宿主内容);`children` 不属于 owner props,类型由 `HocShare` 提供,owner props 不得使用这个名字 | +| 层的义务 | 恰好渲染一次 `children`,自身不产生盒子;不能修改传给内层的 props | +| 无 entry | 原样渲染 `children` | +| 层崩溃 | 每层有独立 error boundary;崩溃时该层渲染自己的 `children` 并 abdicate,下一次渲染不再包含该层 | +| 内容出错 | 被包裹内容抛出的错误以原始错误穿过所有层,不记到任何层名下 | +| 重新挂载 | 增减一层会让其内部内容重新挂载;`session` 或 `session-maybe` 作用域的层在 Session binding 变化时也会让内部内容重新挂载 | + +### 实现 + +| 包 | 实现 | +|---|---| +| `ui-slots` | `SlotKind` 包含 `'hoc'`;`HocRenderOpts`、`HocKeysOf`、`HocShare`;`RenderSlotFn` 为 hoc key 提供接受可选 `{ children }` 的重载;`KindOptions` 有 hoc 分支;`SlotCore.register` / `entriesOfSlot` 按 `id` 划分 hoc cell | +| `ui-renderer` | `HocOutlet` 在插槽 ledger 版本或声明变化时调用 `composeHocLayers`(`useMemo` 缓存在 outlet 实例上),把各层组合成一个嵌套渲染函数。每层是一个组件,在组合时固定按 scope 选定的 Entry(`StrictSessionEntry` / `SessionMaybeEntry` / `RootEntry`)、entry、slot inject 与崩溃上报,渲染时只接收 `owner` 与内一层。没有层时 outlet 直接返回宿主内容。`HocContentBoundary` 包住每层的内层内容,把其中的错误包装为 `HocContentError` 重新抛出;层的边界放行这种错误,`HocOutletBoundary` 再解开为原始错误。Entry 链以 JSX children 把 `children` 交给注册组件;非 hoc entry 收到 `undefined`。`SlotErrorBoundary` 接受 `crashed` 替换节点 | +| `scripts/gen-client-catalog.ts` | hoc 插槽的注册选项、渲染 `children` 的示例,以及 `wraps-owner-content` 替换风险 | + +### 首个使用者 + +| 位置 | 实现 | +|---|---| +| `ui-conversation` | `conversation.hero.frame`(hoc,root),owner props 为 `{ hero: boolean }`,在所有阶段包裹 `.scrollBody`,首页结束时输入框不重新挂载。首页阶段(`.root[data-phase='hero']`),绘制背景的 Conversation 根元素设置 `isolation: isolate`,这是 frame 契约的一部分:层中负 `z-index` 的装饰画在该背景之上、所有 Conversation 内容之下。Conversation 不为层做任何点击判定 | +| `ui-brand-official` | `PixelWhale` 是 frame 层:`display: contents` 外框上的 React `onClick` 在主按钮点击留白时播放,排除 portal 浮层(`currentTarget.contains(target)`)、控件(选择器)和结束外框内文字选择的点击。`hero` 为 true 时以负 `z-index` 绘制 canvas。帧图经包 asset 路由加载;渲染器返回 `{ play, dispose }` | + +## Alternatives considered + +- `onBlankClick` 订阅转发:Conversation 承担只为装饰插件服务的点击分类。 +- canvas 放底下、`.scrollBody` 设 `pointer-events: none`:首页溢出时空白处无法滚动。 +- 不设 `z-index` 的 canvas:不需要层叠上下文,但会画在 `z-index: 1` 的首页输入区以外的所有非定位 Conversation 内容之上(例如输入区接管面板),并依赖那条无关的规则。 +- 由层自己的盒子建立层叠上下文:层必须复刻 body 的 flex 列布局。 +- 单独的 `renderNestedSlot` / JSX 组件 ``:多一套绑定;`hookContext` 已证明可以按 key 约束 `renderSlot` 的第三个参数。 +- 按 ledger 版本预编译所有插槽类型:端到端测不出耗时差异,见 #7838。 + +## Consequences + +- 增减一层会让其内部所有内容重新挂载,包括首页的输入框。 +- 所有非 hoc 的注册组件都会收到 `children: undefined`,会覆盖同名的 owner prop。 +- 每层在组件树中多一个组件和两个 error boundary;有层的 hoc 插槽另多一个 outlet 边界。 +- outlet 的 `[data-slot]` 元素位于 owner 与内容之间,owner 侧选择器不能假定内容是直接子元素。 +- React 18 会在错误被重新抛出的位置重新采集组件栈,因此有层的 hoc 插槽之上的边界收到的内容错误,组件栈从 `HocOutletBoundary` 开始。 +- 层如果自己在 `children` 外包错误边界,捕获到的是内部的 `HocContentError`,而不是原始的内容错误。 +- frame 契约只在首页阶段成立:其他阶段根元素不参与层叠隔离,未经 portal 的 Conversation 浮层仍能盖住悬浮在中栏上方的 AppFrame 元素;首页阶段它们只在根元素内部比较层级。 +- `HocOutlet` 与 Factory 局部组件一样用 `useMemo` 保持层组件身份;如果 React 将来会丢弃 memo 缓存,两处都需要改成显式缓存,因为这些组件是其内部内容的重新挂载边界。 +- 其他 kind 仍以 [slot 体系标准](2026-07-22-slot-type-chain-implementation.zh.md) 为准;本 Note 新增第五种。 diff --git a/apps/web/tests/home-whale.e2e.ts b/apps/web/tests/home-whale.e2e.ts new file mode 100644 index 0000000000..6e0154acdd --- /dev/null +++ b/apps/web/tests/home-whale.e2e.ts @@ -0,0 +1,201 @@ +/** Keyless real-profile checks for the default official-home decoration and native input isolation. */ +import { mkdir, mkdtemp } from 'node:fs/promises' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium, type Browser, type Page } from 'playwright' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { launchWebScaffold, watchConsole, type WebScaffold } from './scaffold.ts' +import { ZH_BROWSER_LOCALE } from './support.ts' + +const artifacts = fileURLToPath(new URL('../../../.artifacts/home-whale/', import.meta.url)) + +describe('official home whale in the default web profile', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let consoleState: ReturnType + let captureDir: string + + beforeAll(async () => { + await mkdir(artifacts, { recursive: true }) + captureDir = await mkdtemp(join(artifacts, 'run-')) + scaffold = await launchWebScaffold({ firstUse: true }) + browser = await chromium.launch() + page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE }) + consoleState = watchConsole(page) + await page.goto(scaffold.authenticatedUrl) + await page.locator('[data-home-whale]').waitFor() + await page.locator('[data-composer-input]').waitFor() + }, 120_000) + + afterAll(async () => { + try { + await browser?.close() + } finally { + await scaffold?.close() + } + }) + + it('alternates grayscale dot and code breaches on click and respects reduced motion', async () => { + const canvas = page.locator('[data-home-whale]') + const surface = page.locator('[data-conversation-content]') + const pixelSummary = () => canvas.evaluate((node: HTMLCanvasElement) => { + const values = node.getContext('2d')!.getImageData(0, 0, node.width, node.height).data + let saturated = 0 + let opaque = 0 + let nontransparent = 0 + let contrast = 0 + for (let i = 0; i < values.length; i += 4) { + const alpha = values[i + 3]! / 255 + if (alpha > 0) nontransparent++ + if (values[i + 3]! > 2) opaque++ + contrast = Math.max(contrast, (255 - Math.min(values[i]!, values[i + 1]!, values[i + 2]!)) * alpha) + if (values[i + 3]! > 10 && values[i + 2]! - values[i]! > 8 && values[i + 2]! - values[i + 1]! > 4) saturated++ + } + return { saturated, opaque, nontransparent, contrast } + }) + expect(await canvas.evaluate(node => getComputedStyle(node).pointerEvents)).toBe('none') + expect(await canvas.getAttribute('aria-hidden')).toBe('true') + expect(await canvas.evaluate(node => getComputedStyle(node).filter)).toBe('grayscale(1)') + await expect.poll(async () => (await pixelSummary()).nontransparent).toBe(0) + await page.screenshot({ path: `${captureDir}/idle.png` }) + const box = await surface.boundingBox() + expect(box).not.toBeNull() + const card = (await page.locator('[data-composer-card]').boundingBox())! + // The home above the input card, read from the composited page: it changes only if the canvas shows through. + const homeClip = { x: box!.x, y: box!.y, width: box!.width, height: card.y - box!.y } + const idleHome = await page.screenshot({ clip: homeClip }) + const marginX = box!.x + 12 + const marginY = card.y + card.height / 2 + expect(await page.evaluate(({ x, y }) => + document.elementFromPoint(x, y)?.closest('[data-composer-seat]') !== null, + { x: marginX, y: marginY })).toBe(true) + await page.clock.install() + await page.clock.pauseAt(await page.evaluate(() => Date.now() + 1000)) + try { + await page.mouse.click(marginX, marginY) + await expect.poll(async () => { + await page.clock.runFor(100) + return (await pixelSummary()).opaque + }).toBeGreaterThan(300) + await page.clock.runFor(1500) + await page.screenshot({ path: `${captureDir}/ripple.png` }) + await page.mouse.click(box!.x + box!.width * 0.78, box!.y + box!.height * 0.72) + expect((await pixelSummary()).opaque).toBeGreaterThan(0) + expect((await pixelSummary()).contrast).toBeLessThanOrEqual(100) + // The last visible pixels disappear before the playback deadline; finish the full timeline before the next click. + await page.clock.runFor(5700) + expect((await pixelSummary()).nontransparent).toBe(0) + + await page.mouse.click(box!.x + box!.width * 0.78, box!.y + box!.height * 0.72) + await page.clock.runFor(1500) + expect((await pixelSummary()).opaque).toBeGreaterThan(300) + expect((await page.screenshot({ clip: homeClip })).equals(idleHome)).toBe(false) + await page.screenshot({ path: `${captureDir}/code.png` }) + await page.clock.runFor(5700) + expect((await pixelSummary()).nontransparent).toBe(0) + + await page.emulateMedia({ reducedMotion: 'reduce' }) + const staticFrame = await canvas.evaluate((node: HTMLCanvasElement) => node.toDataURL()) + await page.mouse.click(box!.x + 100, box!.y + 150) + await page.clock.runFor(300) + expect(await canvas.evaluate((node: HTMLCanvasElement) => node.toDataURL())).toBe(staticFrame) + } finally { + await page.emulateMedia({ reducedMotion: 'no-preference' }) + await page.clock.resume() + } + }) + + it('keeps native draft editing independent of the decoration', async () => { + const input = page.locator('[data-composer-input]') + await input.fill('保留这个试用草稿') + const canvas = page.locator('[data-home-whale]') + const idleFrame = await canvas.evaluate((node: HTMLCanvasElement) => node.toDataURL()) + await input.click() + // Observe several normal animation frames while focus stays in the composer. + await page.waitForTimeout(300) + expect(await canvas.evaluate((node: HTMLCanvasElement) => node.toDataURL())).toBe(idleFrame) + expect(await input.innerText()).toBe('保留这个试用草稿') + await input.press('ControlOrMeta+A') + await input.press('Backspace') + }) + + it('paints the decoration between the Conversation background and the home content', async () => { + const order = await page.locator('[data-home-whale]').evaluate((node) => { + // The canvas's negative z-index resolves in the nearest ancestor that establishes a stacking context. Walk up to + // the document root and report the first one, which must be the home Conversation root that paints the background. + const stacking = (element: Element): boolean => { + const style = getComputedStyle(element) + return style.isolation === 'isolate' || (style.zIndex !== 'auto' && style.position !== 'static') + || style.transform !== 'none' || style.opacity !== '1' || style.filter !== 'none' + } + let context: Element | null = node.parentElement + while (context !== null && context !== document.documentElement && !stacking(context)) context = context.parentElement + if (context === null || context === document.documentElement) return { found: false } + return { + found: true, + canvasZIndex: getComputedStyle(node).zIndex, + conversationPhase: context.getAttribute('data-phase'), + isolation: getComputedStyle(context).isolation, + paintsBackground: getComputedStyle(context).backgroundColor !== 'rgba(0, 0, 0, 0)', + } + }) + expect(order).toEqual({ + found: true, canvasZIndex: '-1', conversationPhase: 'hero', isolation: 'isolate', paintsBackground: true, + }) + }) + + it('loads from the default composition without a separately enabled bundle', async () => { + await page.reload() + await page.locator('[data-home-whale]').waitFor() + const facts = await page.locator('[data-home-whale]').evaluate(node => ({ + count: document.querySelectorAll('[data-home-whale]').length, + hiddenFromAssistiveTech: node.getAttribute('aria-hidden'), + pointerEvents: getComputedStyle(node).pointerEvents, + nativeComposer: document.querySelectorAll('[data-composer-seat]').length, + })) + expect(facts).toMatchInlineSnapshot(` + { + "count": 1, + "hiddenFromAssistiveTech": "true", + "nativeComposer": 1, + "pointerEvents": "none", + } + `) + expect(consoleState.pageErrors).toEqual([]) + expect(consoleState.warnings).toEqual([]) + }) + + it('keeps neutral dots visible in both themes and fits a narrow home', async () => { + const settings = scaffold.ctx.get('settings')! + const canvas = page.locator('[data-home-whale]') + const visibleDots = () => canvas.evaluate((node: HTMLCanvasElement) => + node.getContext('2d')!.getImageData(0, 0, node.width, node.height).data.some((value, i) => i % 4 === 3 && value > 10)) + const clickHome = async () => { + const box = (await page.locator('[data-conversation-content]').boundingBox())! + await page.mouse.click(box.x + box.width * 0.78, box.y + box.height * 0.18) + } + await settings.update('ui-theme', { preference: 'light' }) + await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme')).toBeNull() + const lightInk = await canvas.evaluate(node => getComputedStyle(node).color) + await clickHome() + await expect.poll(visibleDots).toBe(true) + try { + await settings.update('ui-theme', { preference: 'dark' }) + await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme')).toBe('') + await expect.poll(() => canvas.evaluate(node => getComputedStyle(node).color)).not.toBe(lightInk) + // A click during playback is ignored and a fading breach can already read as blank, so keep clicking until a + // breach is visible after the theme switch. + await expect.poll(async () => { await clickHome(); return visibleDots() }, { timeout: 15_000 }).toBe(true) + await page.screenshot({ path: `${captureDir}/dark.png` }) + await page.setViewportSize({ width: 390, height: 844 }) + const box = (await canvas.boundingBox())! + expect(box.x).toBeGreaterThanOrEqual(0) + expect(box.x + box.width).toBeLessThanOrEqual(390) + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390) + } finally { + await page.setViewportSize({ width: 1440, height: 960 }) + await settings.update('ui-theme', { preference: 'light' }) + } + }) +}) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 9e08211588..fb40d60bb7 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -23,6 +23,7 @@ // this client-registered project must not also hold them (one program // cannot see both sides of the cordis Context merges). "exclude": [ + "tests/home-whale.e2e.ts", "tests/sidebar-drawer.e2e.ts", "tests/creation-recommendations.e2e.ts", "tests/plugin-recommendations.e2e.ts", diff --git a/docs/subsystems/slots.i18n.yaml b/docs/subsystems/slots.i18n.yaml index 3397485d88..a101ff98ea 100644 --- a/docs/subsystems/slots.i18n.yaml +++ b/docs/subsystems/slots.i18n.yaml @@ -9,11 +9,14 @@ en: f2d2c0d3b8f14ec7 zh: 441ed13ff3f81d2a /web-client-slots/cardinality-and-scope: - en: cd17e3e0ff6e62ff - zh: 6e0db2cb16c41b73 + en: 488f2f309af94398 + zh: 7941265e4c545c21 +/web-client-slots/cardinality-and-scope/hoc-layers: + en: e6862954bd8874af + zh: 0244a2d7bb0befdf /web-client-slots/component-inputs: - en: 9cb70abb2e39b958 - zh: cd4b6f1705d90e17 + en: 278b2eee160be413 + zh: d6e0fd1e9f4a4b27 /web-client-slots/framework-provided-hooks: en: c9f8d836214623ae zh: 76dec55006396535 @@ -24,5 +27,5 @@ en: e1d572c42e91d663 zh: 5a649dea2682be7f /web-client-slots/extension-rules: - en: b2cce357141883ee - zh: 278080a5fb941189 + en: e5fdcd6fafe4d161 + zh: 7fd0c4374705b372 diff --git a/docs/subsystems/slots.md b/docs/subsystems/slots.md index 1e551c2e51..d13357d0b2 100644 --- a/docs/subsystems/slots.md +++ b/docs/subsystems/slots.md @@ -61,11 +61,22 @@ The slot declaration fixes two independent axes. | cardinality | `list` | Cells are addressed by required `id` and ordered by `order`, then registration order. | | cardinality | `keyed` | The owner dispatches an `entryKey`; the matching cell renders with any key-specific props. | | cardinality | `chain` | Each entry supplies a pure `select(owner)` function. The first non-null result in priority order renders and receives that result as `matched`; otherwise the owner fallback renders. | +| cardinality | `hoc` | Entries nest around the owner's `renderSlot(key, owner, { children })` content, lowest outermost; see [Hoc layers](#hoc-layers). | | scope | `root` | One root-scoped component and store instance. | | scope | `session-maybe` | Inherits the surrounding Provider binding but stays renderable without one; Session values are optional. | | scope | `session` | Requires a resolved surrounding Provider binding and receives definite Session values. | -`priority` is a shadowing rank for `single`, `list`, and `keyed` cells and an election order for `chain`. Lower values run or render first. Ordinary additive contributions should choose a fresh list `id` or keyed `key`; intentionally reusing a shipped cell replaces its presentation. +`priority` is a shadowing rank for `single`, `list`, `keyed`, and `hoc` cells and an election order for `chain`. Lower values run or render first. Ordinary additive contributions should choose a fresh list or hoc `id`, or keyed `key`; intentionally reusing a shipped cell replaces its presentation. + + + +### Hoc layers + +A hoc entry is a layer addressed by its required `id`. Layers nest by ascending `order`, then `priority`, then registration order, lowest outermost. Each layer receives the same owner props plus the next inner layer as `children`, the owner's content at the innermost, and renders it exactly once; owner props must not use the name `children`. Omitted `children` renders the layers around nothing, and with no live layer the content renders unchanged. + +- Remounting: adding or removing a layer remounts the content inside it, including a crashed layer leaving the slot; a `session` or `session-maybe` layer also remounts it when its Session binding changes. +- Errors: a crashed layer renders its `children` and leaves the slot. An error thrown by the wrapped content passes every layer and is reported against none; a layer that wraps its `children` in its own error boundary catches it as an internal wrapper error rather than the original. +- Layout: the outlet inserts a `display: contents` `[data-slot]` element between the owner and its content, so owner selectors cannot rely on the content being a direct child. A layer must not create a box of its own — for example, use `display: contents` — or the content leaves its parent's layout. The [context-menu plugin](../../packages/client/ui-context-menu/README.md) contributes `useContextMenu` through `provideRoot`; every scope receives the same plugin-owned trigger factory. Its prop and retained handlers retire when the provider unloads. Its root-scoped keyed `contextMenu.icon` Slot renders business-owned icons from action metadata. @@ -81,6 +92,7 @@ A registered component receives inputs assembled at its binding site. Components | private data, callbacks, and observable hooks | the registration's `inject` factory | `InjectFace` | | localized `t` function | the registration's `locale` namespace | `PropsLocale` | | selected chain value | the registration's `select` result | `matched` through `ComposedProps` | +| wrapped owner content | the `SlotMap` row's `hoc` kind | `children` through `ComposedProps` | `SessionProvider` is also present in `PropsRenderSlots` when an entry declares a `session` or `session-maybe` child. With no `session` prop it inherits the surrounding binding; an explicit `SessionReference` or `undefined` overrides only that subtree. The Provider does not key its whole body. A strict `session` entry remounts when its binding generation changes. A blank `session-maybe` entry adopts its first binding without remounting, then remounts for a later generation or a return to absence. @@ -182,6 +194,8 @@ Factory: conversation.content [ConversationContent] ├─ conversation.input.dock │ └─ id=queue [QueueDock] │ └─ calls Factory: conversation.user.text +├─ conversation.hero.frame [hoc] +│ └─ id=pixel-whale [PixelWhale] ├─ conversation.hero.brand.mark ├─ conversation.hero.workspace │ └─ conversation.hero.workspace.directoryFlow @@ -210,4 +224,4 @@ The generated [Client inspect catalog](../../packages/extensions/cordis-client-r - Keep business and transport state in their owning Cordis services or Client models. Slot stores hold shared viewing and interaction state only. - Keep observable source and snapshot identities stable between changes. Republish through the same source whenever its value changes. - Pass JSON-compatible data and callbacks between UI domains. The `hooks` compartment is the sole exception for bare observables; React content travels through slots. -- Treat `single` and an occupied keyed cell as replacement points. Use list ids or an unoccupied key for additive extensions. +- Treat `single` and an occupied keyed cell as replacement points. Use fresh list or hoc ids, or an unoccupied key, for additive extensions. diff --git a/docs/subsystems/slots.zh.md b/docs/subsystems/slots.zh.md index 8260a34075..61014d4418 100644 --- a/docs/subsystems/slots.zh.md +++ b/docs/subsystems/slots.zh.md @@ -61,11 +61,22 @@ Slot 声明固定两个相互独立的维度。 | cardinality | `list` | cell 由必填 `id` 定址,先按 `order`、再按注册顺序排列。 | | cardinality | `keyed` | owner 传入 `entryKey`;匹配 cell 以该 key 对应的 props 渲染。 | | cardinality | `chain` | 每个 entry 提供纯 `select(owner)` 函数;按 priority 顺序遇到的第一个非 null 结果获选,并以 `matched` 传给组件;全部拒绝时渲染 owner fallback。 | +| cardinality | `hoc` | entry 层层包裹 owner 经 `renderSlot(key, owner, { children })` 传入的内容,最小者在最外层;见 [Hoc 层](#hoc-layers)。 | | scope | `root` | 一个 root 作用域组件和 store 实例。 | | scope | `session-maybe` | 继承外围 Provider binding,但没有 binding 时仍可渲染;Session 值是可选的。 | | scope | `session` | 要求可解析的外围 Provider binding,并收到确定存在的 Session 值。 | -对于 `single`、`list` 和 `keyed` cell,`priority` 是遮蔽优先级;对于 `chain`,它是选举顺序。数值越小越先运行或渲染。普通增量贡献应选用新的 list `id` 或 keyed `key`;复用已有 cell 表示有意替换其展示。 +对于 `single`、`list`、`keyed` 和 `hoc` cell,`priority` 是遮蔽优先级;对于 `chain`,它是选举顺序。数值越小越先运行或渲染。普通增量贡献应选用新的 list 或 hoc `id`,或 keyed `key`;复用已有 cell 表示有意替换其展示。 + + + +### Hoc 层 + +hoc entry 是以必填 `id` 寻址的一层。各层依次按 `order`、`priority`、注册顺序升序嵌套,最小者在最外层。每层收到同一份 owner props,并以 `children` 收到内一层(最内层是 owner 的内容),且只渲染一次;owner props 不得使用 `children` 这个名字。省略 `children` 时各层包裹空内容;没有存活的层时原样渲染内容。 + +- 重新挂载:增减一层会让其内部内容重新挂载,崩溃的层退出 slot 也是如此;`session` 或 `session-maybe` 作用域的层在 Session binding 变化时也会让内部内容重新挂载。 +- 错误:崩溃的层渲染它的 `children` 并退出 slot。被包裹内容抛出的错误穿过所有层,不记到任何层名下;层如果自己在 `children` 外包错误边界,捕获到的是内部包装错误而不是原始错误。 +- 布局:outlet 会在 owner 与内容之间插入一个 `display: contents` 的 `[data-slot]` 元素,所以 owner 侧的选择器不能假定内容是直接子元素。层自身不得产生盒子——例如应使用 `display: contents`——否则内容会脱离父元素的布局。 @@ -83,6 +94,7 @@ Slot 声明固定两个相互独立的维度。 | 私有数据、callback 与 observable hook | 注册项的 `inject` factory | `InjectFace` | | 本地化 `t` 函数 | 注册项的 `locale` namespace | `PropsLocale` | | chain 选中的值 | 注册项的 `select` 结果 | 通过 `ComposedProps` 提供的 `matched` | +| 被包裹的 owner 内容 | `SlotMap` 条目的 `hoc` kind | 通过 `ComposedProps` 提供的 `children` | 当 entry 声明 `session` 或 `session-maybe` child 时,`PropsRenderSlots` 还会提供 `SessionProvider`。不传 `session` prop 时,它继承外围 binding;显式传入 `SessionReference` 或 `undefined` 时,只覆盖该子树。Provider 不为整个 body 设置 key。严格 `session` entry 在 binding generation 改变时重新挂载。空白 `session-maybe` entry 接受首个 binding 时不重新挂载,后续 generation 变化或回到缺失状态时才重新挂载。 @@ -184,6 +196,8 @@ Factory: conversation.content [ConversationContent] ├─ conversation.input.dock │ └─ id=queue [QueueDock] │ └─ calls Factory: conversation.user.text +├─ conversation.hero.frame [hoc] +│ └─ id=pixel-whale [PixelWhale] ├─ conversation.hero.brand.mark ├─ conversation.hero.workspace │ └─ conversation.hero.workspace.directoryFlow @@ -214,4 +228,4 @@ Factory: conversation.chat.reasoning.content [ReasoningContent / MarkdownText] - 业务与传输状态留在所属 Cordis service 或 Client model 中。Slot store 只承载共享的视图与交互状态。 - 可观测 source 及其 snapshot identity 在值变化前保持稳定;值变化时通过同一个 source 发布。 - UI domain 之间只传 JSON 兼容数据和 callback。`hooks` compartment 是裸 observable 的唯一例外;React 内容通过 slot 传递。 -- 将 `single` 和已有 occupant 的 keyed cell 视为替换点。增量扩展使用 list id 或尚未占用的 key。 +- 将 `single` 和已有 occupant 的 keyed cell 视为替换点。增量扩展使用新的 list 或 hoc id,或尚未占用的 key。 diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index eb076e461e..fb307f3e6a 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -339,7 +339,7 @@ - id: ui-chat name: '@deepseek-ai/dsh-client-ui-chat' - # Official occupants for the generic sidebar and conversation brand slots. + # Official occupants for the sidebar brand slots and the home whale frame layer. - id: ui-brand-official name: '@deepseek-ai/dsh-client-ui-brand-official' diff --git a/packages/client/ui-brand-official/README.i18n.yaml b/packages/client/ui-brand-official/README.i18n.yaml index dd14a7069f..ae7e1f61bc 100644 --- a/packages/client/ui-brand-official/README.i18n.yaml +++ b/packages/client/ui-brand-official/README.i18n.yaml @@ -3,32 +3,35 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-brand-official/README.md /: - en: a031cd88e552dc89 - zh: 76c1af2393cb6983 + en: a46a90b8cedd5c45 + zh: 9b4f0d5fd9ffa945 /deepseek-ai-dsh-client-ui-brand-official: en: 7cee1be833cea807 zh: 9953e1b38cb23d34 /deepseek-ai-dsh-client-ui-brand-official/summary: - en: cb6bf0a4eabd271b - zh: 181cf091ff44c194 + en: 784b8216e88b0578 + zh: 70165dea21d9a2ab /deepseek-ai-dsh-client-ui-brand-official/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-brand-official/use-this-package: - en: 716dc268f98996db - zh: 7372a67ef81a0d5e + en: 8144a213ca978516 + zh: 43dfefbd708d87bf /deepseek-ai-dsh-client-ui-brand-official/use-this-package/choosing-the-profile: - en: 3f3f96d6adf98193 - zh: 0b8691656a8532a0 + en: 876e7a0e21eaa396 + zh: 596fcdec565e3eae +/deepseek-ai-dsh-client-ui-brand-official/use-this-package/home-feedback: + en: f79f52c71a3d1491 + zh: bda67c8b4050316a /deepseek-ai-dsh-client-ui-brand-official/use-this-package/replacing-the-brand: - en: 6a923aa630d01d65 - zh: 5df0c24eebd9d0d0 + en: 3fd4b315b27cfa23 + zh: 8f76a10d642a5811 /deepseek-ai-dsh-client-ui-brand-official/understand-the-implementation: - en: 4e4aa617edd7f4ad - zh: ea4e2d6d4a39535e + en: 4b59ceb74b5c9d1e + zh: c22bcc7d7344fe12 /deepseek-ai-dsh-client-ui-brand-official/further-exploration: - en: c9ae240d8e831cc0 - zh: a7c9170bae79824f + en: 268cb9edf0c7d2f0 + zh: 9b75285a61b44008 /deepseek-ai-dsh-client-ui-brand-official/model-experience: en: 618008c7641070e7 zh: 7233febb4d92ccfb @@ -36,8 +39,8 @@ en: ca75c51c89c2c9b0 zh: f57cc48a1b5cbfa9 /deepseek-ai-dsh-client-ui-brand-official/known-limitations-and-deferred-work: - en: 89e2b4bd095a0265 - zh: a002f0c8a60267ad + en: b8f0ff2ff02bc0be + zh: 8f52f9dee0225fad /deepseek-ai-dsh-client-ui-brand-official/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-brand-official/README.md b/packages/client/ui-brand-official/README.md index 75663d20ab..42df355112 100644 --- a/packages/client/ui-brand-official/README.md +++ b/packages/client/ui-brand-official/README.md @@ -1,5 +1,5 @@ --- -description: "Official DeepSeek Harness brand occupants for the sidebar, active only in official builds; for users and maintainers choosing or replacing brand presentation." +description: "Official sidebar branding and the default click-triggered pixel whale on the home page." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This package gives an `official` client build the DeepSeek Harness mark and name in the sidebar. Other build profiles keep the shell's fish mark and local-build label, while the conversation hero always uses the animated fish. Choose it for deployments branded as DeepSeek Harness; deployments with another identity should provide a replacement brand package. It has no runtime state and does not affect model requests. +This package supplies a click-triggered pixel whale on the blank home and gives an `official` client build the DeepSeek Harness sidebar artwork. The shipped Web and Desktop compositions load it by default. Other build profiles retain their existing branding. The hero mark stays on its animated fish fallback. The background owns only temporary canvas resources and adds no model requests. ## Table of Contents @@ -25,15 +25,23 @@ This package gives an `official` client build the DeepSeek Harness mark and name ## Use this package -Mount this plugin in the browser roster of a deployment whose identity is DeepSeek's own, then build the client with the `official` profile so the occupants register. +The shipped Web and Desktop compositions already mount this plugin. Home feedback needs no extra installation or enablement; an `official` client build also registers the DeepSeek sidebar artwork. ### Choosing the profile -`DSH_CLIENT_BUILD_PROFILE` selects which brand renders. An `official` build shows the official mark and name in the sidebar; any other value leaves the shell fallbacks — the fish mark and the local-build label — in place. The conversation hero shows the animated hero fish from `dsh-client-ui-conversation` regardless of profile, because that fallback is already the official mark. The plugin still loads and validates in both cases; only the registration is profile-gated. +`DSH_CLIENT_BUILD_PROFILE` selects which brand renders. An `official` build shows the official sidebar mark and name; any other value leaves the shell fallbacks — the fish mark and the local-build label — in place. The conversation hero shows the animated hero fish from `dsh-client-ui-conversation` regardless of profile, because that fallback is already the official mark. The plugin still loads and validates in both cases; only the sidebar artwork registration is profile-gated; the home background is shared across build profiles. + +### Home feedback + +The default home stays clear until a primary click on empty space plays one light-gray whale breach. Successive completed clicks alternate between dots and code lettering, starting with dots. Both use the same large animated silhouette. The animation lasts 5.7 seconds, then clears. Repeated clicks leave the current playback uninterrupted. The whale's frame layer classifies clicks: blank space inside its frame triggers playback, including margins beside the input card, while input fields, controls, the input card, portaled overlays, and a click that ends a text selection never do. The canvas takes neither focus nor pointer events; the frame receives the clicks. On narrow homes the whale keeps a larger scale and is cropped at the canvas edges, without adding horizontal scrolling. Reduced-motion preference disables it, and leaving the blank home releases its animation and observers. + +The whale uses very faint neutral theme ink, appearing as a subtle background texture; code lettering has more contrast than the dots. Water splashes retain fine particles with stronger ink, especially on the left during landing. A packaged frame atlas supplies the motion; the browser loads it once through the plugin asset route on the first eligible click. Code fills use dense 7px lettering drawn only from authored code slogans, and the moving silhouette clips text as it enters or leaves the water. Decode or pixel-read failures stop the decoration, leave home usable, and log a reload action. + +The artwork values are fixed and the row takes no configuration. This is built-in presentation, without a separately installed bundle or Plugins-page toggle. ### Replacing the brand -A deployment with its own identity leaves this package out and composes another package that occupies the sidebar slots — and the hero slot, which this package leaves on its fallback. Occupying a slot is the only composition route; there is no brand configuration surface here. +A deployment with its own identity leaves this package out and composes another package that occupies the sidebar slots and the `conversation.hero.frame` layer — and the hero-mark slot, which this package leaves on its fallback. Occupying a slot is the only composition route; the artwork identity is not configurable. ----- @@ -43,7 +51,7 @@ A deployment with its own identity leaves this package out and composes another
Implementation internals — click to expand -The two occupants install as one declaration-aware registration set: nested `ctx.slots.inject()` calls wait on the sidebar declaration, so the set works whether this row activates before or after the declarer, withdraws both occupants when the declaration collapses, and leaves no partial brand mix during HMR. The browser half is [`src/client/index.ts`](src/client/index.ts); the node half is an empty Loader seat. The browser title is a build-environment concern (`DSH_CLIENT_TITLE`), outside the slot system. +The two occupants install as one declaration-aware registration set: nested `ctx.slots.inject()` calls wait on the sidebar declaration, so the set works whether this row activates before or after the declarer, withdraws both occupants when the declaration collapses, and leaves no partial brand mix during HMR. The independent `conversation.hero.frame` layer owns the canvas renderer and waits for its declaration through `ctx.slots.inject()`. While `hero` is true it paints the canvas at negative `z-index`, which the frame contract places above the Conversation background and below all Conversation content. It plays on primary clicks that reach its own frame outside controls and the composer card; clicks from portaled overlays and clicks that end a text selection inside the frame are ignored. It never changes the composer or conversation state. The browser half is [`src/client/index.ts`](src/client/index.ts); the node half is an empty Loader seat. The [frame atlas](src/client/assets/breach-atlas.webp) derives from the user-supplied [showcase video](https://yinghuo-hg.deepseek.com/wuyexinrui/dev-cpu/web-showcase/orca.mp4): 1.5–7.2 seconds at 24 frames per second, sampled to 192 × 108 cells and stored as lossless WebP. Its red and green channels encode dot radius and opacity at 32 levels; the renderer reads it once after the first eligible click. The browser title is a build-environment concern (`DSH_CLIENT_TITLE`), outside the slot system.
@@ -55,7 +63,7 @@ The two occupants install as one declaration-aware registration set: nested `ctx Read these pages when the brand surface is not enough. They move from the slots this package occupies to the shell that renders them. - [ui-sidebar](../ui-sidebar/README.md) — declares `sidebar.brand.mark` and `sidebar.brand.name` and renders their fallbacks. -- [ui-conversation](../ui-conversation/README.md) — declares `conversation.hero.brand.mark` in the hero. +- [ui-conversation](../ui-conversation/README.md) — declares `conversation.hero.brand.mark` in the hero and the `conversation.hero.frame` layer this package occupies. - [Web client architecture](../../../docs/subsystems/web-client.md) — how browser plugin rows load and register slots. ----- @@ -77,6 +85,7 @@ None; this package neither assembles nor sends a provider request. These limits define how brand presentation is supplied. They are current package constraints, not a brand-design comparison or a task backlog. - **One occupant set** — alternative presentation belongs in another Cordis package occupying the same slots. +- **Decorative rendering requires Canvas 2D** — unavailable contexts or reduced-motion preference leave the home clear. - **The browser title is independent** — `DSH_CLIENT_TITLE` selects title text at build time rather than through a UI slot. diff --git a/packages/client/ui-brand-official/README.zh.md b/packages/client/ui-brand-official/README.zh.md index 29be089ecc..5ef55832a4 100644 --- a/packages/client/ui-brand-official/README.zh.md +++ b/packages/client/ui-brand-official/README.zh.md @@ -1,5 +1,5 @@ --- -description: "面向侧栏的官方 DeepSeek Harness 品牌填充,仅在官方构建中生效;供选择或替换品牌呈现的用户与维护者阅读。" +description: "官方侧栏品牌与默认首页点击浮出的像素鲸鱼。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -本包让以 `official` profile 构建的客户端在侧栏显示 DeepSeek Harness 标志与名称。其他构建 profile 保留外壳的鱼形标志与本地构建标签,会话首屏则始终使用动画鱼。品牌为 DeepSeek Harness 的部署应选择本包;使用其他品牌的部署应提供替代品牌包。本包不保留运行时状态,也不影响模型请求。 +本包在空白首页提供点击浮出的像素鲸鱼,并让以 `official` profile 构建的客户端显示 DeepSeek Harness 侧栏品牌图形。随产品提供的 Web 与 Desktop 组合默认加载本包。其他构建 profile 保留原有品牌。首屏标志仍使用动画鱼回退。背景只持有临时 Canvas 资源,不增加模型请求。 ## 目录 @@ -25,15 +25,23 @@ kind: "package-reference" ## 使用本包 -在采用 DeepSeek 自有品牌的部署中,将本插件挂载到浏览器插件名单,然后以 `official` profile 构建客户端,让填充得以注册。 +随产品提供的 Web 与 Desktop 组合已挂载本插件。首页反馈不需要额外安装或启用;`official` 客户端构建还会注册 DeepSeek 侧栏图形。 ### 选择 profile -`DSH_CLIENT_BUILD_PROFILE` 决定渲染哪个品牌。`official` 构建在侧栏显示官方标志与名称;任何其他取值都让外壳回退——鱼形标志与本地构建标签——保持原样。会话首屏无论 profile 如何都显示来自 `dsh-client-ui-conversation` 的动画首屏鱼,因为这个回退本身就是官方标志。两种情况下插件都会照常加载并通过校验;只有注册受 profile 门控。 +`DSH_CLIENT_BUILD_PROFILE` 决定渲染哪个品牌。`official` 构建显示官方侧栏标志和名称;任何其他取值都让外壳回退——鱼形标志与本地构建标签——保持原样。会话首屏无论 profile 如何都显示来自 `dsh-client-ui-conversation` 的动画首屏鱼,因为这个回退本身就是官方标志。两种情况下插件都会照常加载并通过校验;只有侧栏图形注册受 profile 门控;各构建 profile 共用首页背景。 + +### 首页反馈 + +默认首页保持留白,主按钮点击空白处才播放一次浅灰色鲸鱼跃起。每次播放结束后的点击在点阵与代码填充之间交替,首次为点阵;两种填充使用相同的大幅动态轮廓。动画持续 5.7 秒,随后清空。连续点击不会打断当前播放。点击由鲸鱼的 frame 层自己判定:外框内部的留白可触发播放,包括输入卡片两侧的空白;输入框、控件、输入卡片、portal 出去的浮层以及结束文字选择的点击都不触发。Canvas 不获取焦点或指针事件,点击由外框接收。窄屏首页中的鲸鱼保留较大比例,在 Canvas 边缘裁切,不增加横向滚动。减少动态效果偏好会禁用它,离开空白首页时释放动画与观察器。 + +鲸鱼使用非常浅的主题中性色,呈现轻微的背景隐纹;代码文字的对比度略高于点阵。水花保留细小颗粒并使用较深的颜色,尤其是入水时左侧的水花。随包帧图提供动作,首次符合条件的点击时由浏览器经插件 asset 路由加载一次。代码填充使用密排的 7px 文字,内容只来自预设的代码标语,出水与入水时文本随动态轮廓裁切。解码或像素读取失败时停止装饰,首页仍可正常使用,并在日志中提示重新加载。 + +图形参数固定,本行不接受配置。这是内置呈现,没有需要单独安装的 Bundle 或插件页面开关。 ### 替换品牌 -自有身份的部署不组合本包,而是组合另一个占据侧栏 slot——以及本包留给回退的首屏 slot——的包。占据 slot 是唯一的组合路径;这里不存在任何品牌配置面。 +自有身份的部署不组合本包,而是组合另一个占据侧栏 slot 和 `conversation.hero.frame` 层——以及本包留给回退的首屏标志 slot——的包。占据 slot 是唯一的组合路径;品牌图形身份不可配置。 ----- @@ -43,7 +51,7 @@ kind: "package-reference"
实现细节——点击展开 -两个填充作为一组声明感知的注册安装:嵌套的 `ctx.slots.inject()` 调用等待侧栏声明,因此无论本行在声明者之前还是之后激活,这组注册都能工作;声明消失时两个填充一并撤回,HMR 期间也不会留下残缺的品牌混合。浏览器半部是 [`src/client/index.ts`](src/client/index.ts);node 半部是一个空 Loader 座位。浏览器标题是构建环境的事(`DSH_CLIENT_TITLE`),不在 slot 系统之内。 +两个填充作为一组声明感知的注册安装:嵌套的 `ctx.slots.inject()` 调用等待侧栏声明,因此无论本行在声明者之前还是之后激活,这组注册都能工作;声明消失时两个填充一并撤回,HMR 期间也不会留下残缺的品牌混合。独立的 `conversation.hero.frame` 层持有 Canvas 绘制器,通过 `ctx.slots.inject()` 等待声明;`hero` 为 true 时它以负 `z-index` 绘制 Canvas,按 frame 层约定画在 Conversation 背景之上、所有 Conversation 内容之下。它在点击落到自身外框、且不在控件和输入卡片上时播放;portal 浮层的点击以及结束外框内文字选择的点击会被忽略。它不更改输入框或会话状态。浏览器半部是 [`src/client/index.ts`](src/client/index.ts);node 半部是空的 Loader 席位。[帧图](src/client/assets/breach-atlas.webp) 来自用户提供的[展示视频](https://yinghuo-hg.deepseek.com/wuyexinrui/dev-cpu/web-showcase/orca.mp4):截取 1.5–7.2 秒,以每秒 24 帧采样为 192 × 108 个单元,以无损 WebP 存储。红色与绿色通道以 32 级分别编码圆点半径和不透明度;绘制器在首次符合条件的点击后读取一次。浏览器标题是构建环境的事(`DSH_CLIENT_TITLE`),不在 slot 系统之内。
@@ -55,7 +63,7 @@ kind: "package-reference" 当品牌面不够用时阅读以下页面。它们从本包占据的 slot 进入渲染这些 slot 的外壳。 - [ui-sidebar](../ui-sidebar/README.zh.md)——声明 `sidebar.brand.mark` 与 `sidebar.brand.name` 并渲染其回退。 -- [ui-conversation](../ui-conversation/README.zh.md)——在首屏声明 `conversation.hero.brand.mark`。 +- [ui-conversation](../ui-conversation/README.zh.md)——在首屏声明 `conversation.hero.brand.mark`,以及本包占用的 `conversation.hero.frame` 层。 - [Web 客户端架构](../../../docs/subsystems/web-client.zh.md)——浏览器插件行如何加载并注册 slot。 ----- @@ -77,6 +85,7 @@ kind: "package-reference" 这些限制界定了品牌呈现的供给方式。它们是当前包约束,不是品牌设计对比或任务积压。 - **只有一组填充**——替代呈现属于占据相同 slot 的另一个 Cordis 包。 +- **装饰绘制需要 Canvas 2D**——上下文不可用或偏好减少动态效果时,首页保持留白。 - **浏览器标题独立**——`DSH_CLIENT_TITLE` 在构建时选择标题文本,而非通过 UI slot。 diff --git a/packages/client/ui-brand-official/package.json b/packages/client/ui-brand-official/package.json index 47c1960beb..0d88d7f266 100644 --- a/packages/client/ui-brand-official/package.json +++ b/packages/client/ui-brand-official/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-ui-brand-official", - "description": "Official DeepSeek Harness brand occupants for the Web client's sidebar slots", + "description": "Official DeepSeek Harness brand artwork and click-triggered home background", "version": "0.2.2-alpha.3", "publishConfig": { "access": "public" @@ -24,13 +24,15 @@ }, "./src/*": "./src/*", "./package.json": "./package.json", - "./locale/*.json": "./locale/*.json" + "./locale/*.json": "./locale/*.json", + "./assets/*": "./lib/assets/*" }, "dsh": { "client": { "inject": [ "@deepseek-ai/dsh-client-ui-renderer", - "@deepseek-ai/dsh-client-ui-sidebar" + "@deepseek-ai/dsh-client-ui-sidebar", + "@deepseek-ai/dsh-client-ui-conversation" ], "platform": "web" } @@ -50,12 +52,15 @@ "@deepseek-ai/cordis": "workspace:~", "@testing-library/react": "^16.3.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "@deepseek-ai/dsh-client-ui-conversation": "workspace:*", + "@deepseek-ai/dsh-client-ui-slots": "workspace:*" }, "files": [ "locale/*.json", "lib/index.js", "lib/client.js", + "lib/assets", "lib/types/**/*.d.ts" ] } diff --git a/packages/client/ui-brand-official/src/client/assets/breach-atlas.webp b/packages/client/ui-brand-official/src/client/assets/breach-atlas.webp new file mode 100644 index 0000000000000000000000000000000000000000..1d2f5312d25da44e5bde72521847671d806483b9 GIT binary patch literal 189208 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%0Dp=$45!jjjT0f?RipA4>+XL)$Xuiiy6{ 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%0DpM-{>A=t`HY$C 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�)C%6))5iHMZt8Mug3> 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

D_ssimt?31RcmIy=1|9z7Yj?ofH}T;c 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

?!jQ;xE?bR+Dyht+WlhsUrk=78`Wqi(wpg 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 literal 0 HcmV?d00001 diff --git a/packages/client/ui-brand-official/src/client/home-whale/PixelWhale.module.css b/packages/client/ui-brand-official/src/client/home-whale/PixelWhale.module.css new file mode 100644 index 0000000000..03766a54d4 --- /dev/null +++ b/packages/client/ui-brand-official/src/client/home-whale/PixelWhale.module.css @@ -0,0 +1,20 @@ +/* Layout-neutral: the wrapped scroll body stays a direct flex item of the Conversation body. */ +.layer { + display: contents; +} + +.canvas { + --home-whale-code-ink: var(--dsw-alias-label-primary); + /* The host isolates stacking at the Conversation root, so -1 lands above its background and below its content. */ + position: absolute; + inset: 0; + z-index: -1; + width: 100%; + height: 100%; + display: block; + pointer-events: none; + opacity: 0.18; + color: var(--dsw-alias-label-secondary); + filter: grayscale(1); + font-family: var(--ds-font-family-code); +} diff --git a/packages/client/ui-brand-official/src/client/home-whale/PixelWhale.tsx b/packages/client/ui-brand-official/src/client/home-whale/PixelWhale.tsx new file mode 100644 index 0000000000..7a2af72ae2 --- /dev/null +++ b/packages/client/ui-brand-official/src/client/home-whale/PixelWhale.tsx @@ -0,0 +1,53 @@ +/** Home frame layer: a decorative canvas below the Conversation content, played by clicks on blank home space. */ +import { useCallback, useRef, type MouseEvent, type ReactNode } from 'react' +import type { HeroFrameOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client' +import atlasUrl from '../assets/breach-atlas.webp' +import { WHALE_ARTWORK } from './artwork.ts' +import { attachPixelWhale, type PixelWhaleHandle } from './renderer.ts' +import css from './PixelWhale.module.css' + +/** Interactive descendants whose clicks belong to the control, not to blank home space. */ +const CONTROLS = 'button, a, input, textarea, select, label, [contenteditable], [role="button"], [role="dialog"], [role="menu"], [role="listbox"], [data-composer-card]' + +/** Frame owner state and the wrapped content. */ +export type PixelWhaleProps = HeroFrameOwnerProps & { children: ReactNode } + +/** + * Classify a click that bubbled to the frame. React bubbles portaled overlays through their React parent, so a + * target outside the frame's DOM subtree is excluded. A click that ends a text selection made inside the frame is + * not a blank click; a selection elsewhere in the document does not block playback. + * @param event - the click observed on the frame. + * @returns whether the click is a primary click on blank home space. + */ +function isBlankClick(event: MouseEvent): boolean { + // React reports element targets; text-node targets resolve to their parent element. + const target = event.target as Element + const frame = event.currentTarget + const selection = window.getSelection() + return event.button === 0 + && frame.contains(target) + && target.closest(CONTROLS) === null + && !(selection !== null && !selection.isCollapsed && frame.contains(selection.anchorNode)) +} + +/** + * Wrap the Conversation scroll body; while the home is shown, paint the whale canvas below the Conversation content. + * @param props - the home state and the wrapped scroll body. + * @returns a layout-neutral frame around the children. + */ +export function PixelWhale({ hero, children }: PixelWhaleProps) { + const whale = useRef(undefined) + const canvasRef = useCallback((canvas: HTMLCanvasElement | null) => { + whale.current?.dispose() + whale.current = canvas === null ? undefined : attachPixelWhale(canvas, WHALE_ARTWORK, atlasUrl) + }, []) + const onClick = (event: MouseEvent): void => { + if (whale.current !== undefined && isBlankClick(event)) whale.current.play() + } + return ( +
+ {hero &&
+ ) +} diff --git a/packages/client/ui-brand-official/src/client/home-whale/artwork.ts b/packages/client/ui-brand-official/src/client/home-whale/artwork.ts new file mode 100644 index 0000000000..b15be934b2 --- /dev/null +++ b/packages/client/ui-brand-official/src/client/home-whale/artwork.ts @@ -0,0 +1,30 @@ +/** Fixed artwork choices for the official home whale. */ + +/** Visual parameters shared by the dot and code fills. */ +export interface WhaleArtwork { + /** Maximum fill opacity, multiplied by each frame's water and highlight attenuation. */ + activeOpacity: number + /** Word size in CSS pixels. */ + fontSize: number + /** Authored artwork content, drawn verbatim as complete terms or sentences. */ + phrases: readonly string[] + /** Maximum canvas redraw frequency. */ + frameRate: number + /** One click's total lifetime in milliseconds. */ + rippleDurationMs: number +} + +/** The official artwork; these values are part of the design, not deployment settings. */ +export const WHALE_ARTWORK: WhaleArtwork = { + activeOpacity: 0.65, + fontSize: 7, + phrases: [ + '// Everything is a plugin', 'ctx.plugin(Agent)', 'await agent.run()', + 'ctx.on("turn/end", next)', 'const tools = ctx.tools', 'await session.append(event)', + 'return next()', 'ctx.effect(() => dispose)', 'const { skills } = ctx', + 'yield tool.result', 'type Context = Harness', 'MCP.connect()', + 'useSession()', 'const workspace = cwd', 'await runtime.start()', '// Creator Mode', + ], + frameRate: 24, + rippleDurationMs: 5700, +} diff --git a/packages/client/ui-brand-official/src/client/home-whale/code-fill.ts b/packages/client/ui-brand-official/src/client/home-whale/code-fill.ts new file mode 100644 index 0000000000..7b73c30525 --- /dev/null +++ b/packages/client/ui-brand-official/src/client/home-whale/code-fill.ts @@ -0,0 +1,89 @@ +/** Cached code lettering clipped by the same frames that drive the dotted whale. */ +import type { WhaleArtwork } from './artwork.ts' + +interface CodeFill { + resize(canvas: HTMLCanvasElement, width: number): void + paint(ctx: CanvasRenderingContext2D, pixels: Uint8ClampedArray, frame: number, + left: number, top: number, width: number, level: number): void + dispose(): void +} + +/** + * Allocate lettering and alpha canvases for one home attachment. + * @param options - authored code phrases and font size. + * @param atlas - bundled frame geometry; green stores opacity. + * @returns a drawing cache, or undefined when Canvas 2D is unavailable. + */ +export function createCodeFill( + options: WhaleArtwork, atlas: { width: number; height: number; columns: number; frames: number }, +): CodeFill | undefined { + const lettering = document.createElement('canvas') + const mask = document.createElement('canvas') + const ink = lettering.getContext('2d') + const matte = mask.getContext('2d') + if (ink === null || matte === null) return undefined + mask.width = atlas.width + mask.height = atlas.height + const alpha = matte.createImageData(atlas.width, atlas.height) + const stride = atlas.width * atlas.columns + + function opacity(pixels: Uint8ClampedArray, frame: number, x: number, y: number): number { + const offsetX = (frame % atlas.columns) * atlas.width + const offsetY = Math.floor(frame / atlas.columns) * atlas.height + return pixels[((offsetY + y) * stride + offsetX + x) * 4 + 1] as number + } + + return { + resize(canvas: HTMLCanvasElement, width: number): void { + const height = width * atlas.height / atlas.width + const dpr = Math.min(devicePixelRatio, 2) + lettering.width = Math.ceil(width * dpr) + lettering.height = Math.ceil(height * dpr) + ink.setTransform(dpr, 0, 0, dpr, 0, 0) + const style = getComputedStyle(canvas) + ink.fillStyle = style.getPropertyValue('--home-whale-code-ink') + ink.font = `500 ${options.fontSize}px ${style.fontFamily}` + ink.textBaseline = 'middle' + let seed = 0x9e3779b9 + const random = () => { + seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0 + return seed / 0x1_0000_0000 + } + const words: { text: string; x: number; y: number; room: number; alpha: number }[] = [] + for (let y = options.fontSize; y < height; y += options.fontSize * 1.2) { + for (let x = -random() * options.fontSize * 10; x < width;) { + const text = options.phrases[Math.floor(random() * options.phrases.length)] as string + const room = ink.measureText(text).width + words.push({ text, x, y, room, alpha: 0.65 + random() * 0.35 }) + x += room + options.fontSize * (0.35 + random() * 0.65) + } + } + for (const word of words) { + ink.globalAlpha = word.alpha + ink.fillText(word.text, word.x, word.y) + } + ink.globalAlpha = 1 + }, + + paint(ctx: CanvasRenderingContext2D, pixels: Uint8ClampedArray, frame: number, + left: number, top: number, width: number, level: number): void { + for (let y = 0; y < atlas.height; y++) { + for (let x = 0; x < atlas.width; x++) { + alpha.data[(y * atlas.width + x) * 4 + 3] = opacity(pixels, frame, x, y) + } + } + matte.putImageData(alpha, 0, 0) + const height = width * atlas.height / atlas.width + ctx.globalAlpha = level + ctx.drawImage(lettering, left, top, width, height) + ctx.globalAlpha = 1 + ctx.globalCompositeOperation = 'destination-in' + ctx.drawImage(mask, left, top, width, height) + ctx.globalCompositeOperation = 'source-over' + }, + + dispose(): void { + lettering.width = lettering.height = mask.width = mask.height = 0 + }, + } +} diff --git a/packages/client/ui-brand-official/src/client/home-whale/renderer.ts b/packages/client/ui-brand-official/src/client/home-whale/renderer.ts new file mode 100644 index 0000000000..18bc8c22d0 --- /dev/null +++ b/packages/client/ui-brand-official/src/client/home-whale/renderer.ts @@ -0,0 +1,225 @@ +/** Alternating dot and code breaches sharing one bundled animation atlas. */ +import type { WhaleArtwork } from './artwork.ts' +import { createCodeFill } from './code-fill.ts' + +// Geometry of assets/breach-atlas.webp: 137 frames at 192 × 108 cells, in twelve columns. +const ATLAS = { width: 192, height: 108, columns: 12, frames: 137 } as const + +/** Control of one attached whale decoration. */ +export interface PixelWhaleHandle { + /** Request one playback; ignored while playing, failed, hidden, or under reduced motion. */ + play: () => void + /** Release frame work, atlas pixels, and observers; late atlas loads are ignored. */ + dispose: () => void +} + +const NO_WHALE: PixelWhaleHandle = { play: () => {}, dispose: () => {} } + +/** + * Bind one noninteractive, initially transparent home decoration. + * Missing Canvas support returns a no-op handle, so home stays usable without decoration. + * An atlas failure stops decoration and logs a reload action. + * @param canvas - presentation-owned output, with its neutral color supplied by CSS. + * @param options - fixed opacity, lettering, frame rate, and duration artwork. + * @param atlasUrl - URL of the packaged frame atlas, loaded once on the first eligible playback. + * @returns the playback trigger and its teardown. + */ +export function attachPixelWhale(canvas: HTMLCanvasElement, options: WhaleArtwork, atlasUrl: string): PixelWhaleHandle { + const context = canvas.getContext('2d') + if (context === null) return NO_WHALE + const ctx = context + const image = new Image() + const media = matchMedia('(prefers-reduced-motion: reduce)') + let pixels: Uint8ClampedArray | undefined + let started: number | undefined + let loading = false + let disposed = false + let failed = false + let visible = true + let frame = 0 + let layoutFrame = 0 + let last = -Infinity + let width = 0 + let height = 0 + let sceneWidth = 0 + let left = 0 + let top = 0 + let fill: 'pixels' | 'code' = 'code' + let code: ReturnType | undefined + + const moving = () => !media.matches + const drawable = () => !disposed && !document.hidden && visible && width > 0 && height > 0 + + function clear(): void { + ctx.clearRect(0, 0, width, height) + } + + function stop(): void { + cancelAnimationFrame(frame) + frame = 0 + started = undefined + clear() + } + + function fail(error: unknown): void { + if (disposed) return + failed = true + stop() + console.warn('Home whale animation is unavailable; home remains usable. Reload the page to retry.', error) + } + + function paint(now: number): void { + clear() + if (pixels === undefined || started === undefined) return + const progress = (now - started) / options.rippleDurationMs + if (progress >= 1) { + started = undefined + return + } + const index = Math.floor(progress * ATLAS.frames) + const offsetX = (index % ATLAS.columns) * ATLAS.width + const offsetY = Math.floor(index / ATLAS.columns) * ATLAS.height + const stride = ATLAS.width * ATLAS.columns + const cell = sceneWidth / ATLAS.width + const fade = Math.min(1, progress * 15, (1 - progress) * 10) + if (fill === 'code' && code !== undefined) { + code.paint(ctx, pixels, index, left, top, sceneWidth, Math.min(1, options.activeOpacity * 1.5) * fade) + return + } + for (let y = 0; y < ATLAS.height; y++) { + for (let x = 0; x < ATLAS.width; x++) { + const offset = ((offsetY + y) * stride + offsetX + x) * 4 + const alpha = (pixels[offset + 1] as number) / 255 + if (alpha < 0.015) continue + ctx.globalAlpha = alpha * options.activeOpacity * fade * 0.8 + ctx.beginPath() + ctx.arc(left + (x + 0.5) * cell, top + (y + 0.5) * cell, (pixels[offset] as number) / 255 * cell, 0, Math.PI * 2) + ctx.fill() + } + } + ctx.globalAlpha = 1 + } + + function tick(now: number): void { + frame = 0 + if (!drawable()) return + if (!moving()) { stop(); return } + if (now - last + 0.5 >= 1000 / options.frameRate) { + paint(now) + last = now + } + if (started !== undefined) frame = requestAnimationFrame(tick) + } + + function resume(): void { + cancelAnimationFrame(frame) + frame = 0 + if (disposed) return + if (!moving()) { stop(); return } + if (!drawable()) return + paint(performance.now()) + last = performance.now() + if (started !== undefined) frame = requestAnimationFrame(tick) + } + + function resize(): void { + width = canvas.clientWidth + height = canvas.clientHeight + const dpr = Math.min(devicePixelRatio, 2) + canvas.width = Math.round(width * dpr) + canvas.height = Math.round(height * dpr) + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + ctx.fillStyle = getComputedStyle(canvas).color + sceneWidth = Math.min(Math.max(width * 0.94, 720), height * 1.6, 1120) * 1.12 + const sceneHeight = sceneWidth * ATLAS.height / ATLAS.width + left = (width - sceneWidth) / 2 + top = height * 0.43 - sceneHeight / 2 + if (code !== undefined && sceneWidth > 0) code.resize(canvas, sceneWidth) + resume() + } + + // Layout redraws all code lettering, so bursts of size or theme changes collapse into one frame. + function scheduleResize(): void { + if (disposed || layoutFrame !== 0) return + layoutFrame = requestAnimationFrame(() => { + layoutFrame = 0 + resize() + }) + } + + function begin(): void { + if (!moving() || !drawable()) return + if (fill === 'pixels') { + code ??= createCodeFill(options, ATLAS) + if (code === undefined) { fail(new Error('Home whale code Canvas context is unavailable')); return } + code.resize(canvas, sceneWidth) + } + fill = fill === 'pixels' ? 'code' : 'pixels' + started = performance.now() + resume() + } + + image.onload = () => { + if (disposed) return + const source = document.createElement('canvas') + source.width = image.naturalWidth + source.height = image.naturalHeight + try { + if (source.width !== ATLAS.width * ATLAS.columns + || source.height !== ATLAS.height * Math.ceil(ATLAS.frames / ATLAS.columns)) { + throw new Error('Home whale atlas dimensions do not match its frame geometry') + } + const reader = source.getContext('2d') + if (reader === null) { fail(new Error('Home whale atlas Canvas context is unavailable')); return } + reader.drawImage(image, 0, 0) + pixels = reader.getImageData(0, 0, source.width, source.height).data + } catch (error) { + fail(error) + return + } finally { + source.width = source.height = 0 + image.removeAttribute('src') + } + begin() + } + image.onerror = () => { fail(new Error('Home whale atlas could not be decoded')) } + + function play(): void { + if (failed || !moving() || !drawable() || started !== undefined) return + if (pixels !== undefined) { begin(); return } + if (loading) return + loading = true + image.src = atlasUrl + } + + const sizeObserver = new ResizeObserver(scheduleResize) + const visibilityObserver = new IntersectionObserver(([entry]) => { + visible = entry?.isIntersecting ?? false + resume() + }) + const themeObserver = new MutationObserver(scheduleResize) + sizeObserver.observe(canvas) + visibilityObserver.observe(canvas) + themeObserver.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme', 'style'] }) + themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-ds-theme-source', 'style'] }) + document.addEventListener('visibilitychange', resume) + media.addEventListener('change', resume) + resize() + const dispose = (): void => { + disposed = true + stop() + cancelAnimationFrame(layoutFrame) + layoutFrame = 0 + image.onload = image.onerror = null + image.removeAttribute('src') + pixels = undefined + code?.dispose() + code = undefined + sizeObserver.disconnect() + visibilityObserver.disconnect() + themeObserver.disconnect() + document.removeEventListener('visibilitychange', resume) + media.removeEventListener('change', resume) + } + return { play, dispose } +} diff --git a/packages/client/ui-brand-official/src/client/index.ts b/packages/client/ui-brand-official/src/client/index.ts index 332a5f6007..e5dc78b099 100644 --- a/packages/client/ui-brand-official/src/client/index.ts +++ b/packages/client/ui-brand-official/src/client/index.ts @@ -1,19 +1,23 @@ -/** Official DeepSeek Harness occupants for the generic browser-brand slots. */ +/** Official DeepSeek Harness occupants: the profile-gated sidebar brand slots and the home whale frame layer. */ import type { Context as ClientContext } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' +import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' +import { PixelWhale } from './home-whale/PixelWhale.tsx' import { OfficialBrandMark, OfficialBrandName } from './Brand.tsx' /** Required service: the UI slot registry. */ export const inject = ['slots'] /** - * Fill the sidebar brand slots as one declaration-aware registration set. The - * conversation hero stays on its declaring package's animated fish fallback, - * so the official build registers nothing there. + * Fill official sidebar artwork and frame the blank home with the whale decoration. * @param ctx - Client root context. */ export function apply(ctx: ClientContext): void { + ctx.slots.inject('conversation.hero.frame', () => ctx.slots.register({ + name: 'conversation.hero.frame', + id: 'pixel-whale', + }, PixelWhale)) if (process.env.DSH_CLIENT_BUILD_PROFILE !== 'official') return ctx.slots.inject('sidebar.brand.mark', () => ctx.slots.inject('sidebar.brand.name', function* () { diff --git a/packages/client/ui-brand-official/src/css-modules.d.ts b/packages/client/ui-brand-official/src/css-modules.d.ts new file mode 100644 index 0000000000..8811db1264 --- /dev/null +++ b/packages/client/ui-brand-official/src/css-modules.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: Record + export default classes +} diff --git a/packages/client/ui-brand-official/tests/browser-plugin.client.spec.tsx b/packages/client/ui-brand-official/tests/browser-plugin.client.spec.tsx index 47e32068f6..306023be8f 100644 --- a/packages/client/ui-brand-official/tests/browser-plugin.client.spec.tsx +++ b/packages/client/ui-brand-official/tests/browser-plugin.client.spec.tsx @@ -10,11 +10,14 @@ import { apply as hostApply } from '../src/index.ts' afterEach(() => { cleanup() vi.unstubAllEnvs() + vi.restoreAllMocks() + vi.unstubAllGlobals() }) const HOLES = [ 'sidebar.brand.mark', 'sidebar.brand.name', + 'conversation.hero.frame', ] as const const HERO_HOLE = 'conversation.hero.brand.mark' @@ -25,7 +28,10 @@ async function bench(declare = true) { const slots = ctx.get('slots') as SlotRegistry const declareHoles = () => slots.register({ name: 'root', - children: Object.fromEntries([...HOLES, HERO_HOLE].map(name => [name, { kind: 'single', scope: 'root' }])), + children: Object.fromEntries([...HOLES, HERO_HOLE].map(name => [name, { + kind: name === 'conversation.hero.frame' ? 'hoc' : 'single', + scope: 'root', + }])), } as never, () => null) const disposeHoles = declare ? declareHoles() : undefined return { ctx, slots, declareHoles, disposeHoles } @@ -40,11 +46,13 @@ describe('official browser-brand plugin', () => { expect(inject).toEqual(['slots']) }) - it('leaves every slot empty outside the official build profile', async () => { + it('keeps local sidebar branding and provides the same blank-home feedback', async () => { vi.stubEnv('DSH_CLIENT_BUILD_PROFILE', 'local') const subject = await bench() await subject.ctx.plugin({ inject: [...inject], apply }).await() - for (const hole of HOLES) expect(subject.slots.entries(hole)).toHaveLength(0) + expect(subject.slots.entries('sidebar.brand.mark')).toHaveLength(0) + expect(subject.slots.entries('sidebar.brand.name')).toHaveLength(0) + expect(subject.slots.entries('conversation.hero.frame')).toHaveLength(1) }) it('fills declarations before or after apply and removes every occupant on teardown', async () => { @@ -78,6 +86,21 @@ describe('official browser-brand plugin', () => { expect(subject.slots.entries(HERO_HOLE)).toHaveLength(0) }) + it('registers the whale as a self-contained frame layer without replacing the hero mark', async () => { + vi.stubEnv('DSH_CLIENT_BUILD_PROFILE', 'official') + const subject = await bench() + const fiber = subject.ctx.plugin({ inject: [...inject], apply }) + try { + await fiber.await() + const [entry] = subject.slots.entries('conversation.hero.frame') + expect(entry?.options.id).toBe('pixel-whale') + expect(entry?.inject).toBeUndefined() + expect(subject.slots.entries(HERO_HOLE)).toHaveLength(0) + } finally { + await fiber.dispose() + } + }) + it('renders the official name independently from both requested mark sizes', () => { const name = render() expect(name.container.querySelector('svg')?.getAttribute('viewBox')).toBe('26 0 156 24') diff --git a/packages/client/ui-brand-official/tests/home-whale.client.spec.tsx b/packages/client/ui-brand-official/tests/home-whale.client.spec.tsx new file mode 100644 index 0000000000..6c6b4eb427 --- /dev/null +++ b/packages/client/ui-brand-official/tests/home-whale.client.spec.tsx @@ -0,0 +1,320 @@ +// @vitest-environment jsdom +import { cleanup } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { WHALE_ARTWORK, type WhaleArtwork } from '../src/client/home-whale/artwork.ts' +import { attachPixelWhale } from '../src/client/home-whale/renderer.ts' + +const releases: (() => void)[] = [] +afterEach(() => { + for (const release of releases.splice(0)) release() + cleanup() + document.body.replaceChildren() + vi.restoreAllMocks() + vi.unstubAllGlobals() +}) + +function scene(options: WhaleArtwork = WHALE_ARTWORK, missingContext = -1) { + let now = 0 + let nextFrame = 1 + const frames = new Map() + const pixels = new Uint8ClampedArray(2304 * 1296 * 4) + for (let i = 0; i < 137; i++) { + const offset = (Math.floor(i / 12) * 108 * 2304 + (i % 12) * 192) * 4 + pixels[offset] = 90 + pixels[offset + 1] = 180 + } + const drawing = () => ({ + clearRect: vi.fn(), drawImage: vi.fn(), beginPath: vi.fn(), arc: vi.fn(), fill: vi.fn(), setTransform: vi.fn(), + fillText: vi.fn<(text: string, x: number, y: number) => void>(), + measureText: vi.fn((text: string) => ({ width: text.length * 6 }) as TextMetrics), + createImageData: vi.fn((size: number | ImageData, rows?: number) => { + const width = typeof size === 'number' ? size : size.width + const height = typeof size === 'number' ? rows ?? 0 : size.height + return { data: new Uint8ClampedArray(width * height * 4), width, height, colorSpace: 'srgb' as const } + }), + putImageData: vi.fn<(data: ImageData, x: number, y: number) => void>(), + getImageData: vi.fn(() => ({ data: pixels, width: 2304, height: 1296, colorSpace: 'srgb' as const })), + globalAlpha: 1, + }) + const contexts: ReturnType[] = [] + const canvases: HTMLCanvasElement[] = [] + const image = document.createElement('img') + Object.defineProperties(image, { naturalWidth: { value: 2304, configurable: true }, naturalHeight: { value: 1296 } }) + vi.stubGlobal('Image', function () { return image }) + const media = Object.assign(new EventTarget(), { matches: false }) + const hidden = vi.spyOn(document, 'hidden', 'get').mockReturnValue(false) + const disposers: ReturnType[] = [] + let onResize = () => {} + let onTheme = () => {} + let onVisibility: (entries: { isIntersecting: boolean }[]) => void = () => {} + vi.spyOn(performance, 'now').mockImplementation(() => now) + const getContext = vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(function (this: HTMLCanvasElement) { + const context = drawing() + const partial: Partial = context + contexts.push(context) + canvases.push(this) + return contexts.length - 1 === missingContext ? null : partial as CanvasRenderingContext2D + }) + vi.stubGlobal('devicePixelRatio', 3) + vi.stubGlobal('matchMedia', () => media) + vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { + const id = nextFrame++ + frames.set(id, callback) + return id + }) + vi.stubGlobal('cancelAnimationFrame', (id: number) => { frames.delete(id) }) + vi.stubGlobal('ResizeObserver', class { + constructor(callback: () => void) { onResize = callback; disposers.push(this.disconnect) } + observe() {} + disconnect = vi.fn() + }) + vi.stubGlobal('IntersectionObserver', class { + constructor(callback: typeof onVisibility) { onVisibility = callback; disposers.push(this.disconnect) } + observe() {} + disconnect = vi.fn() + }) + vi.stubGlobal('MutationObserver', class { + constructor(callback: () => void) { onTheme = callback; disposers.push(this.disconnect) } + observe() {} + disconnect = vi.fn() + }) + const surface = document.createElement('div') + const canvas = document.createElement('canvas') + let width = 1200 + Object.defineProperties(canvas, { + clientWidth: { get: () => width }, clientHeight: { get: () => 900 }, + }) + vi.spyOn(canvas, 'getBoundingClientRect').mockImplementation(() => new DOMRect(10, 20, width, 900)) + surface.append(canvas) + document.body.append(surface) + const whale = attachPixelWhale(canvas, options, 'data:image/webp;base64,fixture') + const dispose = whale.dispose + releases.push(dispose) + const click = whale.play + function step(time: number) { + now = time + const pending = [...frames.values()] + frames.clear() + for (const callback of pending) callback(time) + } + return { + canvas, surface, contexts, canvases, image, media, hidden, disposers, frames, + dispose, click, step, getContext, pixels, + load: () => image.dispatchEvent(new Event('load')), + theme: () => { onTheme() }, + width: (value: number) => { width = value; onResize() }, + visibility: (entries: { isIntersecting: boolean }[]) => { onVisibility(entries) }, + } +} + +describe('official home pixel whale', () => { + it('alternates dots and code on completed clicks, without interrupting or automatically repeating', () => { + const s = scene() + const output = s.contexts[0]! + expect(s.image.hasAttribute('src')).toBe(false) + expect(s.frames.size).toBe(0) + s.click() + expect(s.image.getAttribute('src')).toBe('data:image/webp;base64,fixture') + s.click() + expect(s.frames.size).toBe(0) + s.load() + expect(s.canvases[1]!.width).toBe(0) + s.step(1500) + expect(output.arc).toHaveBeenCalled() + expect(output.globalAlpha).toBe(1) + s.click() + s.step(5750) + expect(s.frames.size).toBe(0) + const draws = output.arc.mock.calls.length + s.step(6000) + expect(output.arc).toHaveBeenCalledTimes(draws) + s.click() + s.step(6100) + expect(output.arc).toHaveBeenCalledTimes(draws) + expect(output.drawImage).toHaveBeenCalled() + expect(s.contexts[2]!.fillText).toHaveBeenCalled() + expect(s.contexts).toHaveLength(4) + s.step(12000) + expect(s.frames.size).toBe(0) + s.click(); s.step(13000) + expect(output.arc.mock.calls.length).toBeGreaterThan(draws) + s.dispose() + for (const canvas of s.canvases.slice(2)) expect(canvas.width).toBe(0) + }) + + it('fills the code whale only with authored snippets', () => { + const options = { ...WHALE_ARTWORK, phrases: ['ctx.effect(() => dispose)', 'await agent.run()'] } + const s = scene(options) + for (let y = 15; y < 80; y++) { + for (let x = 20; x < 165; x++) s.pixels[((3 * 108 + y) * 2304 + x) * 4 + 1] = 180 + } + s.click(); s.load(); s.step(5800) + s.click(); s.step(7300) + const texts = s.contexts[2]!.fillText.mock.calls.map(call => call[0]) + expect(texts.length).toBeGreaterThan(0) + expect(texts.every(text => options.phrases.includes(text))).toBe(true) + const mask = s.contexts[3]!.putImageData.mock.calls.at(-1)![0].data + expect(mask[3]).toBe(180) + expect(mask[7]).toBe(0) + const count = s.contexts[2]!.fillText.mock.calls.length + s.step(7400) + expect(s.contexts[2]!.fillText).toHaveBeenCalledTimes(count) + }) + + it('plays only when its handle requests it', () => { + const s = scene() + s.surface.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: 200, clientY: 200 })) + expect(s.image.hasAttribute('src')).toBe(false) + s.click() + expect(s.image.hasAttribute('src')).toBe(true) + }) + + it('keeps a large breach in narrow homes and clips both fills within the canvas', () => { + const s = scene() + s.width(390) + s.step(16) + s.click(); s.load(); s.step(1500) + const dot = s.contexts[0]!.arc.mock.calls.at(-1)! + expect(dot[0]).toBeLessThan(0) + expect(s.canvas.width).toBe(780) + s.step(5800); s.click(); s.step(7300) + const image = s.contexts[0]!.drawImage.mock.calls.at(-1)! + expect(image[1]).toBeLessThan(0) + expect(image[3]).toBeGreaterThan(390) + expect(s.canvas.width).toBe(780) + }) + + it('stops hidden work, expires elapsed animations, and clears when reduced motion is enabled', () => { + const s = scene() + s.click(); s.load(); s.step(100) + s.hidden.mockReturnValue(true) + s.step(200) + expect(s.frames.size).toBe(0) + s.hidden.mockReturnValue(false) + document.dispatchEvent(new Event('visibilitychange')) + expect(s.frames.size).toBe(1) + s.visibility([]) + expect(s.frames.size).toBe(0) + s.visibility([{ isIntersecting: true }]) + expect(s.frames.size).toBe(1) + s.media.matches = true + s.media.dispatchEvent(new Event('change')) + expect(s.frames.size).toBe(0) + s.click() + s.media.matches = false + s.media.dispatchEvent(new Event('change')) + expect(s.frames.size).toBe(0) + s.click() + s.media.matches = true + s.step(400) + expect(s.frames.size).toBe(0) + s.media.matches = false + s.click() + s.visibility([]) + s.step(10000) + s.visibility([{ isIntersecting: true }]) + expect(s.frames.size).toBe(0) + }) + + it('does not start a late load after motion is disabled, but allows a subsequent explicit click', () => { + const s = scene() + s.click() + s.media.matches = true + s.load() + expect(s.frames.size).toBe(0) + s.media.matches = false + s.click() + expect(s.frames.size).toBe(1) + }) + + it('redraws for theme and size changes without decoding the atlas again', () => { + const s = scene() + s.click(); s.load(); s.step(100) + const draws = s.contexts[0]!.arc.mock.calls.length + s.theme() + s.theme() + expect(s.contexts[0]!.arc.mock.calls.length).toBe(draws) + s.step(150) + expect(s.contexts[0]!.arc.mock.calls.length).toBeGreaterThan(draws) + s.width(0) + s.step(200) + expect(s.frames.size).toBe(0) + s.width(390) + expect(s.canvas.width).not.toBe(780) + s.step(250) + expect(s.canvas.width).toBe(780) + expect(s.contexts).toHaveLength(2) + }) + + it('throttles to the artwork frame rate and relays out the code fill on resize', () => { + const s = scene() + s.click(); s.load(); s.step(100) + const draws = s.contexts[0]!.arc.mock.calls.length + s.step(110) + expect(s.contexts[0]!.arc).toHaveBeenCalledTimes(draws) + s.step(5800); s.click(); s.step(7300) + const lettering = s.contexts[2]! + const layouts = lettering.setTransform.mock.calls.length + s.width(900) + s.step(7320) + expect(lettering.setTransform.mock.calls.length).toBeGreaterThan(layouts) + }) + + it('stops decoration when the code fill cannot allocate its canvases', () => { + const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const s = scene(WHALE_ARTWORK, 2) + s.click(); s.load(); s.step(5800) + s.click(); s.step(7300) + expect(warning).toHaveBeenCalledOnce() + expect(s.frames.size).toBe(0) + }) + + it('cancels frames and rejects callbacks retained before disposal', () => { + const s = scene() + s.click() + const lateLoad = s.image.onload! + const lateError = s.image.onerror! + s.dispose() + lateLoad.call(s.image, new Event('load')) + lateError.call(s.image, new Event('error')) + s.theme() + s.visibility([{ isIntersecting: true }]) + s.click() + expect(s.frames.size).toBe(0) + expect(s.contexts).toHaveLength(1) + expect(s.image.hasAttribute('src')).toBe(false) + for (const disconnect of s.disposers) expect(disconnect).toHaveBeenCalledOnce() + }) + + it('leaves home usable without Canvas through an inert handle', () => { + const s = scene(WHALE_ARTWORK, 0) + s.click() + s.dispose() + expect(s.frames.size).toBe(0) + expect(s.disposers).toHaveLength(0) + expect(s.image.hasAttribute('src')).toBe(false) + }) + + it.each(['decode', 'geometry', 'context', 'pixels'] as const)('contains %s failure and does not retry until remount', (failure) => { + const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const s = scene(WHALE_ARTWORK, failure === 'context' ? 1 : -1) + s.click() + if (failure === 'decode') s.image.dispatchEvent(new Event('error')) + else { + if (failure === 'geometry') Object.defineProperty(s.image, 'naturalWidth', { value: 1 }) + if (failure === 'pixels') { + const original = s.getContext.getMockImplementation()! + vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(function (this: HTMLCanvasElement) { + const ctx = original.call(this, '2d') as CanvasRenderingContext2D + ctx.getImageData = () => { throw new Error('Unreadable atlas') } + return ctx + }) + } + s.load() + } + expect(warning).toHaveBeenCalledOnce() + s.click(); s.step(100) + expect(s.frames.size).toBe(0) + expect(s.contexts[0]!.arc).not.toHaveBeenCalled() + }) +}) diff --git a/packages/client/ui-brand-official/tests/pixel-whale.client.spec.tsx b/packages/client/ui-brand-official/tests/pixel-whale.client.spec.tsx new file mode 100644 index 0000000000..9ba1f2180a --- /dev/null +++ b/packages/client/ui-brand-official/tests/pixel-whale.client.spec.tsx @@ -0,0 +1,100 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render } from '@testing-library/react' +import { createPortal } from 'react-dom' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { WHALE_ARTWORK } from '../src/client/home-whale/artwork.ts' +import { PixelWhale } from '../src/client/home-whale/PixelWhale.tsx' +import { attachPixelWhale } from '../src/client/home-whale/renderer.ts' + +vi.mock('../src/client/home-whale/renderer.ts', () => ({ attachPixelWhale: vi.fn() })) + +const attach = vi.mocked(attachPixelWhale) + +function handle() { + const whale = { play: vi.fn(), dispose: vi.fn() } + attach.mockReturnValueOnce(whale) + return whale +} + +afterEach(() => { + vi.restoreAllMocks() + cleanup() + document.body.replaceChildren() + attach.mockReset() +}) + +describe('pixel whale frame layer', () => { + it('attaches the artwork to a canvas before its content and releases it on unmount', () => { + const whale = handle() + const view = render(

content

) + const canvas = view.container.querySelector('canvas') + expect(canvas?.getAttribute('aria-hidden')).toBe('true') + expect(canvas?.nextElementSibling?.textContent).toBe('content') + expect(attach).toHaveBeenCalledExactlyOnceWith(canvas, WHALE_ARTWORK, expect.stringContaining('breach-atlas')) + view.rerender(

updated

) + expect(attach).toHaveBeenCalledOnce() + view.unmount() + expect(whale.dispose).toHaveBeenCalledOnce() + }) + + it('wraps its content without decoration outside the blank home', () => { + const whale = handle() + const view = render(

content

) + view.rerender(

content

) + expect(view.container.querySelector('canvas')).toBeNull() + expect(view.getByText('content')).toBeTruthy() + expect(whale.dispose).toHaveBeenCalledOnce() + fireEvent.click(view.getByText('content')) + expect(whale.play).not.toHaveBeenCalled() + }) + + it('plays only for primary clicks on blank space inside its own frame', () => { + const whale = handle() + const overlay = document.createElement('div') + document.body.append(overlay) + const view = render( + +
+ +
card
+ {createPortal(overlay, overlay)} +
+
, + ) + fireEvent.click(view.getByText('control')) + fireEvent.click(view.getByText('card')) + fireEvent.click(view.getByText('overlay')) + fireEvent.click(view.getByTestId('blank'), { button: 2 }) + expect(whale.play).not.toHaveBeenCalled() + fireEvent.click(view.getByTestId('blank')) + expect(whale.play).toHaveBeenCalledOnce() + }) + + it('does not play when the click ends a text selection made inside its frame', () => { + const whale = handle() + const outside = document.createElement('p') + outside.textContent = 'elsewhere' + document.body.append(outside) + const view = render(

selectable headline

) + const text = view.getByTestId('text') + const selection = window.getSelection()! + selection.selectAllChildren(text) + fireEvent.click(text) + expect(whale.play).not.toHaveBeenCalled() + // A selection left elsewhere in the document does not block playback. + selection.selectAllChildren(outside) + fireEvent.click(text) + expect(whale.play).toHaveBeenCalledOnce() + selection.removeAllRanges() + fireEvent.click(text) + expect(whale.play).toHaveBeenCalledTimes(2) + }) + + it('plays when the document exposes no selection', () => { + const whale = handle() + vi.spyOn(window, 'getSelection').mockReturnValue(null) + const view = render(

headline

) + fireEvent.click(view.getByTestId('text')) + expect(whale.play).toHaveBeenCalledOnce() + }) +}) diff --git a/packages/client/ui-brand-official/tsconfig.json b/packages/client/ui-brand-official/tsconfig.json index 1e22d154c9..8a44ae1dc4 100644 --- a/packages/client/ui-brand-official/tsconfig.json +++ b/packages/client/ui-brand-official/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../ui-slots" + }, + { + "path": "../ui-conversation/tsconfig.client.json" } ] } diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 39e30dd70b..00011393b7 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: 04b3ff9e2bdd5eb2 - zh: 38daec55d2e75ac3 + en: c11cc27a337174f3 + zh: a971ef53fa6046a2 /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: 59cd63057dfb6eeb zh: b2e2c86e810b7bb8 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 2c2a84d885..76ed8e06ca 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -68,6 +68,8 @@ The existing semantic `DraftSnapshot` retains quote text and clipboard-coordinat ## Shell and standard props +The root-scoped `conversation.hero.frame` hoc Slot wraps the scroll body in every phase, so its layers never remount the composer when the home ends. Its owner props carry `hero`, true only for the blank home of a standalone Conversation. Layers keep the scroll body a direct flex item of the body, for example with `display: contents`. On the home, the Conversation root, which paints the background, isolates stacking: a layer decoration at negative `z-index` paints above that background and below all Conversation content, including composer takeovers. Other phases leave the root out of the stacking order, so unportaled Conversation popovers can still rise above AppFrame chrome that floats over the column. Layers own their own input handling. With no occupant, the body is unchanged. + Below 768px, Web view tabs align with the header's 20px left inset. Desktop and wider Web windows retain the additional 8px tab inset. The shared image slot props keep display choices separate from durable references: `thumbnail` requests a contained attachment-list thumbnail, while `compact` requests a cropped gallery tile. An optional per-image `label` supplies the accessible display name; loading and cache identity still use the original attachment reference. `MessageImagesContentInput` adds optional complete `MessageImageLabels` for the root-scoped `conversation.images.content` Factory. Message, tool, and Factory inputs carry the view's authorized `loadImage` callback explicitly; loading and URL caching remain with the resource owner. [ui-attachment](../ui-attachment/README.md) provides this Factory and owns rendering and the lightbox. @@ -106,7 +108,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 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)). +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 handle scrolls the first `[data-conversation-scroll]` element inside the body, which is the body's own scroll element even when frame layers wrap it. 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. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 6619be93ef..1101262976 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -68,6 +68,8 @@ Queue 预览启用空白折叠和文档边缘 trim,保留 emoji,并使用 CS ## Shell 与标准 props +root 作用域的 hoc Slot `conversation.hero.frame` 在所有阶段都包裹滚动区,因此首页结束时各层不会让输入框重新挂载。它的所有者 props 提供 `hero`,仅在独立 Conversation 的空白首页时为 true。各层需让滚动区仍是 body 的直接 flex 子项,例如使用 `display: contents`。首页阶段,绘制背景的 Conversation 根元素隔离层叠上下文:层中负 `z-index` 的装饰画在该背景之上、所有 Conversation 内容之下,包括输入区的接管面板。其他阶段根元素不参与层叠隔离,未经 portal 的 Conversation 浮层仍能盖住悬浮在中栏上方的 AppFrame 元素。各层自行处理输入。没有占用者时,body 保持原样。 + 宽度小于 768px 时,Web 的 View 标签与页头的 20px 左内边距对齐。桌面客户端和较宽的 Web 窗口保留标签额外的 8px 左内边距。 共享图片插槽属性将展示选择与持久化引用分开:`thumbnail` 请求完整缩放的附件列表缩略图,`compact` 请求裁剪的图片方块。每张图片可通过可选的 `label` 提供无障碍展示名称;加载和缓存标识仍使用原始附件引用。`MessageImagesContentInput` 为 root 作用域的 `conversation.images.content` Factory 增加可选的完整 `MessageImageLabels`。消息、工具及 Factory 输入显式携带视图提供的授权 `loadImage` 回调;读取和 URL 缓存仍由资源所有者负责。[ui-attachment](../ui-attachment/README.zh.md) 提供该 Factory,负责渲染与灯箱。 @@ -106,7 +108,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 拥有完整的底部区带,该区域不是宽度调整目标;已捕获的拖拽会在该区带和沟槽中的表格下方继续,此时指示线被它们遮住。在活跃 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))。 +主 occurrence 的活跃 transcript 只在未被内容覆盖的两侧沟槽中提供正文宽度拖拽条;嵌入式 occurrence 省略这些拖拽条。View 如果绘制进沟槽,只将具体的可见元素提到拖拽条上方;透明的全宽包装层保持在下方,不会占用空白沟槽。该规则要求此元素与 Conversation body 之间不能引入中间堆叠上下文;浏览器场景固定了交付 Chromium 的行为。Chat 将该规则用于表格元素,其限定在阅读列内的工具卡片无需提高层级。指针位于拖拽条上时,滚轮仍会滚动 transcript,Ctrl+滚轮则保留为浏览器缩放手势。拖拽条滚动的是 body 内第一个 `[data-conversation-scroll]` 元素;即使有 frame 层包在外面,它也是 body 自己的滚动区。粘滞 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))。 宽度拖拽条的指示线只在已捕获指针的拖拽期间跟随指针,普通悬停不改变其位置。 diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 3328e1e5da..aee9351ce0 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -349,6 +349,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { 'conversation.composer': { kind: 'chain', scope: 'session' }, 'conversation.composer.bar': { kind: 'single', scope: 'session-maybe' }, 'conversation.input.dock': { kind: 'list', scope: 'session' }, + 'conversation.hero.frame': { kind: 'hoc', scope: 'root' }, 'conversation.hero.brand.mark': { kind: 'single', scope: 'root' }, 'conversation.hero.workspace': { kind: 'single', scope: 'root' }, 'conversation.hero.agentPreset': { kind: 'single', scope: 'session-maybe' }, diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index 0c438e0f4c..5ce37d9ab9 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -283,6 +283,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { '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 } + /** Layers wrapping the Conversation scroll body; `hero` reports a blank, standalone home. */ + 'conversation.hero.frame': { kind: 'hoc'; scope: 'root'; owner: HeroFrameOwnerProps } /** Brand mark shown before the blank-session headline. */ 'conversation.hero.brand.mark': { kind: 'single'; scope: 'root'; owner: HeroBrandMarkOwnerProps } /** Agent-preset control staged for a New Session. */ @@ -363,6 +365,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { 'conversation.composer': { kind: 'chain'; scope: 'session' } 'conversation.composer.bar': { kind: 'single'; scope: 'session-maybe' } 'conversation.input.dock': { kind: 'list'; scope: 'session' } + 'conversation.hero.frame': { kind: 'hoc'; scope: 'root' } 'conversation.hero.brand.mark': { kind: 'single'; scope: 'root' } 'conversation.hero.workspace': { kind: 'single'; scope: 'root' } 'conversation.hero.agentPreset': { kind: 'single'; scope: 'session-maybe' } @@ -616,7 +619,13 @@ export interface ComposerChainProps { pendingInteraction: SessionPendingInteraction | undefined } -/** Presentation props supplied to the blank-session brand mark. */ +/** Conversation state exposed to scroll-body frame layers. */ +export interface HeroFrameOwnerProps { + /** Whether the body shows the blank home of a standalone, non-embedded Conversation. */ + hero: boolean +} + +/** Brand-mark presentation selected by the blank home. */ export interface HeroBrandMarkOwnerProps { /** Requested square edge in pixels. */ size: number diff --git a/packages/client/ui-conversation/src/client/index.ts b/packages/client/ui-conversation/src/client/index.ts index 7a39200c30..8a1b95e941 100644 --- a/packages/client/ui-conversation/src/client/index.ts +++ b/packages/client/ui-conversation/src/client/index.ts @@ -92,7 +92,7 @@ export type { ConversationViewActionEntry, ConversationViewActionsOwnerProps, ConversationViewsProps, ConversationWidthControlsInputProps, ConversationWidthControlsProps, ConversationStore, ConvViewOwnerProps, ConvViewProps, EmptyWorkspaceOwnerProps, - HeroAgentPresetOwnerProps, HeroBrandMarkOwnerProps, InputControlOwnerProps, InputZone, + HeroAgentPresetOwnerProps, HeroBrandMarkOwnerProps, HeroFrameOwnerProps, InputControlOwnerProps, InputZone, MessageImageLabels, MessageImageLoader, MessageImageSource, MessageImagesContentInput, MessageImagesOwnerProps, UseConversation, UserTextInput, UserTextInjected, UserTextNodeOwnerProps, UserTextNodeProps, UseConversationViews, diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationContent.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationContent.tsx index 218082ae31..4c6b8e6578 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationContent.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationContent.tsx @@ -195,10 +195,15 @@ export function ConversationContent(props: ConversationContentProps) { data-conversation-region="chat" data-content-phase={phase} > -
- {sessionId === undefined ? null : } - {composerSeat} -
+ {/* The frame wraps the scroll body in every phase, so leaving the home keeps the composer mounted. */} + {renderSlot('conversation.hero.frame', { hero: hero && props.variant !== 'embedded' }, { + children: ( +
+ {sessionId === undefined ? null : } + {composerSeat} +
+ ), + })} ) 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 dd60f4cff6..fa1c3a3722 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -7,6 +7,13 @@ background: var(--dsw-alias-bg-base); } +/* `conversation.hero.frame` contract on the home: layer decorations at negative z-index paint above this + background and below all Conversation content. Other phases keep the root out of the stacking order, so + unportaled Conversation popovers still rise above AppFrame chrome that floats over the column. */ +.root[data-phase='hero'] { + isolation: isolate; +} + /* 76px with its rule: the height of the Sidebar's tab strip and header row (ui-dockkit `.tabStrip` 38px + the pane header's 38px), so the two rules meet at the column edge. The rows add up to it — 10px top inset, the 30px diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationWidthControls.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationWidthControls.tsx index 782aacfe0a..53f42c199f 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationWidthControls.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationWidthControls.tsx @@ -106,8 +106,8 @@ function WidthHandle(props: { const body = event.currentTarget.parentElement /* v8 ignore next -- a width handle renders only inside the Conversation body. */ if (body === null) return - const scrollport = body.querySelector(':scope > [data-conversation-scroll]') - /* v8 ignore next -- the Conversation body always contains its direct scroll element. */ + const scrollport = body.querySelector('[data-conversation-scroll]') + /* v8 ignore next -- the Conversation body always contains its scroll element. */ if (scrollport === null) return if (event.ctrlKey || event.deltaY === 0) return scrollport.scrollBy({ top: wheelDeltaY(event, scrollport) }) diff --git a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx index 2cfb73ade9..28910ea625 100644 --- a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx @@ -27,14 +27,13 @@ import { ConversationMainPanel } from '../src/client/skeleton/ConversationMainPa import { ConversationSession, ConversationSessionHeader } from '../src/client/skeleton/ConversationSession.tsx' import { conversationPhase } from '../src/client/contract/snapshot.ts' import { HeroShell } from '../src/client/skeleton/EmptyHero.tsx' -import type { HeroShellProps } from '../src/client/skeleton/EmptyHero.tsx' import './control-row-dom.ts' import { InputBar } from '../src/client/skeleton/InputBar.tsx' import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx' import type { ComposerBarOwnerProps, ConversationContentInputProps, ConversationContentProps, ConversationHeaderLineageOwnerProps, ConversationSessionHeaderSlotProps, ConversationSessionSlotProps, ConversationSlotProps, - ConversationViewActionsOwnerProps, ConversationViewsProps, + ConversationViewActionsOwnerProps, ConversationViewsProps, HeroFrameOwnerProps, } from '../src/client/contract/slots.ts' import type { ViewTab } from '../src/client/contract/views.ts' @@ -201,14 +200,20 @@ function mount( /** Owner share handed to the two composer tool-row seats, per render. */ const seatOwners: { key: string; owner: unknown }[] = [] let pickerOwner: unknown + const frameOwners: HeroFrameOwnerProps[] = [] let actionsOwner: ConversationViewActionsOwnerProps | undefined const setPinned = vi.fn<(id: string, pinned: boolean) => void>() - const renderSlot = ((key: string, owner: object, opts?: { only?: string; fallback?: ReactNode }) => { + const renderSlot = ((key: string, owner: object, opts?: { only?: string; fallback?: ReactNode; children?: ReactNode }) => { slotCalls.push(key) if (key === 'conversation.input.model' || key === 'conversation.input.plan') { seatOwners.push({ key, owner }) } if (key === 'conversation.hero.workspace') { pickerOwner = owner; return null } + if (key === 'conversation.hero.frame') { + // A hoc slot with no layers renders its content unchanged. + frameOwners.push(owner as HeroFrameOwnerProps) + return
{opts?.children}
+ } if (key === 'conversation.session.header.views.actions') { actionsOwner = owner as ConversationViewActionsOwnerProps return @@ -401,6 +406,7 @@ function mount( return { view, store, wiring, sink, retargetWorkspace, session, conversation, slotCalls, lineageOwners, seatOwners, open, pickerOwner: () => pickerOwner, + frameOwner: () => frameOwners.at(-1), setPinned, actionsOwner: () => { if (actionsOwner === undefined) throw new Error('View actions not rendered') @@ -412,8 +418,8 @@ function mount( describe('Hero chrome', () => { it('renders the English preview badge through the hero locale seat', () => { - const renderSlot = vi.fn(() => null) - const view = render() + const renderSlot = vi.fn<(key: string, owner: object, opts?: { fallback?: ReactNode }) => ReactNode>(() => null) + const view = render() expect(view.getByText('Into the Unknown')).toBeTruthy() expect(view.getByText('Preview')).toBeTruthy() expect(renderSlot).toHaveBeenCalledOnce() @@ -652,6 +658,8 @@ describe('ConversationRoot resident composer', () => { it('same textarea DOM node survives the hero → active flip into the sticky scrollport', () => { const b = mount(sessionSnapshotOf({ blank: true })) + const frame = b.view.getByTestId('hero-frame') + expect(b.frameOwner()).toEqual({ hero: true }) const before = b.view.getByRole('textbox') act(() => { b.wiring.setDraft('kept across flip') }) // First message landed: content exists, phase leaves blank. Composer @@ -665,9 +673,19 @@ describe('ConversationRoot resident composer', () => { expect(b.store.store.getSnapshot().draft).toEqual({ text: 'kept across flip', references: [] }) expect(b.view.container.querySelector('[data-conversation-scroll]')?.contains(after)).toBe(true) expect(b.view.queryByTestId('hero-headline')).toBeNull() + // The frame wraps the scroll body in every phase, so leaving the home does not remount it. + expect(b.view.getByTestId('hero-frame')).toBe(frame) + expect(frame.contains(after)).toBe(true) + expect(b.frameOwner()).toEqual({ hero: false }) expect(b.view.getByTestId('view-chat')).toBeTruthy() }) + it('wraps the scroll body in the hero frame', () => { + const b = mount(sessionSnapshotOf({ blank: true })) + const scroll = b.view.container.querySelector('[data-conversation-scroll]') + expect(scroll?.parentElement).toBe(b.view.getByTestId('hero-frame')) + }) + it('keeps the Chat fallback selected by id when a view is inserted before it', () => { const viewTabs: ViewTab[] = [ { id: 'chat', label: 'Chat' }, @@ -1002,16 +1020,6 @@ describe('ConversationRoot resident composer', () => { fireEvent.wheel(handle, { deltaY: 0, deltaMode: 0 }) fireEvent.wheel(handle, { ctrlKey: true, deltaY: 120, deltaMode: 0 }) expect(scrollBy).toHaveBeenCalledTimes(calls) - - scrollport.removeAttribute('data-conversation-scroll') - const nestedScrollport = document.createElement('div') - nestedScrollport.setAttribute('data-conversation-scroll', '') - const nestedScrollBy = vi.fn() - Object.defineProperty(nestedScrollport, 'scrollBy', { value: nestedScrollBy, configurable: true }) - scrollport.append(nestedScrollport) - fireEvent.wheel(handle, { deltaY: 120, deltaMode: 0 }) - expect(scrollBy).toHaveBeenCalledTimes(calls) - expect(nestedScrollBy).not.toHaveBeenCalled() }) it('does not check capture, measure, style, or schedule width-handle moves before dragging', () => { diff --git a/packages/client/ui-renderer/src/client/scoped-slots.tsx b/packages/client/ui-renderer/src/client/scoped-slots.tsx index 89ab67fb19..ee2370be22 100644 --- a/packages/client/ui-renderer/src/client/scoped-slots.tsx +++ b/packages/client/ui-renderer/src/client/scoped-slots.tsx @@ -8,7 +8,8 @@ import { } from 'react' import { SlotOwnershipError, StaleAuthorizationError, standardHookPropName, - type ChainRenderOpts, type HostObservable, type KeyedStandardSource, type LocaleFace, type RenderFactorySlot, type RenderOpts, + type ChainRenderOpts, type HocRenderOpts, type HostObservable, type KeyedStandardSource, type LocaleFace, + type RenderFactorySlot, type RenderOpts, type ScopedStandardSourceBinding, type SessionAreaProps, type SessionProviderComponent, type SlotRenderer, type SlotEntryDef, type SlotRendererHost, type SlotScope, type SlotScopeAdapter, type SlotSpec, type StandardSourceBinding, type StoredEntry, type StoredFactory, type Translate, @@ -31,7 +32,10 @@ interface BoundSlotInject { readonly slotHookFactories?: SlotHookFactories | undefined } -type RenderSlotBinding = (key: string, owner: object, opts?: RenderOpts) => ReactNode +/** Type-erased outlet options: every kind's dispatch fields, validated by the typed render shares. */ +type OutletOpts = RenderOpts & ChainRenderOpts & Partial + +type RenderSlotBinding = (key: string, owner: object, opts?: OutletOpts) => ReactNode type RenderSlotChainBinding = (key: string, owner: object, opts?: ChainRenderOpts) => ReactNode @@ -360,7 +364,7 @@ function useLocaleRevision(face: LocaleFace | undefined): number { /** * Entry-identity React keys for entry boundaries. An outlet renders one - * winner per position (single/keyed/list cell head, chain election) through + * winner per position (single/keyed/list/hoc cell head, chain election) through * an error boundary; without a key, a boundary that failed on entry A would * survive a winner change (re-election, shadowing fallback after an * abdication, HMR re-registration) and keep a healthy entry B blacked out. @@ -392,6 +396,39 @@ function sessionGenerationKeyOf(binding: ScopedStandardSourceBinding): number { return key } +/** + * An error thrown by the content a hoc layer wraps. Layer boundaries pass it + * through untouched, so content failures never retire or blame a layer; the + * hoc outlet rethrows the original error to the boundaries above it. + */ +class HocContentError extends Error { + readonly original: unknown + constructor(original: unknown) { + super('hoc slot content failed', { cause: original }) + this.original = original + } +} + +/** Wrap the content inside one hoc layer: its errors leave as {@link HocContentError}. */ +class HocContentBoundary extends Component<{ children: ReactNode }> { + static getDerivedStateFromError(error: unknown): never { + throw error instanceof HocContentError ? error : new HocContentError(error) + } + override render(): ReactNode { + return this.props.children + } +} + +/** Surround a hoc slot's composed layers: content errors continue upward as their original error. */ +class HocOutletBoundary extends Component<{ children: ReactNode }> { + static getDerivedStateFromError(error: unknown): never { + throw error instanceof HocContentError ? error.original : error + } + override render(): ReactNode { + return this.props.children + } +} + /** * Per-entry isolation: one registrant crashing (component render or inject * factory) must not take down siblings. Assembly errors (missing providers) @@ -400,14 +437,21 @@ function sessionGenerationKeyOf(binding: ScopedStandardSourceBinding): number { * seam); for shadowing kinds the report abdicates the entry, the outlet * re-renders onto the cell's next survivor, and this boundary's crash face * only shows until that re-render lands (permanently once the cell is dry — - * the outlet then owns the crash face). + * the outlet then owns the crash face). A hoc layer supplies `crashed`, its + * inner content, which replaces the crash face so the failed layer is skipped. */ class SlotErrorBoundary extends Component< - { slotKey: string; anchor?: SlotAnchorTag; onEntryError: (error: unknown) => void; children: ReactNode }, { failed: boolean } + { + slotKey: string + anchor?: SlotAnchorTag + crashed?: ReactNode + onEntryError: (error: unknown) => void + children: ReactNode + }, { failed: boolean } > { override state = { failed: false } static getDerivedStateFromError(error: unknown): { failed: boolean } { - if (error instanceof SlotAssemblyError) throw error + if (error instanceof SlotAssemblyError || error instanceof HocContentError) throw error return { failed: true } } override componentDidCatch(error: unknown): void { @@ -416,7 +460,9 @@ class SlotErrorBoundary extends Component< } override render(): ReactNode { const Anchor = this.props.anchor ?? 'div' - if (this.state.failed) return + if (this.state.failed) { + return this.props.crashed === undefined ? : this.props.crashed + } return this.props.children } } @@ -600,7 +646,7 @@ function standardKit( * the registration and renderSlot seams already proved their contracts. */ function ContextualEntry({ - slotKey, Comp, kit, standard, injected, slotInjected, ownerProps, hookContext, hasHookContext, + slotKey, Comp, kit, standard, injected, slotInjected, ownerProps, hookContext, hasHookContext, children, }: { slotKey: string Comp: FC @@ -611,6 +657,7 @@ function ContextualEntry({ ownerProps: object hookContext: unknown hasHookContext: boolean + children?: ReactNode }) { const contextual = useMemo( () => { @@ -621,7 +668,7 @@ function ContextualEntry({ }, [hasHookContext, hookContext, slotInjected.slotHookFactories, slotKey, standard], ) - return + return {children} } function renderEntry( @@ -634,9 +681,11 @@ function renderEntry( ownerProps: object, hookContext: unknown, hasHookContext: boolean, + children: ReactNode | undefined, ): ReactNode { + // `children` is a hoc layer's inner content; other kinds pass undefined. if (slotInjected.slotHookFactories === undefined) { - return + return {children} } return ( + > + {children} + ) } -function SessionEntry({ entry, ownerProps, binding, slotKey, slotInjected, hookContext, hasHookContext }: { +/** Props shared by the scope-specific Entry Components; `children` is a hoc layer's inner content. */ +interface EntryProps { entry: StoredEntry ownerProps: object - binding: StandardSourceBinding & { readonly key: string } slotKey: string slotInjected: BoundSlotInject hookContext: unknown hasHookContext: boolean + children?: ReactNode +} + +function SessionEntry({ entry, ownerProps, binding, slotKey, slotInjected, hookContext, hasHookContext, children }: EntryProps & { + binding: StandardSourceBinding & { readonly key: string } }) { const host = useHost() const rootBinding = useRootBinding() const Comp = entry.component as FC const { kit, standard, actions } = standardKit(host, entry, 'session', rootBinding, binding) const injected = cachedSessionInject(entry, binding, actions) - return renderEntry(slotKey, Comp, kit, standard, injected, slotInjected, ownerProps, hookContext, hasHookContext) + return renderEntry(slotKey, Comp, kit, standard, injected, slotInjected, ownerProps, hookContext, hasHookContext, children) } -function SessionMaybeEntryBody({ entry, ownerProps, binding, slotKey, slotInjected, hookContext, hasHookContext }: { - entry: StoredEntry - ownerProps: object - binding: StandardSourceBinding - slotKey: string - slotInjected: BoundSlotInject - hookContext: unknown - hasHookContext: boolean -}) { +function SessionMaybeEntryBody({ + entry, ownerProps, binding, slotKey, slotInjected, hookContext, hasHookContext, children, +}: EntryProps & { binding: StandardSourceBinding }) { const host = useHost() const rootBinding = useRootBinding() const Comp = entry.component as FC const { kit, standard, actions } = standardKit(host, entry, 'session-maybe', rootBinding, binding) const injected = cachedSessionMaybeInject(entry, binding, actions) - return renderEntry(slotKey, Comp, kit, standard, injected, slotInjected, ownerProps, hookContext, hasHookContext) + return renderEntry(slotKey, Comp, kit, standard, injected, slotInjected, ownerProps, hookContext, hasHookContext, children) } /** @@ -700,14 +750,7 @@ function SessionMaybeEntryBody({ entry, ownerProps, binding, slotKey, slotInject * that must SURVIVE a switch belongs in session-bound sources (machine, * store, hooks) — the existing layering rule, now load-bearing. */ -function SessionMaybeEntry({ entry, ownerProps, slotKey, slotInjected, hookContext, hasHookContext }: { - entry: StoredEntry - ownerProps: object - slotKey: string - slotInjected: BoundSlotInject - hookContext: unknown - hasHookContext: boolean -}) { +function SessionMaybeEntry({ entry, ownerProps, slotKey, slotInjected, hookContext, hasHookContext, children }: EntryProps) { const binding = useScopeBinding() const epoch = useMaybeIncarnation(binding) return ( @@ -720,7 +763,9 @@ function SessionMaybeEntry({ entry, ownerProps, slotKey, slotInjected, hookConte slotInjected={slotInjected} hookContext={hookContext} hasHookContext={hasHookContext} - /> + > + {children} + ) } @@ -764,20 +809,13 @@ function useMaybeIncarnation(binding: StandardSourceBinding): number { return epoch } -function RootEntry({ entry, ownerProps, slotKey, slotInjected, hookContext, hasHookContext }: { - entry: StoredEntry - ownerProps: object - slotKey: string - slotInjected: BoundSlotInject - hookContext: unknown - hasHookContext: boolean -}) { +function RootEntry({ entry, ownerProps, slotKey, slotInjected, hookContext, hasHookContext, children }: EntryProps) { const host = useHost() const rootBinding = useRootBinding() const Comp = entry.component as FC const { kit, standard, actions } = standardKit(host, entry, 'root', rootBinding, undefined) const injected = cachedRootInject(entry, actions) - return renderEntry(slotKey, Comp, kit, standard, injected, slotInjected, ownerProps, hookContext, hasHookContext) + return renderEntry(slotKey, Comp, kit, standard, injected, slotInjected, ownerProps, hookContext, hasHookContext, children) } interface FactoryOccurrenceValue { @@ -1026,14 +1064,11 @@ function FactoryOutlet({ name, inputProps, slots: selected = EMPTY_FACTORY_SELEC ) } -function StrictSessionEntry({ slotKey, anchor, entry, ownerProps, slotInjected, hookContext, hasHookContext, onEntryError }: { - slotKey: string +function StrictSessionEntry({ + slotKey, anchor, crashed, entry, ownerProps, slotInjected, hookContext, hasHookContext, onEntryError, children, +}: EntryProps & { anchor: SlotAnchorTag - entry: StoredEntry - ownerProps: object - slotInjected: BoundSlotInject - hookContext: unknown - hasHookContext: boolean + crashed?: ReactNode onEntryError: (error: unknown) => void }) { const binding = useScopeBinding() @@ -1044,7 +1079,13 @@ function StrictSessionEntry({ slotKey, anchor, entry, ownerProps, slotInjected, // Per-session remount rides this key; per-entry remount rides the outer // element's entry-identity key (the outlet's guarded() call). return ( - + + > + {children} + ) } +/** Props of one hoc layer Component: the owner share and the content it wraps. */ +interface HocLayerProps { + owner: object + children: ReactNode +} + +/** Render the content inside one hoc layer: the next inner layer, or the owner's content itself at the innermost layer. */ +type RenderHocInner = (owner: object, children: ReactNode) => ReactNode + +/** The innermost step: the owner's content unchanged, with no Component of its own. */ +const renderHocContent: RenderHocInner = (_owner, children) => children + +/** + * Build one hoc layer Component. Scope, entry, and crash reporting are fixed + * here; a render passes only the owner share and the next inner layer. A + * crashed layer renders that inner content and abdicates. + * @param host - registry host owning the ledger. + * @param slotKey - the hoc slot. + * @param spec - its live declaration. + * @param entry - the layer's registration. + * @param renderInner - renders the content inside this layer. + * @returns the layer Component. + */ +function hocLayer( + host: SlotRendererHost, + slotKey: string, + spec: SlotSpec, + entry: StoredEntry, + renderInner: RenderHocInner, +): FC { + const anchor: SlotAnchorTag = spec.inline === true ? 'span' : 'div' + const slotInjected = cachedSlotInject(spec.inject) + const onEntryError = (error: unknown): void => { + host.reportEntryError(slotKey, entry, error, { abdicate: true }) + } + let Layer: FC + switch (spec.scope) { + case 'session': + Layer = ({ owner, children }) => { + const inner = {renderInner(owner, children)} + return ( + + {inner} + + ) + } + break + case 'session-maybe': + Layer = ({ owner, children }) => { + const inner = {renderInner(owner, children)} + return ( + + + {inner} + + + ) + } + break + case 'root': + Layer = ({ owner, children }) => { + const inner = {renderInner(owner, children)} + return ( + + + {inner} + + + ) + } + break + } + return Layer +} + +/** + * Compose a hoc slot's live layers, lowest `order` outermost. Winners arrive + * in ledger order (priority first), so nesting re-sorts by `order`. Each layer + * adds exactly one Component; with no layer the result renders the owner's + * content directly. + * @param host - registry host owning the ledger. + * @param slotKey - the hoc slot. + * @param spec - its live declaration. + * @returns the composed render function; its layer Components are the layer set's identity. + */ +function composeHocLayers(host: SlotRendererHost, slotKey: string, spec: SlotSpec): RenderHocInner { + const layers = [...host.entriesOfSlot(slotKey)].sort((a, b) => (a.options.order ?? 0) - (b.options.order ?? 0)) + return layers.reduceRight((renderInner, entry) => { + const Layer = hocLayer(host, slotKey, spec, entry, renderInner) + return (owner, children) => {children} + }, renderHocContent) +} + +/** Hoc dispatch: recomposes only when the slot's ledger version or declaration changes. */ +function HocOutlet({ slotKey, spec, ownerProps, children }: { + slotKey: string + spec: SlotSpec + ownerProps: object + children: ReactNode +}) { + const host = useHost() + const version = host.getVersion(slotKey) + // `version` keys the ledger read inside composeHocLayers; a new layer set yields new layer Components and remounts + // the content. + const renderLayers = useMemo(() => composeHocLayers(host, slotKey, spec), [host, slotKey, spec, version]) + if (renderLayers === renderHocContent) return children + return {renderLayers(ownerProps, children)} +} + /** * Anchor style shared by every outlet wrapper: `display:contents` keeps the * wrapper out of layout (grid/flex parents see the slot's own children), so @@ -1069,7 +1228,7 @@ const ANCHOR_STYLE = { display: 'contents' } as const function SlotOutlet({ slotKey, ownerProps, opts, declaredInline }: { slotKey: string ownerProps: object - opts?: (RenderOpts & ChainRenderOpts) | undefined + opts?: OutletOpts | undefined declaredInline: boolean }) { const host = useHost() @@ -1099,12 +1258,12 @@ function SlotOutlet({ slotKey, ownerProps, opts, declaredInline }: { ) } -/** Kind dispatch behind the outlet anchor (single/keyed/list/chain, fallbacks, crash faces). */ +/** Kind dispatch behind the outlet anchor (single/keyed/list/chain/hoc, fallbacks, crash faces). */ function renderOutletContent( host: SlotRendererHost, slotKey: string, ownerProps: object, - opts: (RenderOpts & ChainRenderOpts) | undefined, + opts: OutletOpts | undefined, scopeBinding: StandardSourceBinding, spec: SlotSpec, ): ReactNode { @@ -1115,6 +1274,9 @@ function renderOutletContent( if (spec.scope === 'session' && scopeBinding.key === undefined) { throw new SlotAssemblyError(`strict session slot '${slotKey}' rendered without a scope binding`) } + if (spec.kind === 'hoc') { + return {opts?.children ?? null} + } const entries = host.entriesOf(slotKey) const slotInjected = cachedSlotInject(spec.inject) diff --git a/packages/client/ui-renderer/tests/hoc-slots.client.spec.tsx b/packages/client/ui-renderer/tests/hoc-slots.client.spec.tsx new file mode 100644 index 0000000000..c00ee22480 --- /dev/null +++ b/packages/client/ui-renderer/tests/hoc-slots.client.spec.tsx @@ -0,0 +1,205 @@ +// @vitest-environment jsdom +/** + * Hoc slots over the real ui-slots SlotCore: nesting order, owner and + * children delivery, remount semantics, crash skipping, and every scope. + */ +import { Context } from '@deepseek-ai/cordis' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { act, cleanup, render } from '@testing-library/react' +import { Component, useEffect, useState, type ReactNode } from 'react' +import { + SlotCore, type PropsRenderSlots, type ScopedStandardSourceBinding, type SlotRendererHost, + type SlotScopeAdapter, type StandardSourceBinding, +} from '@deepseek-ai/dsh-client-ui-slots' +import { createSlotRenderer } from '../src/client/scoped-slots.tsx' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface SlotMap { + 'spec.hoc': { kind: 'hoc'; scope: 'root'; owner: { tone: string } } + 'spec.hoc.maybe': { kind: 'hoc'; scope: 'session-maybe' } + 'spec.hoc.session': { kind: 'hoc'; scope: 'session' } + } +} + +type Frame = PropsRenderSlots<'spec.hoc' | 'spec.hoc.maybe' | 'spec.hoc.session'> + +afterEach(() => { + cleanup() + vi.restoreAllMocks() +}) + +/** Passthrough host over the real core whose current scope is one live Session. */ +function hostOver(core: SlotCore): SlotRendererHost { + const rootBinding: StandardSourceBinding = { key: undefined, hooks: {}, keyedHooks: {}, props: {} } + const session = { key: 'S1', ctx: new Context(), hooks: {}, keyedHooks: {}, props: {} } as ScopedStandardSourceBinding + const source = (value: T) => ({ getSnapshot: () => value, subscribe: () => () => {} }) + const sessionSource = source(session) + const adapter: SlotScopeAdapter = { + current: sessionSource, + bindingSource: () => sessionSource, + renderArea: (_binding, { children }) => <>{children}, + } + return { + subscribe: (key, fn) => core.subscribe(key, fn), + getVersion: key => core.getVersion(key), + entriesOf: key => core.entries(key), + entriesOfSlot: key => core.entriesOfSlot(key), + reportEntryError: (key, entry, error, info) => { core.reportEntryError(key, entry, error, info) }, + reportFactoryError: () => {}, + specOf: key => core.specDynamic(key), + isLive: entry => core.isLive(entry), + storeOf: () => undefined, + factoryStoreOf: () => undefined, + retainFactoryOccurrence: () => () => {}, + subscribeFactory: () => () => {}, + getFactoryVersion: () => 0, + factoryOf: () => undefined, + isFactoryLive: () => false, + root: source(rootBinding), + scopeRevision: source(0), + scope: () => adapter, + } +} + +const flush = () => act(async () => { await Promise.resolve() }) + +const mounts = vi.fn() + +function Content({ label = 'content' }: { label?: string }) { + useEffect(() => { mounts(label) }, [label]) + return

{label}

+} + +/** A layer that wraps its children in a section carrying its id and the owner's tone. */ +function layer(id: string) { + return ({ tone, children }: { tone?: string; children: ReactNode }) => ( +
{children}
+ ) +} + +let setTone: (tone: string) => void = () => {} + +/** Register the root declaring the hoc slots, render the root-scope hoc around Content, and mount. */ +function mount(core: SlotCore, body?: (renderSlot: Frame['renderSlot']) => ReactNode) { + core.register({ + name: 'root', + children: { + 'spec.hoc': { kind: 'hoc', scope: 'root' }, + 'spec.hoc.maybe': { kind: 'hoc', scope: 'session-maybe' }, + 'spec.hoc.session': { kind: 'hoc', scope: 'session' }, + }, + }, function Root(props: Frame) { + const [tone, set] = useState('calm') + setTone = set + return <>{body?.(props.renderSlot) ?? props.renderSlot('spec.hoc', { tone }, { children: })} + } as never) + return render(<>{createSlotRenderer().renderRoot(hostOver(core), {})}) +} + +describe('hoc slots', () => { + it('renders the content unchanged without layers, then nests layers lowest order outermost', async () => { + const core = new SlotCore() + const view = mount(core) + expect(view.getByTestId('content')).toBeTruthy() + expect(view.container.querySelector('section')).toBeNull() + + core.register({ name: 'spec.hoc', id: 'inner', order: 10 }, layer('inner') as never) + core.register({ name: 'spec.hoc', id: 'outer' }, layer('outer') as never) + await flush() + const outer = view.container.querySelector('section[data-layer="outer"]') + const inner = outer?.querySelector(':scope > section[data-layer="inner"]') + expect(inner?.querySelector(':scope > p')?.textContent).toBe('content') + expect(outer?.getAttribute('data-tone')).toBe('calm') + expect(inner?.getAttribute('data-tone')).toBe('calm') + }) + + it('updates owner props without remounting the content, and remounts it when the layer set changes', async () => { + const core = new SlotCore() + mounts.mockClear() + const view = mount(core) + core.register({ name: 'spec.hoc', id: 'outer' }, layer('outer') as never) + await flush() + mounts.mockClear() + act(() => { setTone('calm') }) + expect(mounts).not.toHaveBeenCalled() + + act(() => { setTone('loud') }) + expect(view.container.querySelector('section')?.getAttribute('data-tone')).toBe('loud') + expect(mounts).not.toHaveBeenCalled() + + const dispose = core.register({ name: 'spec.hoc', id: 'inner', order: 10 }, layer('inner') as never) + await flush() + expect(mounts).toHaveBeenCalledOnce() + dispose() + await flush() + expect(view.container.querySelector('section[data-layer="inner"]')).toBeNull() + expect(view.getByTestId('content')).toBeTruthy() + }) + + it('skips a crashed layer: its content still renders and the layer leaves the slot', async () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + const core = new SlotCore() + const view = mount(core) + core.register({ name: 'spec.hoc', id: 'outer' }, layer('outer') as never) + core.register({ name: 'spec.hoc', id: 'broken', order: 10 }, (() => { throw new Error('layer failed') }) as never) + await flush() + await flush() + expect(view.container.querySelector('section[data-layer="outer"] p')?.textContent).toBe('content') + expect(view.container.querySelector('[data-slot-error]')).toBeNull() + expect(core.entriesOfSlot('spec.hoc').map(entry => entry.options.id)).toEqual(['outer']) + expect(error).toHaveBeenCalled() + }) + + it('passes content errors upward without blaming or removing any layer', async () => { + vi.spyOn(console, 'error').mockImplementation(() => {}) + const core = new SlotCore() + const reported = vi.fn() + core.onEntryError(reported) + let failing = false + function Fragile() { + if (failing) throw new Error('content failed') + return

fragile

+ } + const caught = vi.fn() + class Catch extends Component<{ children: ReactNode }, { error: unknown }> { + override state: { error: unknown } = { error: undefined } + static getDerivedStateFromError(error: unknown) { return { error } } + override componentDidCatch(error: unknown) { caught(error) } + override render() { return this.state.error === undefined ? this.props.children :

} + } + const view = mount(core, renderSlot => {renderSlot('spec.hoc', { tone: 'calm' }, { children: })}) + core.register({ name: 'spec.hoc', id: 'outer' }, layer('outer') as never) + core.register({ name: 'spec.hoc', id: 'inner', order: 10 }, layer('inner') as never) + await flush() + failing = true + act(() => { setTone('loud') }) + await flush() + expect(view.getByTestId('host-fallback')).toBeTruthy() + expect(caught).toHaveBeenCalledWith(expect.objectContaining({ message: 'content failed' })) + expect(reported).not.toHaveBeenCalled() + expect(core.entriesOfSlot('spec.hoc').map(entry => entry.options.id)).toEqual(['outer', 'inner']) + }) + + it('renders its layers around nothing when the owner omits children', async () => { + const core = new SlotCore() + const view = mount(core, renderSlot => renderSlot('spec.hoc', { tone: 'calm' })) + core.register({ name: 'spec.hoc', id: 'outer' }, layer('outer') as never) + await flush() + expect(view.container.querySelector('section[data-layer="outer"]')?.childNodes).toHaveLength(0) + }) + + it('wraps content in session-maybe and strict session layers', async () => { + const core = new SlotCore() + const view = mount(core, renderSlot => ( + <> + {renderSlot('spec.hoc.maybe', {}, { children: })} + {renderSlot('spec.hoc.session', {}, { children: })} + + )) + core.register({ name: 'spec.hoc.maybe', id: 'maybe' }, layer('maybe') as never) + core.register({ name: 'spec.hoc.session', id: 'session' }, layer('session') as never) + await flush() + expect(view.container.querySelector('section[data-layer="maybe"] p')?.textContent).toBe('maybe-content') + expect(view.container.querySelector('section[data-layer="session"] p')?.textContent).toBe('session-content') + }) +}) diff --git a/packages/client/ui-slots/README.i18n.yaml b/packages/client/ui-slots/README.i18n.yaml index 453ea30bac..c305f377d5 100644 --- a/packages/client/ui-slots/README.i18n.yaml +++ b/packages/client/ui-slots/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-slots/use-this-package: - en: 05691d44a0c6c48f - zh: 434033d5761297f3 + en: eb72211e6b97afbc + zh: d959b329c2eee0c6 /deepseek-ai-dsh-client-ui-slots/use-this-package/reusable-component-factories: en: 71cbe7aa26f3ff80 zh: 0e35265d4d95cc36 diff --git a/packages/client/ui-slots/README.md b/packages/client/ui-slots/README.md index 0698714bff..e64f58be6c 100644 --- a/packages/client/ui-slots/README.md +++ b/packages/client/ui-slots/README.md @@ -25,7 +25,7 @@ English | [中文](README.zh.md) ## Use this package -Compose UI through this package whenever you write a client plugin: register a component into a slot your parent declared, or declare child slots your component renders. The four kinds cover the composition shapes — `single` (one occupant), `list` (ordered entries), `keyed` (dispatch by a key), and `chain` (entries elect themselves). +Compose UI through this package whenever you write a client plugin: register a component into a slot your parent declared, or declare child slots your component renders. The five kinds cover the composition shapes — `single` (one occupant), `list` (ordered entries), `keyed` (dispatch by a key), `chain` (entries elect themselves), and `hoc` (entries nest around the owner's `children`). ### Reusable Component Factories diff --git a/packages/client/ui-slots/README.zh.md b/packages/client/ui-slots/README.zh.md index 365db5b428..ee59e97605 100644 --- a/packages/client/ui-slots/README.zh.md +++ b/packages/client/ui-slots/README.zh.md @@ -25,7 +25,7 @@ kind: "package-library" ## 使用本包 -编写客户端插件时都通过本包组合 UI:把组件注册进父级已声明的 slot,或声明组件将要渲染的子 slot。四种 kind 覆盖组合形态——`single`(单个占位者)、`list`(有序条目)、`keyed`(按键分派)与 `chain`(条目自行提名)。 +编写客户端插件时都通过本包组合 UI:把组件注册进父级已声明的 slot,或声明组件将要渲染的子 slot。五种 kind 覆盖组合形态——`single`(单个占位者)、`list`(有序条目)、`keyed`(按键分派)、`chain`(条目自行提名)与 `hoc`(条目层层包裹 owner 的 `children`)。 ### 可复用 Component Factory diff --git a/packages/client/ui-slots/src/index.ts b/packages/client/ui-slots/src/index.ts index 85c7d676cc..947f507dd5 100644 --- a/packages/client/ui-slots/src/index.ts +++ b/packages/client/ui-slots/src/index.ts @@ -97,8 +97,8 @@ export type PropsLocale = N extends keyof LocaleNamespaceMap & string } : object -/** Slot cardinality: single occupant, ordered list, key-dispatched, or selector-routed chain. */ -export type SlotKind = 'single' | 'list' | 'keyed' | 'chain' +/** Slot cardinality: single occupant, ordered list, key-dispatched, selector-routed chain, or nested wrapper layers. */ +export type SlotKind = 'single' | 'list' | 'keyed' | 'chain' | 'hoc' /** Slot data context: global, current-session-optional, or strict session-bound. */ export type SlotScope = 'root' | 'session-maybe' | 'session' @@ -286,6 +286,16 @@ export interface ChainRenderOpts { overlay?: boolean } +/** + * renderSlot options for a hoc slot. `children` is the owner's content; every + * live entry wraps it, and the slot renders it unchanged when no entry is live. + * Omitted content renders the layers around nothing. + */ +export interface HocRenderOpts { + /** Innermost content wrapped by the slot's entries. */ + children?: ReactNode +} + /** * Chain-entry selector: the routing decision of one chain contribution. * Runs at render time in chain order (ascending `priority`, default 0, lower @@ -303,12 +313,16 @@ export type ChainSelect = (owner: O) => M | null export type ChainKeysOf = S extends unknown ? (SlotMap[S]['kind'] extends 'chain' ? S : never) : never +/** Keys of a slot-key union whose SlotMap entry is hoc-kind (renderSlot requires `children`). */ +export type HocKeysOf = + S extends unknown ? (SlotMap[S]['kind'] extends 'hoc' ? S : never) : never + /** Keys in a render share whose dispatch occurrence requires hookContext. */ type ContextualKeysOf = S extends unknown ? (SlotMap[S] extends { hookContext: unknown } ? S : never) : never /** Keys in a render share with the ordinary optional options bag. */ -type OrdinaryKeysOf = Exclude> +type OrdinaryKeysOf = Exclude | HocKeysOf> /** * Plain and contextual child dispatch signatures. Keeping them as separate @@ -316,9 +330,12 @@ type OrdinaryKeysOf = Exclude = - ([ContextualKeysOf] extends [never] ? object : { + ([HocKeysOf] extends [never] ? object : { + >(key: K, owner: OwnerOf, opts?: HocRenderOpts): ReactNode + }) & + ([Exclude, HocKeysOf>] extends [never] ? object : { < - K extends ContextualKeysOf, + K extends Exclude, HocKeysOf>, EntryKey extends EntryKeyOf = EntryKeyOf, >( key: K, @@ -345,6 +362,14 @@ type RenderSlotFn = export type MatchedShare = E['kind'] extends 'chain' ? { matched: M } : object +/** + * Hoc children share: a hoc-slot component receives the next inner layer, or + * the owner's content at the innermost layer, as the framework-injected + * `children` prop and renders it exactly once. + */ +export type HocShare = + E['kind'] extends 'hoc' ? { children: ReactNode } : object + /** Props of the standard-kit SessionProvider seat. */ export interface SessionAreaProps { /** @@ -377,10 +402,11 @@ export type SessionProviderComponent = (props: SessionAreaProps) => ReactNode export type PropsRenderSlots = { /** * Render a declared non-chain child slot (chain keys dispatch through - * `renderSlotChain` — their routing lives in entry selectors). + * `renderSlotChain` — their routing lives in entry selectors). A hoc key + * takes the content its entries wrap as `opts.children`. * @param key - declared child key. * @param owner - owner props share for that key (decided at the render site). - * @param opts - kind dispatch options. + * @param opts - kind dispatch options; `{ children }` for hoc keys. * @returns rendered node(s). */ renderSlot: RenderSlotFn>> @@ -611,7 +637,7 @@ export type ComposedProps< M = never, N = undefined, > = PropsRuntime & PropsRenderSlots & PropsRenderFactories - & PropsStore & InjectFace & MatchedShare & PropsLocale + & PropsStore & InjectFace & MatchedShare & HocShare & PropsLocale /** * Inject factory parameter list, derived from the registration's declaration: @@ -763,7 +789,8 @@ export type SlotLabel = string | (() => string) /** * Kind shape fields carried in register options (keyed dispatch key; list - * id/order/label; chain select/priority; non-chain priority = cell shadowing rank). + * id/order/label; hoc id/order; chain select/priority; for every other kind + * priority is the cell shadowing rank, and hoc nesting follows order). */ export type KindOptions< K extends keyof SlotMap & string, @@ -788,13 +815,20 @@ export type KindOptions< /** Explicit chain position (ascending, default 0, lower tries first); ties keep registration = assembly order. */ priority?: number } - : { - /** - * Cell shadowing rank (ascending, default 0, lowest renders; a - * same-priority second registration throws — see {@link SlotCore.register}). - */ + : SlotMap[K]['kind'] extends 'hoc' ? { + id: string + /** Nesting position: ascending, default 0; the lowest order is the outermost layer. */ + order?: number + /** Cell shadowing rank (ascending, default 0, lowest renders; same id + same priority throws — see {@link SlotCore.register}). */ priority?: number } + : { + /** + * Cell shadowing rank (ascending, default 0, lowest renders; a + * same-priority second registration throws — see {@link SlotCore.register}). + */ + priority?: number + } /** * Compile-time presence check: an entry declaring children MUST consume @@ -1132,11 +1166,11 @@ export class SlotCore { * an already-declared child key throws (one declarer per slot — the message * names the first declarer); mounting one shared store handle under slots * of different scopes throws. Kind constraints: keyed — missing `key` - * throws; list — missing `id` throws; chain — missing `select` throws (the - * selector is the entry's routing seat, see {@link ChainSelect}). + * throws; list and hoc — missing `id` throws; chain — missing `select` + * throws (the selector is the entry's routing seat, see {@link ChainSelect}). * - * Shadowing (single/keyed/list): entries sharing one cell (single — the - * slot itself; keyed — same `key`; list — same `id`) coexist at distinct + * Shadowing (single/keyed/list/hoc): entries sharing one cell (single — the + * slot itself; keyed — same `key`; list and hoc — same `id`) coexist at distinct * priorities, sorted ascending with ties keeping registration order; the * cell's lowest live entry renders ({@link SlotCore.entriesOfSlot}). A * second registration at an occupied cell's exact priority (default 0) @@ -1230,11 +1264,12 @@ export class SlotCore { } break } - case 'list': { - if (options.id === undefined) throw new Error(`list slot "${options.name}" requires options.id`) + case 'list': + case 'hoc': { + if (options.id === undefined) throw new Error(`${spec.kind} slot "${options.name}" requires options.id`) const occupant = rec.entries.find(e => e.options.id === options.id && (e.options.priority ?? 0) === priority) if (occupant) { - throw new Error(`list slot "${options.name}" already has an entry with id "${options.id}" ${occupantHint(occupant)}`) + throw new Error(`${spec.kind} slot "${options.name}" already has an entry with id "${options.id}" ${occupantHint(occupant)}`) } break } @@ -1281,9 +1316,10 @@ export class SlotCore { const next = [...rec.entries, entry] // Stable sorts: priority ascending for every kind, ties keep registration // sequence — a cell's winner is its first occurrence, chain tries lower - // priority first. List refines equal priorities by explicit `order` so the - // raw ledger keeps its display sequence for priority-less compositions. - next.sort(spec.kind === 'list' + // priority first. List and hoc refine equal priorities by explicit `order` + // so the raw ledger keeps its display or nesting sequence for priority-less + // compositions. + next.sort(spec.kind === 'list' || spec.kind === 'hoc' ? (a, b) => ((a.options.priority ?? 0) - (b.options.priority ?? 0)) || ((a.options.order ?? 0) - (b.options.order ?? 0)) : (a, b) => (a.options.priority ?? 0) - (b.options.priority ?? 0)) rec.entries = next @@ -1344,8 +1380,9 @@ export class SlotCore { /** * Project a key's entries to its shadowing winners: the first live * (non-abdicated) entry of each cell in priority order — single: the slot - * is one cell; keyed: one cell per `key`; list: one cell per `id` (winners - * keep ledger sequence; list renderers still refine display by `order`). + * is one cell; keyed: one cell per `key`; list and hoc: one cell per `id` + * (winners keep ledger sequence; renderers still refine list display and hoc + * nesting by `order`). * Chain keys return the raw entries unchanged: election consumes every * entry, shadowing does not apply. The raw {@link SlotCore.entries} view * stays the inspection surface. Builds a fresh array per call — a render @@ -1363,7 +1400,7 @@ export class SlotCore { for (const entry of rec.entries) { if (this.abdicated.has(entry)) continue // Single-kind entries all share the one undefined cell. - const cell = kind === 'keyed' ? entry.options.key : kind === 'list' ? entry.options.id : undefined + const cell = kind === 'keyed' ? entry.options.key : kind === 'list' || kind === 'hoc' ? entry.options.id : undefined if (seenCells.has(cell)) continue seenCells.add(cell) heads.push(entry) @@ -1529,7 +1566,7 @@ export class SlotCore { /** * Renderer crash report from an entry boundary. Always notifies * {@link SlotCore.onEntryError} listeners; with `info.abdicate` set (the - * shadowing kinds — single/keyed/list) it first retires the entry from its + * shadowing kinds — single/keyed/list/hoc) it first retires the entry from its * cell, one-shot: the record's version bumps through the ordinary mutation * channel so outlets re-project onto the cell's next survivor, and a * repeat abdicating report no-ops entirely. Chain crashes report with diff --git a/packages/client/ui-slots/tests/core.client.spec.ts b/packages/client/ui-slots/tests/core.client.spec.ts index 6c63928f82..604b6f3804 100644 --- a/packages/client/ui-slots/tests/core.client.spec.ts +++ b/packages/client/ui-slots/tests/core.client.spec.ts @@ -11,6 +11,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { 'test.list': { kind: 'list'; scope: 'root' } 'test.keyed': { kind: 'keyed'; scope: 'session' } 'test.chain': { kind: 'chain'; scope: 'session'; owner: { tags: string[] } } + 'test.hoc': { kind: 'hoc'; scope: 'root' } 'test.grandchild': { kind: 'single'; scope: 'root' } } } @@ -36,6 +37,7 @@ function mountFrame(core: SlotCore) { 'test.list': { kind: 'list', scope: 'root' }, 'test.keyed': { kind: 'keyed', scope: 'session' }, 'test.chain': { kind: 'chain', scope: 'session' }, + 'test.hoc': { kind: 'hoc', scope: 'root' }, }, // Type-level renderSlot presence is proven by the type-chain spec; erasing // here keeps runtime fixtures terse. @@ -168,6 +170,19 @@ describe('kind semantics', () => { expect(core.entries('test.list').map(e => e.options.id)).toEqual(['a', 'b', 'c']) }) + it('hoc: missing or duplicate id throws, layers sort by priority then order, one winner per id', () => { + const core = new SlotCore() + mountFrame(core) + core.register({ name: 'test.hoc', id: 'inner', order: 10 }, Comp as never) + core.register({ name: 'test.hoc', id: 'outer' }, Comp as never) + expect(() => core.register({ name: 'test.hoc', id: 'outer' }, Comp as never)).toThrow('hoc slot "test.hoc" already has an entry with id "outer"') + // @ts-expect-error hoc registration requires options.id + expect(() => core.register({ name: 'test.hoc' }, Comp)).toThrow('hoc slot "test.hoc" requires options.id') + core.register({ name: 'test.hoc', id: 'inner', priority: -1, registrant: 'shadow' }, Comp as never) + expect(core.entries('test.hoc').map(e => e.options.id)).toEqual(['inner', 'outer', 'inner']) + expect(core.entriesOfSlot('test.hoc').map(e => e.registrant ?? e.options.id)).toEqual(['shadow', 'outer']) + }) + it('chain: missing select throws; select and priority land on the stored entry', () => { const core = new SlotCore() mountFrame(core) @@ -393,7 +408,7 @@ describe('subscription API', () => { const off = core.onMutate(key => keys.push(key)) mountFrame(core) // Contribution first, then each declared child key. - expect(keys).toEqual(['root', 'test.single', 'test.session', 'test.list', 'test.keyed', 'test.chain']) + expect(keys).toEqual(['root', 'test.single', 'test.session', 'test.list', 'test.keyed', 'test.chain', 'test.hoc']) keys.length = 0 core.register({ name: 'test.list', id: 'a' }, Comp) expect(keys).toEqual(['test.list']) diff --git a/packages/client/ui-slots/tests/type-chain.client.spec.tsx b/packages/client/ui-slots/tests/type-chain.client.spec.tsx index 3320dfcfed..7985d07915 100644 --- a/packages/client/ui-slots/tests/type-chain.client.spec.tsx +++ b/packages/client/ui-slots/tests/type-chain.client.spec.tsx @@ -28,6 +28,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { } 'chain.tools': { kind: 'keyed'; scope: 'session' } 'chain.takeover': { kind: 'chain'; scope: 'session'; owner: { items: readonly Item[] } } + 'chain.layer': { kind: 'hoc'; scope: 'root'; owner: { tone: string } } } interface LocaleNamespaceMap { @@ -213,6 +214,14 @@ describe('terminal-design type chain', () => { chainSlots.renderSlotChain('chain.takeover', { items: [] }, { fallback: null }) chainSlots.renderSlot('chain.conv', {}) + // hoc share: a layer component receives the owner props plus children; renderSlot takes optional + // { children } for hoc keys. + const Layer: SlotComponent & { children: ReactNode }> = ({ children }) => children + core.register({ name: 'chain.layer', id: 'frame', order: 1 }, Layer) + const layerSlots: PropsRenderSlots<'chain.layer' | 'chain.conv'> = null as never + layerSlots.renderSlot('chain.layer', { tone: 'calm' }, { children: null }) + layerSlots.renderSlot('chain.layer', { tone: 'calm' }) + // A parent registration declares the Slot inject once; every child // entry receives the same custom Hook, bound to official standard props // and each render occurrence's opaque context. @@ -305,6 +314,16 @@ describe('terminal-design type chain', () => { // @ts-expect-error a children set without chain keys provides no renderSlotChain type _NoChainSeat = typeof fp.renderSlotChain + // hoc keys take only { children }; other keys take no children; hoc registration requires an id. + // @ts-expect-error hoc keys accept no fallback + layerSlots.renderSlot('chain.layer', { tone: 'calm' }, { fallback: null }) + // @ts-expect-error non-hoc keys accept no children + layerSlots.renderSlot('chain.conv', {}, { children: null }) + // @ts-expect-error hoc registration requires options.id + core.register({ name: 'chain.layer' }, Layer) + // @ts-expect-error a hoc layer must accept children + core.register({ name: 'chain.layer', id: 'no-children' }, (_p => null) as SlotComponent & { children: number }>) + // renderSlot owner share typed at the call site. // @ts-expect-error owner shape mismatch (width missing) fp.renderSlot('chain.side', { collapsed: false }) diff --git a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts index c50e2ee705..00d17ca655 100644 --- a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts @@ -529,7 +529,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ComposedProps', - declaration: 'export type ComposedProps, S extends keyof SlotMap & string, H, I extends object, M = never, N = undefined> = PropsRuntime & PropsRenderSlots & PropsRenderFactories & PropsStore & InjectFace & MatchedShare & PropsLocale;', + declaration: 'export type ComposedProps, S extends keyof SlotMap & string, H, I extends object, M = never, N = undefined> = PropsRuntime & PropsRenderSlots & PropsRenderFactories & PropsStore & InjectFace & MatchedShare & HocShare & PropsLocale;', }, { name: 'ConnectionGeneration', @@ -639,6 +639,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'HandleOf', declaration: 'export type HandleOf = H extends () => infer R ? R : H;', }, + { + name: 'HocShare', + declaration: 'export type HocShare = E[\'kind\'] extends \'hoc\' ? {\n children: ReactNode;\n} : object;', + }, { name: 'HooksSources', declaration: 'export type HooksSources = Record>;', @@ -1001,7 +1005,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SlotKind', - declaration: 'export type SlotKind = \'single\' | \'list\' | \'keyed\' | \'chain\';', + declaration: 'export type SlotKind = \'single\' | \'list\' | \'keyed\' | \'chain\' | \'hoc\';', }, { name: 'SlotLabel', 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 49ac87383a..da94e32725 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -30,7 +30,7 @@ export interface ClientSlotOption { export interface ClientSlotEntry { /** SlotMap key passed as the register call's `name`. */ key: string - /** Cardinality: `single`, `list`, `keyed`, or `chain`. */ + /** Cardinality: `single`, `list`, `keyed`, `chain`, or `hoc`. */ kind: string /** Data scope: `root`, `session`, or `session-maybe`. */ scope: string @@ -56,7 +56,10 @@ export interface ClientSlotEntry { declaredBy: string /** Entries the shipped composition already registered here. */ occupants: readonly string[] - /** `shadows-shipped-ui` when registering here replaces shipped UI; `none` when additive. */ + /** + * `shadows-shipped-ui` when registering here replaces shipped UI; `wraps-owner-content` when your + * component wraps the owner content and must render `children`; `none` when additive. + */ replaceRisk: string /** A minimal browser half that registers into this slot. */ example: string @@ -853,7 +856,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:327', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:329', }, { key: 'conversation.composer.dock', @@ -914,7 +917,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:306', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:308', }, { key: 'conversation.header', @@ -1026,7 +1029,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:289', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:291', }, { key: 'conversation.hero.brand.mark', @@ -1036,7 +1039,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ doc: 'Brand mark shown before the blank-session headline.', registerOptions: [], ownerProps: [ - '/** Presentation props supplied to the blank-session brand mark. */\nexport interface HeroBrandMarkOwnerProps {\n /** Requested square edge in pixels. */\n size: number\n /** Host class preserving the surrounding mark geometry. */\n className?: string | undefined\n}', + '/** Brand-mark presentation selected by the blank home. */\nexport interface HeroBrandMarkOwnerProps {\n /** Requested square edge in pixels. */\n size: number\n /** Host class preserving the surrounding mark geometry. */\n className?: string | undefined\n}', ], ownerPropsReferences: [], standardProps: [ @@ -1058,6 +1061,53 @@ 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:289', + }, + { + key: 'conversation.hero.frame', + kind: 'hoc', + scope: 'root', + summary: 'Layers wrapping the Conversation scroll body; `hero` reports a blank, standalone home.', + doc: 'Layers wrapping the Conversation scroll body; `hero` reports a blank, standalone home.', + registerOptions: [ + { + name: 'id', + requirement: 'required', + type: 'string', + doc: 'Your layer key. A fresh id adds a layer; reusing a shipped id replaces that layer. Your component must render its `children` prop: a layer that omits it hides the owner content, such as the whole conversation and its composer.', + }, + { + name: 'order', + requirement: 'optional', + type: 'number', + doc: 'Nesting position, ascending (default 0); the lowest order is the outermost layer. Render the `children` prop exactly once.', + }, + ], + ownerProps: [ + '/** Conversation state exposed to scroll-body frame layers. */\nexport interface HeroFrameOwnerProps {\n /** Whether the body shows the blank home of a standalone, non-embedded Conversation. */\n hero: boolean\n}', + ], + ownerPropsReferences: [], + standardProps: [ + 'useRemoteAccess: (action: RemoteEndpoint) => boolean', + 'useResource: UseResource', + 'useContextMenu: UseContextMenu', + 'useWorkspaces: SnapshotSelectorHook', + 'usePanelInfo: UsePanelInfo', + 'useSessions: UseSessions', + 'useSessionStatus: UseSessionStatus', + 'useSessionRetainInfo: UseSessionRetainInfo', + 'useSessionAccess: (sessionId: SessionId, action: RemoteEndpoint) => boolean', + 'useWorkspaceAccess: (workspaceId: WorkspaceId, action: RemoteEndpoint) => boolean', + ], + keyDomain: '', + hookContext: '', + slotInject: '', + declaredBy: 'factory \'conversation.content\' (client-ui-conversation), so it exists while that definition is registered', + occupants: [ + 'client-ui-brand-official PixelWhale id \'pixel-whale\'', + ], + replaceRisk: 'wraps-owner-content', + example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.frame\', () => ctx.slots.register(\n { name: \'conversation.hero.frame\', id: \'my-layer\', order: 100 },\n ({ children }) => React.createElement(\'div\', null, children),\n ))\n },\n}', source: 'packages/client/ui-conversation/src/client/contract/slots.ts:287', }, { @@ -1118,7 +1168,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:291', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:293', }, { key: 'conversation.hero.workspace', @@ -1234,7 +1284,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.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:322', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:324', }, { key: 'conversation.input.attachments', @@ -1279,7 +1329,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:332', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:334', }, { key: 'conversation.input.dock', @@ -1348,7 +1398,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:296', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:298', }, { key: 'conversation.input.left', @@ -1406,7 +1456,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:311', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:313', }, { key: 'conversation.input.model', @@ -1450,7 +1500,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:353', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:355', }, { key: 'conversation.input.model.label', @@ -1616,7 +1666,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:301', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:303', }, { key: 'conversation.input.permission', @@ -1660,7 +1710,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:346', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:348', }, { key: 'conversation.input.plan', @@ -1703,7 +1753,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:341', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:343', }, { key: 'conversation.input.right', @@ -1761,7 +1811,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:316', + source: 'packages/client/ui-conversation/src/client/contract/slots.ts:318', }, { key: 'conversation.plan-review.actions', diff --git a/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md b/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md index 99542a4a18..ea3f3b40b1 100644 --- a/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md +++ b/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md @@ -71,7 +71,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo | `@deepseek-ai/dsh-client-ui-agent-team` | no | Web shared task board for a subagent tree | | `@deepseek-ai/dsh-client-ui-approval` | no | Approval composer takeover over the scoped Remote Event waterfall | | `@deepseek-ai/dsh-client-ui-attachment` | no | Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots | -| `@deepseek-ai/dsh-client-ui-brand-official` | no | Official DeepSeek Harness brand occupants for the Web client's sidebar slots | +| `@deepseek-ai/dsh-client-ui-brand-official` | no | Official DeepSeek Harness brand artwork and click-triggered home background | | `@deepseek-ai/dsh-client-ui-chat` | no | Chat Conversation target, node definitions, renderers, and details surface | | `@deepseek-ai/dsh-client-ui-commands` | no | Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry | | `@deepseek-ai/dsh-client-ui-context-menu` | no | Typed, plugin-extensible context menus with captured targets and shared Web presentation | diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49b2ead623..2757ab08f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3918,6 +3918,9 @@ importers: '@deepseek-ai/cordis': specifier: workspace:~ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-client-ui-conversation': + specifier: workspace:* + version: link:../ui-conversation '@deepseek-ai/dsh-client-ui-primitives': specifier: workspace:* version: link:../ui-primitives @@ -3927,6 +3930,9 @@ importers: '@deepseek-ai/dsh-client-ui-sidebar': specifier: workspace:* version: link:../ui-sidebar + '@deepseek-ai/dsh-client-ui-slots': + specifier: workspace:* + version: link:../ui-slots '@testing-library/react': specifier: ^16.3.3 version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/scripts/gen-client-catalog.spec.ts b/scripts/gen-client-catalog.spec.ts index 09cd3d1845..5e681dc106 100644 --- a/scripts/gen-client-catalog.spec.ts +++ b/scripts/gen-client-catalog.spec.ts @@ -241,13 +241,17 @@ describe('the real workspace surface', () => { expect(entries.length).toBeGreaterThan(30) for (const entry of entries) { expect(entry.summary, `${entry.key} has no summary`).not.toBe('') - expect(['single', 'list', 'keyed', 'chain']).toContain(entry.kind) + expect(['single', 'list', 'keyed', 'chain', 'hoc']).toContain(entry.kind) expect(['root', 'session', 'session-maybe']).toContain(entry.scope) } // The frame root is the canonical trap: occupied by the shipped app frame, // so a dynamic package registering there replaces the whole UI. const root = entries.find(entry => entry.key === 'root') expect(root?.replaceRisk).toBe('shadows-shipped-ui') + // A hoc layer wraps the owner content; its example must keep rendering it. + const frame = entries.find(entry => entry.key === 'conversation.hero.frame') + expect(frame?.replaceRisk).toBe('wraps-owner-content') + expect(frame?.example).toContain('({ children }) => React.createElement(\'div\', null, children)') expect(root?.occupants.join(' ')).toContain('AppFrame') expect(entries.find(entry => entry.key === 'conversation.session')?.declaredBy) .toContain("factory 'conversation.content' (client-ui-conversation)") diff --git a/scripts/gen-client-catalog.ts b/scripts/gen-client-catalog.ts index f4f7860b8e..24f65e5647 100644 --- a/scripts/gen-client-catalog.ts +++ b/scripts/gen-client-catalog.ts @@ -33,7 +33,7 @@ const OUT = 'packages/extensions/cordis-client-runner/src/client/slot-catalog.ts const SOURCE_GLOBS = ['packages/*/*/src/**/*.ts', 'packages/*/*/src/**/*.tsx'] /** Slot cardinalities the contract allows. */ -const KINDS = ['single', 'list', 'keyed', 'chain'] as const +const KINDS = ['single', 'list', 'keyed', 'chain', 'hoc'] as const /** Slot data scopes the contract allows. */ const SCOPES = ['root', 'session', 'session-maybe'] as const @@ -82,6 +82,10 @@ const REGISTER_OPTIONS: Readonly unknown | null', doc: 'Pure routing selector. Entries are tried in ascending order; the first non-null result wins and arrives as the component\'s `matched` prop. All-null falls through to the owner\'s fallback.' }, ], + hoc: [ + { name: 'id', requirement: 'required', type: 'string', doc: 'Your layer key. A fresh id adds a layer; reusing a shipped id replaces that layer. Your component must render its `children` prop: a layer that omits it hides the owner content, such as the whole conversation and its composer.' }, + { name: 'order', requirement: 'optional', type: 'number', doc: 'Nesting position, ascending (default 0); the lowest order is the outermost layer. Render the `children` prop exactly once.' }, + ], } /** The one register option a dynamic package must NOT pass, and why. */ @@ -301,9 +305,11 @@ function entryOf( ...occupant.id === undefined ? [] : [`id '${occupant.id}'`], ...occupant.entryKey === undefined ? [] : [`key '${occupant.entryKey}'`], ].join(' ')), - replaceRisk: cellOccupied && (declaration.kind === 'single' || declaration.kind === 'keyed') - ? 'shadows-shipped-ui' - : 'none', + replaceRisk: declaration.kind === 'hoc' + ? 'wraps-owner-content' + : cellOccupied && (declaration.kind === 'single' || declaration.kind === 'keyed') + ? 'shadows-shipped-ui' + : 'none', example: exampleOf(declaration), source: declaration.source, } @@ -365,7 +371,9 @@ function exampleOf(declaration: SlotDeclaration): string { ' apply(ctx) {', ` ctx.slots.inject('${declaration.key}', () => ctx.slots.register(`, ` { ${options} },`, - " () => React.createElement('div', null, 'hello'),", + declaration.kind === 'hoc' + ? " ({ children }) => React.createElement('div', null, children)," + : " () => React.createElement('div', null, 'hello'),", ' ))', ' },', '}', @@ -393,6 +401,7 @@ const KIND_EXAMPLE: Readonly> = { list: ["id: 'my-entry'", 'order: 100', "label: 'My entry'"], keyed: ["key: ''"], chain: ['select: owner => null'], + hoc: ["id: 'my-layer'", 'order: 100'], } /** Drop the `@deepseek-ai/dsh-` prefix so rows stay readable. */ @@ -476,7 +485,7 @@ export function renderClientCatalog(entries: readonly SlotEntry[]): string { 'export interface ClientSlotEntry {', ' /** SlotMap key passed as the register call\'s `name`. */', ' key: string', - ' /** Cardinality: `single`, `list`, `keyed`, or `chain`. */', + ' /** Cardinality: `single`, `list`, `keyed`, `chain`, or `hoc`. */', ' kind: string', ' /** Data scope: `root`, `session`, or `session-maybe`. */', ' scope: string', @@ -502,7 +511,10 @@ export function renderClientCatalog(entries: readonly SlotEntry[]): string { ' declaredBy: string', ' /** Entries the shipped composition already registered here. */', ' occupants: readonly string[]', - ' /** `shadows-shipped-ui` when registering here replaces shipped UI; `none` when additive. */', + ' /**', + ' * `shadows-shipped-ui` when registering here replaces shipped UI; `wraps-owner-content` when your', + ' * component wraps the owner content and must render `children`; `none` when additive.', + ' */', ' replaceRisk: string', ' /** A minimal browser half that registers into this slot. */', ' example: string', diff --git a/scripts/slot-walk.ts b/scripts/slot-walk.ts index 5e0c6c1d33..3dd152519d 100644 --- a/scripts/slot-walk.ts +++ b/scripts/slot-walk.ts @@ -24,7 +24,7 @@ const REGISTER_HEAD = /\.(?:register|registerFactory)\(/ export interface SlotDeclaration { /** SlotMap key, e.g. `settings.section`. */ key: string - /** Cardinality literal (`single` / `list` / `keyed` / `chain`), or '' when not a literal. */ + /** Cardinality literal (`single` / `list` / `keyed` / `chain` / `hoc`), or '' when not a literal. */ kind: string /** Data-scope literal (`root` / `session` / `session-maybe`), or '' when not a literal. */ scope: string diff --git a/tsconfig.host.json b/tsconfig.host.json index dfc1f56f96..cfb75ecbc1 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -8,6 +8,7 @@ "rewriteRelativeImportExtensions": false }, "include": [ + "apps/web/tests/home-whale.e2e.ts", "apps/web/tests/sidebar-drawer.e2e.ts", "apps/web/tests/creation-recommendations.e2e.ts", "apps/web/tests/plugin-recommendations.e2e.ts",