A Session row's "..." menu and its hover buttons become two list slots the WorkspaceBrowser entry declares, `sidebar.workspaces.session.menu.item` and `sidebar.workspaces.session.row.action`. The shipped actions register from apply the way a client plugin does: pin (menu 100, button 200), rename (200), fork (300), archive (menu 400, button 100). An entry receives only the row identity, reads the pin and archive sets through Set hooks its own face injects, decides its own visibility, renders `MenuItemButton` or one icon button, and dismisses the menu through the slot-level `useMenuOpenState` hook bound from the row's render occurrence. Each action's behavior lives in its injected callbacks. `UiWorkspace` gains `pinSession` and `unpinSession`; the pin write into the saved orders happens there, through the viewing-store instance the apply creates and hands the browser's registration as its handle (the ui-layout pattern), reading the memberships current at completion. The rename dialog and the row-action notice are `shell.overlay` entries fed by apply-closure observables through the faces' `hooks`; the archived notice switches the archived filter directly. WorkspaceBrowser threads no action callbacks, renders no toast, and hosts no rename dialog; its title double-click raises the same rename request. In ui-primitives, `MenuItemButton` is the plain `role="menuitem"` row for menus whose rows are components: it shares a data row's markup and styling and needs no shared state, because `Menu` decides the keyboard walk from the DOM and now runs the post-selection focus return once on the list's click bubble for every row kind. `Menu` accepts such rows as `children` beside `items`, which becomes optional together with `onSelect`; `separatorBefore` lets a row start a group with the same hairline a separator entry draws. The context-based `MenuAction` goes away with its extension group.
13 KiB
Web Client Slots
English | 中文
Slots are the Web Client's typed React composition system. dsh-client-ui-slots defines the React-free registry and type algebra; dsh-client-ui-renderer binds observable sources to hooks, renders the tree, and owns React contexts internally. A feature plugin contributes UI through ctx.slots.register() and never imports another feature plugin's component.
This page documents slot ownership, component inputs, extension APIs, and the shipped hierarchy. The surrounding boot, Remote, Client model, and Conversation paths are in Web Client architecture.
Declaration and lifecycle
SlotMap is the compile-time registry. A package declaration-merges the key, cardinality, scope, owner props, keyed props, and optional slot-level inject face. The runtime declaration is the matching children entry on the component that owns the render location.
Declaring a child has three effects: it makes the child key live, authorizes that parent entry's renderSlot or renderSlotChain call, and records the runtime dispatch specification. One live entry owns each declaration. Registering into an undeclared slot or declaring a child already owned elsewhere fails during plugin activation.
root is the only built-in declaration and the only key rendered through the Cordis service itself. ui-renderer calls ctx.slots.renderSlot('root', {}); every descendant is rendered through the renderSlot or renderSlotChain prop of the entry that declared it.
Registrations and declarations follow Cordis effect lifetimes. Disposing an entry removes its contribution and recursively collapses the child slots it declared. A feature that contributes into another package's slot therefore uses ctx.slots.inject(key, callback): the callback runs for each declaration lifetime, its effects are removed when the owner collapses, and it runs again if the owner is mounted again.
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
type HeaderActionProps = PropsRuntime<'conversation.session.header.actions'>
function HeaderAction({ useSession }: HeaderActionProps) {
const running = useSession(snapshot => snapshot.running)
return <button disabled={running}>Review</button>
}
export const inject = ['slots']
export function apply(ctx: Context): void {
ctx.slots.inject('conversation.session.header.actions', () =>
ctx.slots.register({
name: 'conversation.session.header.actions',
id: 'review',
order: 100,
}, HeaderAction))
}
Cardinality and scope
The slot declaration fixes two independent axes.
| Axis | Value | Meaning |
|---|---|---|
| cardinality | single |
One cell. The active priority winner renders. Use a child slot instead of treating this as an additive list. |
| 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. |
| 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.
Component inputs
A registered component receives inputs assembled at its binding site. Components derive these types rather than copying their members.
| Input | Declared by | Component type |
|---|---|---|
| owner values and standard scope values | the SlotMap row and installed scope adapters |
PropsRuntime<K> |
| authorized child renderers | the registration's children keys |
PropsRenderSlots<S> |
| selector hook and mutation callbacks for shared view state | the registration's store |
PropsStore<H> |
| private data, callbacks, and observable hooks | the registration's inject factory |
InjectFace<I> |
localized t function |
the registration's locale namespace |
PropsLocale<N> |
| selected chain value | the registration's select result |
matched 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.
Components never receive ctx. Parent-owned point-in-time values enter through the owner argument to renderSlot; shared view state uses a declared store; services and model objects stay in the apply closure and are projected into callbacks or observable sources.
Framework-provided hooks
The shipped adapters add these standard props. They are available according to the target slot's scope, independent of which package registered the component.
| Availability | Props | Owner |
|---|---|---|
| every scope | useSessions, useSessionStatus, useSessionRetainInfo |
ui-session |
| every scope | useWorkspaces |
ui-workspace |
| every scope | usePanelInfo |
ui-layout |
session |
sessionId, useSession, useProjection |
ui-session |
session-maybe |
optional sessionId, useSession, useProjection results |
ui-session |
session |
useConversation, useInput, inputActions |
ui-conversation |
session-maybe |
optional useConversation, useInput, inputActions results |
ui-conversation |
session |
useChat |
ui-chat |
session |
useTrajectory |
ui-trajectory |
The renderer also creates useStore from a declared store and t from a declared locale namespace. These are registration-derived props rather than global standard props.
Framework and domain-adapter owners may extend the standard set through ctx.slots.provideRoot() or ctx.uiSession.provide() together with the corresponding GlobalStandardProps, SessionStandardProps, or SessionMaybeStandardProps declaration merge. A feature component should not create a React hook prop itself or add a global standard prop for entry-private data.
Developer-provided injection
The inject option on a registration is the ordinary feature-owned injection point. Its factory runs in the plugin's apply world, may close over injected Cordis services, and returns only the data and callbacks that the component needs. For a session slot it receives sessionId; for session-maybe it receives sessionId | undefined; when a store is declared it also receives the store's bound actions.
A reserved hooks object in that return value accepts bare getSnapshot/subscribe sources. The renderer converts hooks: { status } into a useStatus(selector) component prop and caches the binding by source identity. Components do not receive the source itself and do not call useSyncExternalStore directly.
The owner of a slot may put an inject face in the child declaration when every occupant needs the same capability. Plain members reach all occupants unchanged. Function-valued members inside its hooks object are hook factories; they receive the slot's standard props and optional per-render hookContext, then return the constrained hook exposed to the occupant. conversation.chat.node uses this mechanism to provide useTurnData(key) for the node currently being rendered.
Use owner props for values already known at one render occurrence, registration inject for one entry's callbacks and private observables, slot-level inject for a capability controlled by the slot owner, and a declared store for mutable view state shared across entries or preserved across remounts. React nodes compose through child slots, not through injected values.
Current hierarchy
The hierarchy below is the shipped declaration tree. A child exists only while the named parent entry is mounted; optional feature entries can therefore make a subtree appear or disappear as one lifecycle unit.
root
├─ sidebar
│ ├─ sidebar.brand.mark
│ ├─ sidebar.brand.name
│ ├─ sidebar.panellist
│ ├─ sidebar.footer.action
│ ├─ sidebar.workspaces
│ │ ├─ sidebar.workspaces.directoryFlow
│ │ ├─ sidebar.workspaces.session.menu.item
│ │ └─ sidebar.workspaces.session.row.action
│ └─ sidebar.settings
│ ├─ settings.trigger
│ ├─ settings.header
│ ├─ settings.action
│ ├─ settings.close
│ ├─ settings.onboarding
│ └─ settings.section
│ ├─ settings.general.item
│ ├─ settings.models.provider-card
│ ├─ settings.models.footer
│ └─ settings.plugins.tab
├─ main
│ ├─ plugins.item
│ ├─ plugins.bundle.config
│ ├─ plugins.row.config
│ └─ main.conversation
│ ├─ conversation.session
│ │ └─ conversation.view
│ │ ├─ conversation.chat.node
│ │ │ ├─ conversation.chat.assistant-actions
│ │ │ ├─ conversation.chat.commandview
│ │ │ ├─ conversation.chat.turnTail
│ │ │ └─ tool.call.toolview
│ │ │ ├─ tool.call.images
│ │ │ └─ tool.view.cordis
│ │ ├─ conversation.message.images
│ │ └─ conversation.trajectory.images
│ ├─ conversation.header
│ │ ├─ conversation.header.leading
│ │ └─ conversation.session.header
│ │ ├─ conversation.session.header.lineage
│ │ ├─ conversation.session.header.actions
│ │ ├─ conversation.session.header.utilities
│ │ └─ conversation.session.header.corner
│ ├─ conversation.composer
│ │ ├─ conversation.approval.detail
│ │ └─ conversation.plan-review.actions
│ ├─ conversation.composer.bar
│ │ ├─ conversation.input.attachments
│ │ ├─ conversation.input.permission
│ │ ├─ conversation.input.plan
│ │ └─ conversation.input.model
│ ├─ conversation.input.overlay
│ ├─ conversation.input.dock
│ ├─ conversation.composer.dock
│ ├─ conversation.input.left
│ ├─ conversation.input.right
│ ├─ conversation.hero.brand.mark
│ ├─ conversation.hero.workspace
│ │ └─ conversation.hero.workspace.directoryFlow
│ └─ conversation.hero.agentPreset
├─ rightbar
│ └─ rightbar.session
│ ├─ sidebar.right.pane.tab
│ │ ├─ sidebar.right.tab.guide
│ │ └─ sidebar.right.tab.guide.entry
│ ├─ sidebar.right.pane.tab.title
│ └─ sidebar.right.tab.menu.item
├─ shell.leading
└─ shell.overlay
The generated Client inspect catalog is the exhaustive contract for each key: cardinality, scope, owner props, standard props, current occupants, declaration owner, and replacement risk. A running dynamic package can query the live tree and an exact key with cordis_inspect what:"client"; the source catalog is generated from SlotMap declarations and slots.register() call sites by pnpm run gen-client-catalog.
Extension rules
- Import another feature package only for declarations with
import type; never import or re-export its runtime values. - Declare a new child slot only in the component that owns and renders that location. Other packages wait with
ctx.slots.inject()and contribute throughctx.slots.register(). - 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
hookscompartment is the sole exception for bare observables; React content travels through slots. - Treat
singleand an occupied keyed cell as replacement points. Use list ids or an unoccupied key for additive extensions.