Skip to content

Large parts of the browser are built from ~30 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 ui-* modules

PackageJob
ui-slotsThe pure extension-point core (SlotCore) — no React, no Cordis
ui-primitivesShared building blocks (markdown, math, highlight) used by widgets
ui-themeTheme registry (--dsw-* tokens), Appearance settings row
ui-layoutThe root AppFrame: sidebar+conversation+details columns, shell.overlay, theme presenter
ui-sidebarWordmark, New Session, collapse rail, Workspace/Settings seats
ui-workspaceWorkspaceBrowser in the sidebar + WorkspacePicker in the hero
ui-conversationSkeleton, chat view, composer/input docks, details shell, header actions
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
ui-tool (trajectory) → 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-settingsSettings domain base: ctx.settingsScope + 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 (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/runtime/src/client/slots.ts) adds the store-instance axis (per session), install(createSlotRenderer()) (boot-once), installLocale(localeFace), and routes registration disposal through the caller's fiber.

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. The sidebar occupant receives { collapsed, width } from the concession solve; the conversation occupant is session-maybe (keeps identity across session switches); the details column is strict-session and reports live width. shell.overlay is the additive, frame-wide float seat (badges, toasts) — the recommended place to float a surface over the whole app instead of fighting the single root/sidebar/conversation seats.

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
conversation.session.header.actionsui-conversationui-subagent (catalog), ui-jobs (jobs list)
conversation.input.dockui-conversationui-goal (GoalBar), queue rows
conversation.input.plan, conversation.input.modelui-conversationui-plan, ui-model-selection
conversation.chat.nodeui-conversation (keyed)ui-tool, ui-workflow-run
conversation.chat.turnTailui-conversationui-deliverables
conversation.chat.assistant-actionsui-conversation (list)ui-message-feedback (feedback)
conversation.composerui-conversation (chain)ui-user-questions (question)
conversation.viewui-conversation (list)ui-trajectory
conversation.hero.workspace, .directoryFlowui-conversationui-workspace, ui-directory-picker-*
sidebar.workspaces, sidebar.workspaces.directoryFlowui-sidebarui-workspace, ui-directory-picker-*
settings.trigger/.header/.action/.close/.section/.plugins.tab/.onboardingui-settingsui-settings-general chrome/sections
settings.general.itemui-settings-generalui-locale (Language), ui-theme (Appearance), ui-permission-presets, ui-agent-preset
tool.call.toolviewui-tool (keyed)business Tool views

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 (pruneStoreScope) 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-tool, ui-deliverables, ui-workspace, ui-settings*, ui-goal, ui-plan, ui-message-feedback, ui-model-selection, …). 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: Schema form — the settings editors that ui-settings owns.
  • Frontend: LocalizationLocaleNamespaceMap merges and the t seat.
  • packages/client/ui-slots/src/index.tsSlotCore, SlotKind, SlotScope, ComposedProps.
  • packages/client/ui-layout/src/client/AppFrame.tsx— the three-column frame — and packages/bundle/web-app/cordis.patch.yml (the ui-module roster).