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:
- Merge
SlotMap(andLocaleNamespaceMap) to declare the slot contract it renders or owns. 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.- 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
| Package | Job |
|---|---|
ui-slots | The pure extension-point core (SlotCore) — no React, no Cordis |
ui-renderer | The runtime registry: SlotRegistry (a Cordis Service, ctx.slots), React slot bindings, ctx.uiRenderer.mount |
ui-primitives | Shared building blocks (markdown, math, highlight) used by widgets |
ui-theme | Theme registry (--dsw-* tokens), Appearance + FontSize settings rows |
ui-layout | The root AppFrame: sidebar+conversation+details columns, shell.overlay, theme presenter, ctx.layout panel service |
ui-sidebar | The 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-workspace | The Workspace/Session browser in the sidebar + WorkspacePicker in the hero — grouped or flat rows, search, state dots, fork/archive |
ui-session | Session Controller adapter for React: useSession/useSessions hooks and session-scoped slot data |
ui-conversation | Assembly seats: chat skeleton, composer/input docks, details shell, header actions (the seats ui-chat and friends occupy) |
ui-chat | The 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-commands | ctx.commandUi: command-directory cache and 3-kind dispatch |
ui-skill | /-triggered skill invocation source |
ui-subagent | Subagent catalog header action, composer chain, @ reference source |
ui-jobs | Background-job list header action over jobsBySession |
ui-goal | GoalBar in the input dock over the goal projection |
ui-plan | Plan-mode status chip in conversation.input.plan |
ui-tool | Tool-call presentation, keyed dispatch + generic fallback (tool.call.toolview), tool details in conversation.details.tool |
ui-trajectory | Turn-aware event ledger, a conversation.view tab |
ui-workflow-run | Durable workflow runs as Chat nodes |
ui-deliverables | Produced-files row + inline-file links |
ui-message-feedback | Like/Dislike + note in the assistant-actions strip |
ui-model-selection | /model popupSelect + composer model seat |
ui-permission-presets | Permission defaults row w/ risk acknowledgment |
ui-agent-preset | Agent-preset selection + roster manager |
ui-user-questions | Renders user questions via conversation.composer |
ui-approval | Approval composer takeover over the scoped Remote Event waterfall (conversation.approval.detail); zero runtime dependencies |
ui-reference | Unified Web @file/@session reference source for the composer |
ui-schedule | Read-only active Schedule catalog in the Session header |
ui-settings | Settings domain base: ctx.settingsScope + settings schema service + slot contract |
ui-settings-general | The Settings shell, General section, chrome |
ui-settings-models | DeepSeek / pi-ai provider + model editors |
ui-settings-plugins | Host-plane plugin configuration cards |
ui-settings-plugin-inventory | Read-only plugin-inventory tab |
ui-directory-picker-browse | In-app browse directory picker |
ui-directory-picker-native | Renderless native OS picker driver |
ui-attachment | Attachment 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:
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/onEntryErrorbridge intoctx.emitand 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:
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:
| Seat | Declared by | Occupied by |
|---|---|---|
root | runtime (built-in) | ui-layout AppFrame |
sidebar, conversation, details, shell.overlay | ui-layout | ui-sidebar, ui-conversation, (anonymous entry), overlay list |
sidebar.brand.mark, sidebar.brand.name | ui-sidebar (single) | ui-brand-official (official builds only) |
sidebar.workspaces, sidebar.workspaces.directoryFlow | ui-sidebar | ui-workspace, ui-directory-picker-* |
sidebar.settings | ui-sidebar (single) | ui-settings-general (Settings trigger + panel) |
sidebar.footer.action | ui-sidebar (list) | feature footer actions |
conversation.session, conversation.session.header | ui-conversation (assembly) | composed row |
conversation.session.header.lineage, .utilities | ui-conversation | feature header cells (fork ancestry, utilities) |
conversation.session.header.actions | ui-conversation | ui-subagent (catalog), ui-jobs (jobs list) |
conversation.view | ui-conversation (list) | ui-chat (id: 'chat'), ui-trajectory |
conversation.chat.node | ui-chat (keyed, scope: session) | ui-tool, ui-workflow-run, ui-deliverables rows |
conversation.chat.commandview | ui-chat (keyed) | command rows |
conversation.message.images | ui-chat (single) | shipped gallery (ui-chat itself) |
conversation.chat.turnTail | ui-chat (chain) | ui-deliverables |
conversation.chat.assistant-actions | ui-chat (list) | ui-message-feedback (feedback) |
conversation.details.tool | ui-chat (single) | ui-tool (ToolDetails) |
conversation.approval.detail | ui-approval (single) | ui-approval approval composer |
conversation.composer | ui-conversation (chain) | ui-user-questions (question) |
conversation.composer.bar | ui-conversation (single, session-maybe) | composer bar |
conversation.composer.dock | ui-conversation (list) | input zone rows |
conversation.input.left, .right, .overlay | ui-conversation (list) | feature input cells |
conversation.input.attachments | ui-conversation | attachment preview |
conversation.input.dock | ui-conversation | ui-goal (GoalBar), queue rows |
conversation.input.plan, conversation.input.model | ui-conversation | ui-plan, ui-model-selection |
conversation.hero.workspace, .directoryFlow | ui-conversation | ui-workspace, ui-directory-picker-* |
conversation.hero.brand.mark, conversation.hero.agentPreset | ui-conversation (root) | brand mark occupant, ui-agent-preset |
settings.trigger/.header/.action/.close/.section/.plugins.tab/.onboarding | ui-settings | ui-settings-general chrome/sections |
settings.general.item | ui-settings | ui-theme (Appearance + FontSize), locale (Language), ui-permission-presets, ui-agent-preset |
tool.call.toolview | ui-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:
- Register inside
ctx.effect(or theslots.registerService wrapper, which routes disposal through the caller's fiber). Unload then cascades registration + declarations + store mounts automatically. - 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
renderSlotsafe to hand down through props. - Store ownership is per scope. The
SlotRegistryinstance axis mapshandle × 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.workspacessit 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 —
LocaleNamespaceMapmerges and thetseat. packages/client/ui-slots/src/index.ts—SlotCore,SlotKind,SlotScope,ComposedProps.packages/client/ui-renderer/src/client/registry.ts—SlotRegistry(the runtime service) andpackages/client/ui-layout/src/client/AppFrame.tsx(the three-column frame).packages/bundle/web-app/cordis.patch.yml— the ui-module roster.