Web UI and Plugin Communication
The Web UI interacts with plugins through three mechanisms:
CordisDynamicPortRPC seam: three methods — inventory / stop / remove — deliberately abstracted away from the React layer for testabilityHostObservablereactive data source: inventory / activeRuns / runErrors / renderFailures / loaded- Slot system: the
tool.view.cordiskeyed slot lets dynamic Client Packages render business UI inside the Run card
CordisPanel is the frame-wide entry point; it uses createCordisInventory for single-flight reads + reconnect-reset handling for unreliable wire.
CordisDynamicPort: the RPC Seam
interface CordisDynamicPort at packages/extensions/ui-cordis/src/client/dynamic-port.ts:14-24:
interface CordisDynamicPort {
stop // stop a run
remove // remove a plugin
inventory // read inventory
}Deliberately abstracted away from the React layer for testability — UI logic does not directly couple to the RPC implementation.
createCordisInventory: Single-flight Read + Reconnect
createCordisInventory(port, onError) at packages/extensions/ui-cordis/src/client/inventory.ts:54-113:
- Uses a
listenersSet + agenerationcounter for single-flight reads reset()makes a reconnect discard old in-flight results — prevents old host rows from being published onto a new connection
The types at packages/extensions/ui-cordis/src/client/inventory.ts:25-46:
CordisInventorySnapshot { rows, removed, read, error }
CordisInventory { refresh, retire, reset }When read: false, the panel shows loading rather than the empty state.
Single-flight read + generation is the key to reconnect: even if an old connection's in-flight promise resolves, it is discarded because
issued !== generation; on reconnect,resetdoesgeneration += 1to invalidate old reads.
removed: Set<CordisDynamicPluginId>retains explicitly-removed IDs for the history cards — preventing the panel from immediately erasing a just-deleted plugin from the UI and causing flicker.
Slot System: The Core UI Extension Point
The tool.view.cordis keyed slot
packages/extensions/ui-cordis/src/client/slots.ts:23-37:declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
'tool.view.cordis': {
scope: 'session'
owner: CordisToolViewOwnerProps // pluginId/packageId/pluginRunId
}
}
}Dynamic Client code registers with key: 'self', and the Guard binds to the current Plugin / Package.
The Slot is the core extension point of DSH UI: rather than letting a plugin directly
return <Element>, it is required to firstSlots.listSubTreeto look up the contract before registering — this ensures owner currency (pluginId / packageId / pluginRunId) is propagated, and the SlotCore's single-owner unregister cleans up on unload.
CordisPanelFace
The CordisPanelFace interface at packages/extensions/ui-cordis/src/client/slots.ts:57-72:
- hooks: inventory / activeRuns / runErrors / renderFailures / loaded
- Six callbacks:
onApprove/onDecline/onRun/onStop/onRemove/onRefresh - Injected via the sidebar footer slot
CordisPanel: the Frame-wide Entry
export function CordisPanel({...}) at packages/extensions/ui-cordis/src/client/CordisPanel.tsx:106-120:
function CordisPanel({...}) {
// uses useInventory / useActiveRuns / useRunErrors / useLoaded
// / useRenderFailures / useSessions — six hooks pulling reactive data
// selected / pending / actionErrors local state
}visiblePanelStatus: Blocking-First
visiblePanelStatus(view, selectedPackageId, loaded) at packages/extensions/ui-cordis/src/client/CordisPanel.tsx:61-81:
awaiting-approvalfirstfailednext- If no activeRun, then
idle - Otherwise delegate to
cordisVisibleStatus(...)
Ensures blocking (awaiting-approval) rows are ranked first.
UI state is in two layers: the static inventory (
rowsfrom the host registry) and the dynamic activity (activeRuns/runErrorsfrom the Client runner); the panel merges both viaRowView.selectedPackageIdOfprefers the user's selection, then falls back to next / current / last / active version.
Event Types
packages/extensions/ui-cordis/src/client/events.ts:6-18: type re-exports (CordisDynamicPluginId / DynamicCordisInventoryRow / DynamicCordisRunRequest etc.); via @deepseek-ai/dsh-api-remotes/client the types assembled by Remote are merged into the client program.Browser-side Hot Swap
reload(id) at packages/client/hmr/src/client/index.ts:104-140:
invalidate(id) → prefetch(id) → grab entry.fiber
→ registry.delete(runtime.callback) // registry-first
→ drain oldFiber.inertia
→ delete entry.fiber
→ removeOwnedStyles(id)
→ entry.refresh()
→ entry.fiber?.await()For the detailed registry-first teardown sequence and rationale, see Unloading and Hot Reload.
Dynamic port (SSE) and static port (
/plugins/events): the host half stat-polls bundle mtime → SSE pushesrebuilt→ the browser half's EventSource receives it → a serial queue prevents interleaved dispose / execute from corrupting the single-slot handoff.
What to read next
- Plugin Registry — the three-layer identity model of DynamicCordisRegistry
- Unloading and Hot Reload — the full flow of browser-side reload