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:
- 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 ui-* modules
| Package | Job |
|---|---|
ui-slots | The pure extension-point core (SlotCore) — no React, no Cordis |
ui-primitives | Shared building blocks (markdown, math, highlight) used by widgets |
ui-theme | Theme registry (--dsw-* tokens), Appearance settings row |
ui-layout | The root AppFrame: sidebar+conversation+details columns, shell.overlay, theme presenter |
ui-sidebar | Wordmark, New Session, collapse rail, Workspace/Settings seats |
ui-workspace | WorkspaceBrowser in the sidebar + WorkspacePicker in the hero |
ui-conversation | Skeleton, chat view, composer/input docks, details shell, header actions |
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 |
ui-tool (trajectory) → 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-settings | Settings domain base: ctx.settingsScope + 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 (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/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:
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:
| 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 |
conversation.session.header.actions | ui-conversation | ui-subagent (catalog), ui-jobs (jobs list) |
conversation.input.dock | ui-conversation | ui-goal (GoalBar), queue rows |
conversation.input.plan, conversation.input.model | ui-conversation | ui-plan, ui-model-selection |
conversation.chat.node | ui-conversation (keyed) | ui-tool, ui-workflow-run |
conversation.chat.turnTail | ui-conversation | ui-deliverables |
conversation.chat.assistant-actions | ui-conversation (list) | ui-message-feedback (feedback) |
conversation.composer | ui-conversation (chain) | ui-user-questions (question) |
conversation.view | ui-conversation (list) | ui-trajectory |
conversation.hero.workspace, .directoryFlow | ui-conversation | ui-workspace, ui-directory-picker-* |
sidebar.workspaces, sidebar.workspaces.directoryFlow | ui-sidebar | ui-workspace, ui-directory-picker-* |
settings.trigger/.header/.action/.close/.section/.plugins.tab/.onboarding | ui-settings | ui-settings-general chrome/sections |
settings.general.item | ui-settings-general | ui-locale (Language), ui-theme (Appearance), ui-permission-presets, ui-agent-preset |
tool.call.toolview | ui-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:
- 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 (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.workspacessit 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: Localization —
LocaleNamespaceMapmerges and thetseat. packages/client/ui-slots/src/index.ts—SlotCore,SlotKind,SlotScope,ComposedProps.packages/client/ui-layout/src/client/AppFrame.tsx— the three-column frame — andpackages/bundle/web-app/cordis.patch.yml(the ui-module roster).