Skip to content
源码版本47f9438 (dsh@0.1)

Web UI and Plugin Communication

The Web UI interacts with plugins through three mechanisms:

  1. CordisDynamicPort RPC seam: three methods — inventory / stop / remove — deliberately abstracted away from the React layer for testability
  2. HostObservable reactive data source: inventory / activeRuns / runErrors / renderFailures / loaded
  3. Slot system: the tool.view.cordis keyed 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:

ts
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 listeners Set + a generation counter 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:

ts
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, reset does generation += 1 to 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:
ts
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 first Slots.listSubTree to 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:

tsx
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-approval first
  • failed next
  • 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 (rows from the host registry) and the dynamic activity (activeRuns / runErrors from the Client runner); the panel merges both via RowView. selectedPackageIdOf prefers 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 pushes rebuilt → the browser half's EventSource receives it → a serial queue prevents interleaved dispose / execute from corrupting the single-slot handoff.

Unofficial community learning site. Content based on the MIT-licensed deepseek-ai/deepseek-harness source. · Privacy · Terms · About