DSHarness Systems Dissection Pinned baseline 47f943859b · 36 verified / 0 in progress / 36 chapters
中文
Reliability and Product Surfaces·Chapter 30

The Web UI Plugin Architecture

Client-side Cordis, slots, SessionRuntime, and conversation projections

VerifiedUpstream 47f943859bScope: Analyze dual host/browser plugins, module loading, slots, conversation nodes, tool presentation, reconnects, and state ownership.

Conclusion: the Web UI is a second governed plugin tree; React is only the last mile

The DeepSeek Harness browser is not a set of React components bundled directly into the page by the Host. The Host Cordis tree first derives a client-plugin graph from the current Loader composition, serves bundles with content revisions, and writes the manifest into HTML. The page then creates an independent browser Cordis tree and loads plugins from that same graph. Business state remains in the React-free ConnectionController, SessionManager, Session, SlotRegistry, and ConversationNodeAssembler; web-react binds bare observables into hooks, composes terminal props, and a single root Slot finally enters React.

This architecture separates “how code reaches the browser,” “when a plugin is authorized to contribute UI,” “what must be refetched after disconnect,” and “when one session's view state dies” into distinct protocols. Its value goes beyond pluggability: dynamic packages can declare placement, state, and capabilities without teaching the Shell a business roster. The cost is that boot, authorization, scope, and replay boundaries must agree; when they do not, the system chooses a loud failure over a quietly half-valid tree.

1. Two Cordis trees: the Host decides reachable code; the Browser decides live objects

The Web bundle's Host configuration lists ordinary Host services—API proxy, runner, and webserver—alongside the browser-plugin roster carrying dsh.client. Modules is explicitly dual-faced: its Node half scans the Host Loader tree and publishes the graph, while its browser half becomes the in-page module table. Connection, runtime, theme, locale, layout, conversation, and tool are all independent client entries in that graph.WEBUI-DUAL-ROSTER

Host Cordis tree
  Loader entries ──scan dsh.client──► manifest + /plugins bundles
          │                                  │
          └── webserver / API / SSE          ▼
                                      window.__DSH_BOOT__
                                               │
Browser Cordis tree                            ▼
  Context + Loader ◄── ClientModuleSystem ◄── manifest
          │
          └── connection → runtime → UI plugins → root Slot → React

On the Host, ClientModuleRegistry subscribes to Loader fiber changes and uses one scan path for both initial activation and incremental change. It accepts only entries that still have a fiber, are not disabled, and declare a Web client, registers the /plugins route, and injects the current graph through an index tap.WEBUI-HOST-GRAPH The page parses that graph, creates its own Context, and then starts the browser Loader. It does not serialize the Host context into the page: the Host delivers composition facts and the Browser reconstructs lifecycle.WEBUI-BROWSER-CONTEXT

Inference

The trust boundary is therefore layered. The Host decides which packages the page can obtain; the browser Cordis fibers then decide whether their dependencies are satisfied, whether they activate, and when they unload. The manifest is a composition protocol, not a remote mirror of the live service container.

2. Manifest and two-stage boot: code arrival is not plugin activation

Each graph row carries an id, bundle url, content rev, informational inject edges, and optional immediately. Entry order explicitly has no activation semantics; Cordis fiber inject waiting determines the real dependency order. After row-field shape validation at the browser boundary, the manifest splits into a module view and a plugin view.WEBUI-MANIFEST-CONTRACT

StageActionCommit condition
Module facePrefetch immediately rows in parallel; script execution only registers factoriesEvery prefetch attempt has settled; entry import reports failures explicitly
Plugin faceMount Loader and inject the module system first, then create entries concurrentlyLoader quiesced
UI gateSweep every entry's root fiberSwitch once from loading to the full UI only when all are ACTIVE

Boot waits for the immediate-tier barrier, then creates the modules entry, manifest entries, and the shell-owned app-shell entry. An individual prefetch failure is swallowed only at that optimization layer; entry import then retries it and surfaces the outcome. After loader.await(), boot still sweeps every fiber: no fiber means import failure, PENDING reports missing services, and FAILED retains the failed state. Any entry that did not activate keeps the loading page in place as the error surface instead of exposing a partial UI.WEBUI-BOOT-SETTLE

3. Dynamic module table and HMR: replace code first, then replace the fiber

ClientModuleSystem uses a lazy CJS table. An external classic script calling window.__ModuleLoader__.load only records its factory; the first import or synchronous require executes that factory and caches its exports, dependency edges, and injected styles. Synchronous require may hit only a platform seed, shell static, cache, or registered factory; it cannot start a network request. Cycles and unknown modules fail loudly.WEBUI-LAZY-MODULES

The Host HMR half watches every graph bundle with a 500ms default stat poll, updates the graph revision only after a real content-hash change, and broadcasts rebuilt over the /plugins/events SSE endpoint; it also sends a graph snapshot on connect.WEBUI-HMR-HOST The Browser serializes reloads. invalidate → prefetch obtains the new factory while the old fiber still serves, then it removes the registry runtime first, drains the old fiber, removes owned styles, clears entry.fiber, and finally calls entry.refresh() to materialize and apply again. Failures do not roll back the old bundle: an early prefetch failure leaves the old fiber serving, while an apply failure leaves a FAILED fiber.WEBUI-HMR-SWAP

Tradeoff

The HMR consistency unit is a plugin fiber, not a React component. Replacing a service provider therefore cascades naturally through Cordis activation epochs without a second HMR dependency graph. React-local state inside the reloaded plugin is correspondingly not preserved.

4. The Shell is thin: it installs the renderer and renders one root

The app-shell entry waits for slots, sessions, and layout, installs the Slot renderer supplied by web-react, and exposes an identity-stable renderApp closure.WEBUI-SHELL-INSTALL Apart from the session-title projection, the real assembly makes a single ctx.slots.renderSlot('root', {}) call. AppFrame, sidebar, conversation, details, and overlays are all formed by subsequent registrations.WEBUI-SHELL-ROOT

Fact

The Shell neither enumerates business pages nor imports every UI package. It knows the boot state, the renderer contract, and the root Slot; the Host graph supplies the surface roster, and Slot declarations supply the tree shape.

5. React-free object layer: connection, sessions, and projections exist before components subscribe

The runtime plugin first creates SlotRegistry, Conversation event/view registries, SessionRuntime, and WorkspaceRuntime, then starts the connection streams through their sole consumer. Mux frames enter sessions, Host frames enter both sessions and workspaces, generic remote events pass to remote dispatch, and connection establishment or loss calls only object-layer methods.WEBUI-RUNTIME-WIRING

WebSocket / HTTP
      │
ConnectionController       physical generations, backoff, frame pump
      │
SessionManager             instance directory, cold buffers, list, projections
      │
Session                    contiguous seq window, pending, Conversation snapshots
      │
bare getSnapshot/subscribe sources
      │
web-react                  useSyncExternalStore selector hooks
      │
components

The runtime explicitly owns the snapshot-store engine and exports bare observables; React binding does not flow back into the object layer.WEBUI-RUNTIME-STORE-OWNER The same Session can therefore serve non-React observers, the Slot renderer, and tests, while business state such as connection generations and history repair remains independent of component mount and unmount.

6. WebSocket reconnect: rebuild a generation instead of pretending to continue the old streams

Each ConnectionController generation opens mux and host downlinks while concurrently calling host.describe. describe must succeed; stream readiness normally waits for both onOpen callbacks, but the 3s carrier-open anti-wedge guard also permits progress on timeout. After readiness converges, a stream end or stream/error aborts the whole generation, enters jittered exponential backoff, and creates a new generation. Business-sink exceptions are isolated from the pump. However, host.describe itself has no generation signal or timeout: if it hangs forever, even an ended stream leaves control flow stuck inside the handshake Promise.all instead of reaching shared reconnect.WEBUI-CONNECTION-GENERATION

When a generation dies, the runtime first clears pending interactions that were valid only for that connection. When the new generation is ready, it refreshes Session and Workspace baselines and emits connection/reset.WEBUI-RUNTIME-RECONNECT SessionManager also reloads the session list and opened subagent catalogs, then calls resync() on every resident Session.WEBUI-MANAGER-RECONNECT A cold resident that has never opened no-ops on this path.WEBUI-SESSION-RESYNC

Fact

refreshList() is single-flight. Live mutations that arrive after the request starts are replayed over its response baseline, so a refetch cannot simply overwrite state already observed during reconnection with an older list.WEBUI-LIST-REFETCH

7. Cold sessions, refetch, and seq-gap repair are three distinct paths

SessionManager does not instantiate a Session for every mux frame. Ordinary events for an uninstantiated session are dropped because history will backfill them on open(). The pending buffer that must replay into a Session retains only approvals, questions, and the latest queue snapshot—facts history cannot recover. Projections and jobs instead land in manager-level mirrors without creating a Session.WEBUI-COLD-SESSION-DISPATCH A Session is created lazily when its scope is actually resolved, then its pending buffer is replayed.WEBUI-LAZY-SESSION

SituationAuthorityHandling
Cold session never openedhistory + list baselineMaintain no live window; backfill a full page on open
Previously opened resident session after reconnectnew-generation baselineClear the old window and run generation-guarded resync()
seq > tail + 1 in an open windowhistory tailBuffer new frames and repull; keep the old UI window without a loading flash

A Session's first open pulls the history tail. If session/subscribed.lastSeq is already beyond the tail and the live buffer did not cover the gap, it pulls once more. Window installation deduplicates only by seq and then stitches the buffer. A live hole enters repairGap(): later frames are buffered, the tail is fetched again, and the shared installation path runs. A failure never appends a sequence with a hole directly into the UI.WEBUI-SESSION-GAP-REPAIR

8. SlotRegistry: a children declaration is structure, specification, and render authorization

SlotMap defines kind, scope, and owner props for every location. A registration's children table not only records each child Slot's runtime specification; it also declares that this entry is the sole subject authorized to render those keys.WEBUI-SLOT-TYPE-CONTRACT SlotCore declares only root a priori. An undeclared target, duplicate child declaration, missing kind parameter, or shared store handle crossing scopes throws at load time.WEBUI-SLOT-CORE-COMMIT Once committed, the entry disposer recursively collapses its entire declared subtree.WEBUI-SLOT-CASCADE

ctx.slots.inject(key, callback) lets independently activating contributors wait for a declaration. It runs synchronously when the declaration already exists, disposes in reverse when that declaration collapses, and runs again under a new epoch after redeclaration. The controller and callback effect belong to the caller's fiber, so unloading a contributing plugin cancels either a pending wait or its active registrations.WEBUI-SLOT-DECL-INJECT

ui-layout's one registration into root simultaneously declares sidebar, conversation, details, and shell.overlay, while seating a layout store and business inject.WEBUI-ROOT-DECLARATION Layout is therefore not a hard-coded import tree; it is the first registrant that owns authorization for those child locations.

9. Four props shares: each ownership fact has one source

ShareSourceWhat the component receives
PropsRuntimeSlotMap owner/key/scope/inject plus the parent registration's child specParent owner props, slot-level inject, global kit, and scope-dependent session kit
PropsRenderSlotsThis registration's childrenrenderSlot narrowed to declared keys, plus SessionProvider when needed
PropsStoreThe registration's store handleuseStore selector and the complete actions set with draft stripped
InjectFaceThe registrant inject returnBusiness callbacks; the hooks compartment becomes useXxx

The runtime share derives scope and owner types from SlotMap; the child-render and business-inject shares meet it in final ComposedProps.WEBUI-FOUR-SHARES The store contract makes its actions the sole write set. Components never receive instance-level set/update; they read through useStore and write through baked actions.WEBUI-STORE-CONTRACT

10. web-react is the binding terminal, not the business runtime

bindSnapshotSelector() is the client stack's uniform general-purpose selector-hook constructor. It turns any getSnapshot/subscribe source into a useSyncExternalStoreWithSelector hook while capturing source methods stably.WEBUI-REACT-BINDING When rendering an entry, the renderer synthesizes global/session hooks, the projection hook, the store pair, authorized renderSlot, SessionProvider, and locale t. What the runtime supplies across this composition boundary remains a bare source.WEBUI-RENDER-KIT

A retained renderSlot closure also checks on every call that the entry remains in the ledger and that its children declared the target key. A stale closure after unload or an unauthorized key raises a dedicated error.WEBUI-STALE-AUTHORIZATION

Inference

React owns subscription consistency and component isolation; SlotCore owns composition facts; Session and Store own state facts. Keeping hooks at the terminal means HMR, tests, and non-React consumers need not emulate component lifecycle to keep business objects correct.

11. UI-state versus business-state ownership follows lifecycle, not folders

State classActual ownerExamplesReset / lifetime boundary
Connection/business factsConnectionController, SessionManager, Sessiongeneration, list, pending, event window, projectionsConnection generation, Session scope, or runtime fiber ends
Cross-component session UIDeclared Slot storeselected call, draft, active view, inspect targetCorresponding session scope is pruned
Ephemeral local UIComponentWhether one Tool row is expandedThat component unmounts or reloads
Render input known by the parentOwner propsFrozen Tool block, cwd, openFile/inspect callbackParent projection replaces it
Durable user preferenceDomain service + SettingsScopelocale, theme, busy-enter, and similar settingsHost mode follows document revisions; memory mode lasts only for the page/runtime

The conversation chat store is created as a handle during apply, with selection, draft, view, and inspect in its initial state, plus one declared action set and persistence key.WEBUI-CHAT-STORE In contrast, ToolRow's expanded flag is explicitly component-local useState and never contaminates a Session snapshot or Slot store.WEBUI-TOOL-LOCAL-STATE

12. Session scope: selection stages, resolution creates lazily, and prune reclaims one axis

SessionRuntime jointly owns the list projection, persisted selection, SessionManager, scope map, and current provide bundle. A provider-roster change rebuilds that bundle even when the id is stable.WEBUI-SESSION-RUNTIME dsh.sessions.current restores a selection candidate only at boot, while the manager retains the live in-memory selection; only a selected id still present in list items or backed by a retained subagent address projects as current.WEBUI-SELECTION-ADDRESSABILITY A masked gap clears the persisted cell so a reload stays empty, but the same process can resurface its selection when the row returns.WEBUI-SESSION-SELECTION

followCurrent() moves the stage to current and triggers an idempotent history open. Only resolve() lazily creates the Cordis Agent scope, Session object, binding, and bare provide sources. When a session is no longer eligible and has left the stage, one teardown disposes the scope fiber, unbinds the Session, clears session-keyed Slot stores, and removes the in-memory manager instance. If it is removed while staged, teardown waits until the stage moves and preserves a frozen scope meanwhile. The Host Session log remains the authoritative source from which a later open can backfill.WEBUI-SESSION-SCOPE-LIFECYCLE

A Slot store instance is cached by handle × scope key: root uses one fixed key, while session scope uses the session id. Scope pruning also clears the matching persisted value so a dead session leaves no orphan UI state.WEBUI-STORE-SCOPE-LIFECYCLE web-react's root provider follows current; strict SessionProvider remounts the whole session subtree with key={sessionId}, preventing local component state from leaking across sessions.WEBUI-SESSION-PROVIDER

13. Conversation Nodes: fold the log into business Contexts, then project multiple views

A Conversation Definition's match(event) may inspect only the current event and returns a stable business id plus a start/update role. start/update fold State, buildLocationData may publish owned data onto a Turn or Step, and buildViewNode emits the final Node for one target.WEBUI-CONVERSATION-DEFINITION

contiguous SessionEvent window
        │ match(event) → {kind, id, role}
        ▼
Context(kind:id): start + ordered updates + State
        │                  │
        │                  └── Location index: Session / Turn / Step
        ▼
target-specific stable Node ──► chat / trajectory / other view builders

replaceWindow is reserved for a full open, resync, or gap repair and for a low-frequency registry rebuild. An ordinary append does not scan old Contexts: it updates any location boundary, matches just that input, and replays only Contexts affected by dependency changes. Prepend collects only fresh events from the older page, refreshes matches whose locations changed, and replays the affected closure.WEBUI-CONVERSATION-INCREMENTAL Multiple Definitions may accept the same event; the fallback runs only when no ordinary Definition matched and claimed that same target. Context keys are kind + id. Duplicate starts, or an update that precedes its start in log order, fail loudly.WEBUI-CONVERSATION-FOLD A Definition returning an unstable view key or wrong target also fails.WEBUI-CONVERSATION-VIEW-VALIDATION

The Location index preserves object references for unchanged Turns and Steps during rebuild and returns the seq set whose locations changed. A live non-boundary append records only the current coordinates, while a boundary append revisits only seqs owned by that Turn.WEBUI-LOCATION-INDEX Historical paging can therefore supply an earlier start or predecessor without changing every visible Node's identity.

14. Tool presentation: runtime owns recursive topology; wire names dispatch atomic appearance

ui-tool first occupies conversation.chat.node under the tool-call key and declares the session-scoped keyed child tool.call.toolview in the same registration. It then mounts shell, read, mutation, search, web, todo, question, and other views independently.WEBUI-TOOL-REGISTRATION An atomic view's owner payload contains only call id, wire tool name, a frozen running/settled block, cwd, a Host-bound openFile callback, and a local inspect callback; the framework still injects the session-scoped standard kit, and the registrant may add its own inject share.WEBUI-TOOLVIEW-CONTRACTWEBUI-FOUR-SHARES It does not own traversal of the root/subcall topology or re-pair events.

ToolCallTree sends both the root call and every level of subCalls through the same keyed dispatch, with the wire tool name as entryKey. An unregistered name falls back to GenericToolCard.WEBUI-TOOL-DISPATCH The generic renderer selects terminal/read/diff/search/web cards.WEBUI-GENERIC-TOOLVIEW Its pure model derives variant, lifecycle, summary, path, and input/output from the frozen slice. A matching specialized keyed registrant replaces this fallback instead of wrapping a second layer around it.WEBUI-TOOL-MODEL

Inference

The minimum surface for extending a Tool UI is registering an atomic renderer for its wire name. Session-event pairing, Code Mode child-call topology, and ChatFlow placement remain owned by upstream projections, so each business plugin need not implement its own transcript parser.

15. Locale, Theme, and Settings: features own preferences; the Shell only provides containers

DomainState ownerPresentation ownerSynchronization
Settings transportOne SettingsScope per namespaceEach feature's own setting rowBackground read; serialized one-field write with expectedRevision; reset/event refetch
LocaleLocaleRuntime active locale and dictionary registrySlot renderer synthesizes tSnapshot revision; active namespace → zh namespace → active common → zh common → key
ThemeThemeRuntime preference, system resolution, and token layersui-layout ThemePresentertheme/change; pure DOM projection outside React
Settings shellSlot ledgers and shell-local navigation projectionui-settings-generalSubscribes to both Slot version and locale revision

SettingsScope serializes reads and writes per namespace, with writes carrying the latest revision. A refusal or transport failure causes only the latest write to trigger a recovery read. A stale read may still advance revision/base/user metadata, but it does not publish a decoded value or ready status. Binder subscribes to invalidations on the caller's context before starting the first nonblocking read. A non-loopback browser enters memory mode and never calls privileged settings RPC.WEBUI-SETTINGS-SCOPE

LocaleRuntime derives a provisional choice from the browser language and later adopts the Host selection. Typed registration requires zh and en together for a namespace.WEBUI-LOCALE-OWNER The service installs itself into SlotRegistry as the LocaleFace and registers its Language row in General.WEBUI-LOCALE-INSTALL ThemeRuntime owns light/dark/system, the OS media query, and token override layers but never touches the DOM.WEBUI-THEME-OWNER ui-layout reads its initial getter and listens to theme/change.WEBUI-THEME-PRESENTER ThemePresenter writes only color-scheme, the dark attribute, owned CSS variables, and its own meta element.WEBUI-THEME-DOM

ui-settings-general occupies sidebar.settings, then declares trigger, header, action, close, section, and onboarding children. Navigation rows derive from the Slot ledger, and copy freshness follows locale revision as well. Each feature only registers its own row or section into those locations.WEBUI-SETTINGS-SHELL

16. Known gaps: Developer Preview architecture boundaries are still moving

Public status

The root README labels the current release a Developer Preview and explicitly warns of compatibility-breaking changes.WEBUI-DEVELOPER-PREVIEW

GapCurrent behaviorPractical effect
Hung host.describe can wedge reconnectThe stream-open wait is bounded, but the concurrent unary describe call has no generation timeout or signalA dead stream cannot advance to backoff until describe settles
No progressive rendering during bootIf any entry is not ACTIVE, the page remains on loading/errorStrong consistency, but one plugin failure prevents whole-page availability
No HMR rollbackImport/apply failure leaves a fiberless or FAILED entryA later rebuild must heal it; plugin-local React state is lost
The general client unload chain remains a stubloader.unload is unimplemented; HMR uses a dedicated registry-first swapDevelopment-time hot replacement does not establish a general arbitrary-plugin unload capabilityWEBUI-UNLOAD-LIMIT
Remote browser settings are not durableNon-loopback SettingsScope uses memory modeLocale/theme can change their local runtime but lose the choice on refresh; settings surfaces that require a Host scope remain unavailable
Session scope is a single-stage modelOne list.current drives a single staged occupant; masked or cleared gaps retain the previous occupant until the next open moves the stageMultiple panes or concurrent observers require teardown to expand from one occupant to real observer ownershipWEBUI-SINGLE-STAGE-LIMIT
The Tool-details entry point is incompleteopenDetails exists but has no caller in the assembled UIThe selected call's raw details are unreachable in the current product compositionWEBUI-DETAILS-LIMIT

17. Verification scope for this chapter

Every repository claim is pinned to commit 47f943859bef60e4160492346772ded9b24f765a. This review ran only 15 existing test files directly related to the chapter's control flows; all 321 cases passed. It did not run a full build, the full GUI suite, E2E, performance, or stress tests.

pnpm exec vitest run \
  packages/client/modules/tests/loader.client.spec.ts \
  packages/client/hmr/tests/node-half.client.spec.ts \
  packages/client/connection/tests/connection.client.spec.ts \
  packages/client/runtime/tests/manager.client.spec.ts \
  packages/client/runtime/tests/session.client.spec.ts \
  packages/client/runtime/tests/sessions-service.client.spec.ts \
  packages/client/runtime/tests/conversation-assembler.client.spec.ts \
  packages/client/ui-slots/tests/core.client.spec.ts \
  packages/client/web-react/tests/scoped-slots-real-core.client.spec.tsx \
  packages/client/web-react/tests/session-provider.client.spec.tsx \
  packages/client/ui-tool/tests/toolview-slot.client.spec.tsx \
  packages/client/ui-settings/tests/settings-scope.client.spec.ts \
  packages/client/locale/tests/locale.client.spec.ts \
  packages/client/ui-theme/tests/theme.client.spec.ts \
  packages/client/ui-layout/tests/theme-presenter.client.spec.ts

Test Files  15 passed (15)
Tests      321 passed (321)
Duration   9.03s

The SessionProvider fail-loud case emitted an expected stderr stack while asserting its error boundary. The command still exited 0 with no failed case.

The loader and HMR tests pin factory arrival, materialization, invalidation, graph watching, and disposal.WEBUI-MODULE-TESTSWEBUI-HMR-TESTS Connection tests pin generations; manager and Session tests pin refetch, stitching, and gap repair, while sessions-service tests pin lazy scope creation and deferred staged teardown.WEBUI-CONNECTION-TESTSWEBUI-MANAGER-TESTSWEBUI-SESSION-TESTSWEBUI-SESSION-SCOPE-TESTS Slot/web-react tests pin the declaration gate, lifecycle cascade, real uSES binding, and keyed remount.WEBUI-SLOT-TESTSWEBUI-REACT-BINDING-TESTSWEBUI-WEB-REACT-TESTS Conversation tests pin append/prepend and location replay.WEBUI-CONVERSATION-TESTSWEBUI-LOCATION-TESTS Tool, Settings, Locale, and Theme tests pin keyed fallback, preference ownership, and the DOM-projection boundary.WEBUI-TOOL-TESTSWEBUI-SETTINGS-TESTSWEBUI-PREFERENCE-TESTSWEBUI-THEME-TESTSWEBUI-THEME-PRESENTER-TESTS

My Learning Notes

Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.