Files
deepseek-harness/docs/subsystems/slots.md
T

12 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 Follows current selection but stays renderable without a Session; Session values are optional.
scope session Requires a resolved Session 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 strict Session child. It binds that subtree to the current Session identity and remounts the body when the identity changes.

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, useSessionPendingInteraction ui-session
every scope useWorkspaces ui-workspace
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.footer.action
│  ├─ sidebar.workspaces
│  │  └─ sidebar.workspaces.directoryFlow
│  └─ sidebar.settings
│     ├─ settings.trigger
│     ├─ settings.header
│     ├─ settings.action
│     ├─ settings.close
│     ├─ settings.onboarding
│     └─ settings.section
│        ├─ settings.general.item
│        └─ settings.plugins.tab
│           └─ settings.plugin.item
├─ conversation
│  ├─ conversation.session
│  │  └─ conversation.view
│  │     ├─ conversation.chat.node
│  │     │  ├─ conversation.chat.assistant-actions
│  │     │  ├─ conversation.chat.commandview
│  │     │  ├─ conversation.chat.turnTail
│  │     │  └─ tool.call.toolview
│  │     │     └─ tool.view.cordis
│  │     └─ conversation.message.images
│  ├─ conversation.session.header
│  │  ├─ conversation.session.header.lineage
│  │  ├─ conversation.session.header.actions
│  │  └─ conversation.session.header.utilities
│  ├─ conversation.composer
│  │  └─ conversation.approval.detail
│  ├─ conversation.composer.bar
│  │  ├─ conversation.input.attachments
│  │  ├─ 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
├─ details
│  └─ conversation.details.tool
└─ 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 through ctx.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 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.