Skip to content

Large parts of the browser are built from 38 ui-* modules, each a Cordis plugin that paints a slice of the surface. They are not wired by editing a central layout; each one registers a component into a named slot — an extension point declared by the owner that declares it. Composition lives in cordis.patch.yml (the web-app bundle), and the render tree hangs off one root slot.

The module pattern ​

Every ui-* package ships a browser half (./client) with the same shape:

  1. Merge SlotMap (and LocaleNamespaceMap) to declare the slot contract it renders or owns.
  2. ctx.slots.register({ name, children?, store?, locale?, inject? }, Component) — one call contributes the component and (optionally) declares child slots, a shared/exclusive store, a locale namespace, and a business inject face.
  3. Register/declare happens inside ctx.effect, so unload cascades automatically.

The framework composes each component's props from four shares: the runtime share (owner props + standard kit — useSession, useSessions, useWorkspaces, sessionId, useProjection), the render share (renderSlot/renderSlotChain/SessionProvider), the store share (useStore/actions), and the inject face (business hooks bound to your <inject> factory). See packages/client/ui-slots/src/index.ts.

SlotKind is 'single' | 'list' | 'keyed' | 'chain'; SlotScope is 'root' | 'session-maybe' | 'session'. Declaring is claiming: only the entry that declares a child key may render it.

All 38 ui-* modules ​

PackageJob
ui-slotsThe pure extension-point core (SlotCore) — no React, no Cordis
ui-rendererThe runtime registry: SlotRegistry (a Cordis Service, ctx.slots), React slot bindings, ctx.uiRenderer.mount
ui-primitivesShared building blocks (markdown, math, highlight) used by widgets
ui-themeTheme registry (--dsw-* tokens), Appearance + FontSize settings rows
ui-layoutThe root AppFrame: sidebar+conversation+details columns, shell.overlay, theme presenter, ctx.layout panel service
ui-sidebarThe navigation shell: brand row (sidebar.brand.mark/sidebar.brand.name), New Session, collapse rail, the browsing region that hosts the session tree, bottom-pinned sidebar.settings/sidebar.footer.action
ui-workspaceThe Workspace/Session browser in the sidebar + WorkspacePicker in the hero — grouped or flat rows, search, state dots, fork/archive
ui-sessionSession Controller adapter for React: useSession/useSessions hooks and session-scoped slot data
ui-conversationAssembly seats: chat skeleton, composer/input docks, details shell, header actions (the seats ui-chat and friends occupy)
ui-chatThe Chat target: owns conversation.chat.node (keyed) and conversation.message.images, node definitions, details, scroll state
ui-input-trigger/ and @ detection, candidate menu, ctx.inputTriggers source roster
ui-commandsctx.commandUi: command-directory cache and 3-kind dispatch
ui-skill/-triggered skill invocation source
ui-subagentSubagent catalog header action, composer chain, @ reference source
ui-jobsBackground-job list header action over jobsBySession
ui-goalGoalBar in the input dock over the goal projection
ui-planPlan-mode status chip in conversation.input.plan
ui-toolTool-call presentation, keyed dispatch + generic fallback (tool.call.toolview), tool details in conversation.details.tool
ui-trajectoryTurn-aware event ledger, a conversation.view tab
ui-workflow-runDurable workflow runs as Chat nodes
ui-deliverablesProduced-files row + inline-file links
ui-message-feedbackLike/Dislike + note in the assistant-actions strip
ui-model-selection/model popupSelect + composer model seat
ui-permission-presetsPermission defaults row w/ risk acknowledgment
ui-agent-presetAgent-preset selection + roster manager
ui-user-questionsRenders user questions via conversation.composer
ui-approvalApproval composer takeover over the scoped Remote Event waterfall (conversation.approval.detail); zero runtime dependencies
ui-referenceUnified Web @file/@session reference source for the composer
ui-scheduleRead-only active Schedule catalog in the Session header
ui-settingsSettings domain base: ctx.settingsScope + settings schema service + slot contract
ui-settings-generalThe Settings shell, General section, chrome
ui-settings-modelsDeepSeek / pi-ai provider + model editors
ui-settings-pluginsHost-plane plugin configuration cards
ui-settings-plugin-inventoryRead-only plugin-inventory tab
ui-directory-picker-browseIn-app browse directory picker
ui-directory-picker-nativeRenderless native OS picker driver
ui-attachmentAttachment rendering primitives — composed in the web-app bundle, no longer a platform seed module

(Plus ui-cordis, the Cordis dynamic-plugin definition card — a regular row in the web-app cordis.patch.yml, not a dev-only surface.)

ui-slots: the extension points ​

SlotCore (packages/client/ui-slots/src/index.ts) is framework-free and declares a built-in root seat at construction:

ts
constructor() {
  const root = this.record('root')
  root.spec = { kind: 'single', scope: 'root' }
  root.declaredBy = '(built-in)'
  root.declarationEpoch = 1
}

Registration semantics worth knowing:

  • Registering into an undeclared slot throws; declaring an already-declared child throws.
  • Shadowing: single/keyed/list cells sort ascending by priority (default 0); the lowest live entry in a cell renders, so priority 0 is the historical one-occupant-per-cell fail-loud.
  • Disposal collapses every declared child recursively — one lifecycle axis.
  • onMutate/onEntryError bridge into ctx.emit and crash supervision; snapshot() exports a JSON-safe declaration tree.

The runtime SlotRegistry (a Cordis Service, packages/client/ui-renderer/src/client/registry.ts, ctor at line ~95) adds the store-instance axis (per scope), install(createSlotRenderer()) (boot-once), installLocale(localeFace), installScope, and routes registration disposal through the caller's fiber. Store instances are pruned with their scope owner on scope death.

ui-layout: composing the shell ​

ui-layout contributes AppFrame into the runtime root slot via one register call (packages/client/ui-layout/src/client/index.ts), and in the same breath declares its four child slots:

ts
declare module '@deepseek-ai/dsh-client-ui-slots' {
  interface SlotMap {
    'root':      { kind: 'single'; scope: 'root' }
    'sidebar':   { kind: 'single'; scope: 'root' }
    'conversation': { kind: 'single'; scope: 'session-maybe' }
    'details':   { kind: 'single'; scope: 'session' }
    'shell.overlay': { kind: 'list'; scope: 'root' }
  }
}

AppFrame.tsx renders a three-column grid with drag handles (pointer capture + rAF throttle), a concession solver over the panel widths, and renders each child slot at a fixed tree position. Owner props are gone: the details occupant (ui-conversation's DetailsPanel) no longer receives column geometry from the frame — the framework injects the session id and hooks for the session scope, and the new ctx.layout panel service (packages/client/ui-layout/src/client/service.ts) owns whether the column is open. ctx.layout is a small cross-plugin face — toggleSidebar(), openDetails(), closeDetails() — backed by the root entry's layout store actions (attachPanels wires them once the root mounts), so a widget in any plugin can drive panel transitions without reaching into the frame. shell.overlay remains the additive, frame-wide float seat (badges, toasts).

Named seats across the surface ​

Real slot names collected from the module READMEs and source merges:

SeatDeclared byOccupied by
rootruntime (built-in)ui-layout AppFrame
sidebar, conversation, details, shell.overlayui-layoutui-sidebar, ui-conversation, (anonymous entry), overlay list
sidebar.brand.mark, sidebar.brand.nameui-sidebar (single)ui-brand-official (official builds only)
sidebar.workspaces, sidebar.workspaces.directoryFlowui-sidebarui-workspace, ui-directory-picker-*
sidebar.settingsui-sidebar (single)ui-settings-general (Settings trigger + panel)
sidebar.footer.actionui-sidebar (list)feature footer actions
conversation.session, conversation.session.headerui-conversation (assembly)composed row
conversation.session.header.lineage, .utilitiesui-conversationfeature header cells (fork ancestry, utilities)
conversation.session.header.actionsui-conversationui-subagent (catalog), ui-jobs (jobs list)
conversation.viewui-conversation (list)ui-chat (id: 'chat'), ui-trajectory
conversation.chat.nodeui-chat (keyed, scope: session)ui-tool, ui-workflow-run, ui-deliverables rows
conversation.chat.commandviewui-chat (keyed)command rows
conversation.message.imagesui-chat (single)shipped gallery (ui-chat itself)
conversation.chat.turnTailui-chat (chain)ui-deliverables
conversation.chat.assistant-actionsui-chat (list)ui-message-feedback (feedback)
conversation.details.toolui-chat (single)ui-tool (ToolDetails)
conversation.approval.detailui-approval (single)ui-approval approval composer
conversation.composerui-conversation (chain)ui-user-questions (question)
conversation.composer.barui-conversation (single, session-maybe)composer bar
conversation.composer.dockui-conversation (list)input zone rows
conversation.input.left, .right, .overlayui-conversation (list)feature input cells
conversation.input.attachmentsui-conversationattachment preview
conversation.input.dockui-conversationui-goal (GoalBar), queue rows
conversation.input.plan, conversation.input.modelui-conversationui-plan, ui-model-selection
conversation.hero.workspace, .directoryFlowui-conversationui-workspace, ui-directory-picker-*
conversation.hero.brand.mark, conversation.hero.agentPresetui-conversation (root)brand mark occupant, ui-agent-preset
settings.trigger/.header/.action/.close/.section/.plugins.tab/.onboardingui-settingsui-settings-general chrome/sections
settings.general.itemui-settingsui-theme (Appearance + FontSize), locale (Language), ui-permission-presets, ui-agent-preset
tool.call.toolviewui-tool (keyed)business Tool views

Two ownership changes worth calling out: the chat node and image gallery seats (conversation.chat.node, conversation.message.images) are declared and registered by ui-chat — ui-conversation keeps only the assembly seats (conversation.session.*, conversation.view, composer, hero.*, input.*) that frame where those occupants render.

SlotCore also gives supervisors one seam: reportEntryError can abdicate a crashing entry from its cell (the renderer retires it, so the slot falls back to the next survivor) while the registration itself stays on the ledger until its disposer runs. Chain kinds never abdicate — election alternatives resolve at select time. snapshot() projects the live declaration tree JSON-safely (registrant, cell, priority, active) so a status surface can mirror contribution health without importing any component.

Lifecycle discipline for module authors ​

Three rules keep ui-module composition safe:

  1. Register inside ctx.effect (or the slots.register Service wrapper, which routes disposal through the caller's fiber). Unload then cascades registration + declarations + store mounts automatically.
  2. One declarer per slot. Declaring a child key that another entry already declared throws naming the first declarer — the exclusive-render-authority guarantee that makes renderSlot safe to hand down through props.
  3. Store ownership is per scope. The SlotRegistry instance axis maps handle × scope-key; a session-scoped store gets one instance per session and is pruned on scope death, dropping persisted state with it. Root-scoped records are untouched by session teardown.

Following these, a module can be mounted or hot-reloaded (dsh-client-hmr) without any central registry knowing it exists — composition just works because every seat is discoverable in SlotMap and every widget is a disposable contribution.

Composition lives in the bundle patch ​

No ui-module is hard-wired into another by code. The roster that ships the browser surface lives in packages/bundle/web-app/cordis.patch.yml as dsh.client rows (ui-theme, ui-layout, ui-sidebar, ui-conversation, ui-chat, ui-tool, ui-deliverables, ui-workspace, ui-settings*, ui-goal, ui-plan, ui-message-feedback, ui-model-selection, ui-approval, ui-reference, ui-schedule, …). Deleting a row from the patch removes that feature's whole contribution — views, slots, and copy together — because nothing else in the shell imports it. This is the same "row-overlay" mechanism the host uses to disable base agent-plane rows behind agent presets; the browser half just rides the same graph.

Version table (this page's packages) ​

Further reading ​

  • Frontend: Client runtime and wire — what ctx.slots, ctx.sessions, ctx.workspaces sit on.
  • Frontend: The web frontend — which ui modules the web-app bundle mounts and how the shell renders root.
  • Frontend: Settings Schema & Forms — the settings model layer ui-settings owns.
  • Frontend: Localization — LocaleNamespaceMap merges and the t seat.
  • packages/client/ui-slots/src/index.ts — SlotCore, SlotKind, SlotScope, ComposedProps.
  • packages/client/ui-renderer/src/client/registry.ts — SlotRegistry (the runtime service) and packages/client/ui-layout/src/client/AppFrame.tsx (the three-column frame).
  • packages/bundle/web-app/cordis.patch.yml — the ui-module roster.