Skip to main content
PathDocs

Client Resources and Modules

Audit baseline 0.1.5-alpha.1 @ 5dda764ed3; see source/npm channels.

One-liner: live data in the browser is expressed uniformly as a resource address — one dsh-resource://<type>/… address is one resource, the protocol's owning package registers exactly one provider through ctx.resources.register, and any slot component reads it with useResource<P>(address); the right Sidebar is then ui-dockkit (a host-agnostic engine) + ui-sidebar-right (the product host) + two tab-type packages (files / text).

All four packages are new in 0.1.5-alpha.1. After reading this you can answer: how one address becomes a self-updating value, how a plugin contributes its own protocol, why dockkit is not a plugin, and what actually happens after you click a file in the tree.

1. The resource model: one address, one provider

  • A resource address is a dsh-resource://<type>/… URL whose host is the protocol name (RESOURCE_SCHEME = 'dsh-resource', and protocolOf(address) takes the host).
  • A protocol that needs a scope encodes it in the path, as dsh-resource://file/session/<sessionId>/<path relative to the workspace root> and dsh-resource://file/absolute/<absolute path> do. The model itself knows only addresses.
  • Addresses under any other scheme (sidebar://guide) are navigation addresses and name no resource.
  • ResourceProtocolMap (declared in packages/client/ui-slots/src/index.ts) is the declaration-merged protocol → value-type roster: a consumer names the protocol as a type argument and receives the owner's value type without importing the owner's runtime.

2. ctx.resources: register, pin, subscribe

MemberContract
register<P>(provider)registers the one provider for a protocol; a second registration throws. Returns an idempotent disposer (the caller holds it in its own ctx.effect)
pin(address, signal)keeps a resource open without subscribing, until the signal aborts; an already-aborted signal pins nothing
source(address)the bare observable behind the hook; getSnapshot() reads without holding the resource

The provider contract (src/client/contract.ts):

interface ResourceProvider<P> {
readonly protocol: P
open(address: string, ctx: { signal: AbortSignal }): AsyncIterable<RemoteResult<Value>>
reload?(address: string): void
}

Three hard rules:

  • open's first frame is the current content, and every later frame is one change;
  • a failure is a frame, not a throw{ ok: false, error } turns the resource failed while keeping the last value; a throw inside the stream is a programming error and is not caught;
  • it must honour signal: the stream is aborted when the last holder releases.

Lifecycle: one record per address, kept for the page lifetime and never dropped (only its state is discarded). The holder count is subscribers plus pins; the first holder opens the provider's stream, later holders share it, and the last release aborts the stream and resets the snapshot to idle. Keeping the record is what makes source() reference-stable across React's render-then-subscribe window and a StrictMode remount.

3. useResource: four states

The browser half of @deepseek-ai/dsh-client-resources builds the registry at apply's top level, then:

ctx.slots.provideRoot({ keyedHooks: { resource: address => resources.source(address) } })

so every slot component receives useResource in its props, whatever its scope.

statusMeaning
noneno provider is registered for that protocol, or the address is not a dsh-resource:// URL
loadinga provider is open and has not yielded yet
livevalue is the latest ok frame's value
failedthe latest frame reported a failure; value keeps the last one and failure is this one

reload() asks the provider for a fresh frame; it is a no-op when the protocol has no provider or no reload.

4. The client module system: dsh.client → browser bundle

Browser plugins are not imported by the host; they are declared and lazily loaded:

DeclarationPurpose
package.json#dsh.client.platform: "web"declares a browser plugin
exports["./client"]the browser half's entry bundle
dsh.client.inject[]module-graph dependencies (bundle load order) — not the same thing as cordis inject service dependencies
dsh.client.external[]extra module requests beyond the platform seed table

The host half of @deepseek-ai/dsh-client-modules scans the enabled Loader entries, composes the boot graph (injected as window.__DSH_BOOT__), and serves each bundle under /plugins; the browser half loads them lazily: executing a bundle only registers a factory, and module bodies (CSS injection included) run at materialization.

ui-dockkit is not a plugin: it declares no dsh.client and is statically linked into its consumer's bundle through tsdown's staticLinked preset (packages/client/tsdown.client.ts). It is a library, not a loadable browser plugin — which is also why its README calls it an internal engine with no stable API promise.

5. How the right Sidebar composes

ui-sidebar-right (inject = ['slots', 'layout', 'locale', 'resources']) provides two services and one domain:

NameRole
ctx.sidebarRightthe navigation and layout controller: openResource / openTab / close / active / isExpanded / toggleExpanded / focus / split / float / dock
ctx.sidebarRightTabsthe tab-type registry: register(definition) / candidates(address) / claim(address, kind?)
the Tab domainper (Session, tab id) it retains navigation, an abort signal, and bound actions; only record removal or plugin unload aborts the signal

It is also what binds dockkit to the product: every open tab pins its resource through ctx.resources.pin(address, signal), so switching away and back reads the latest value instead of reopening the stream.

The seats, all declared at runtime by ui-sidebar-right:

SeatKindPurpose
rightbarcomponentthe panel itself; it declares the child seats sidebar.right.pane.tab (keyed), .title (keyed), and sidebar.right.tab.menu.item (list)
conversation.session.header.cornercomponentthe expand button shown while the panel is collapsed
sidebar.right.pane.tabkeyeda tab's body, keyed by the type definition's id
sidebar.right.tab.guidechainreplace the guide page's contents without replacing the tab
sidebar.right.tab.menu.itemlistappend content-level menu items (layout gestures belong to dockkit)

6. Two-stage tab-type registration

  1. The type: ctx.sidebarRightTabs.register({ id, kind, patterns?, priority?, canOpen?, title, guide? }) — a static declaration returning a disposer; id is this implementation's identity in the tab system, and a second registration of an id throws.
  2. The body: ctx.slots.register({ name: 'sidebar.right.pane.tab', key: definition.id }, Body), where the body reads { sidebar, panel, tab } through useTabInfo().

Routing rules (the editor-resolver convention):

DimensionRule
bandextension (default, highest) > builtin > fallback
patternone containing : matches the whole address (dsh-resource://file/**); one without matches the URI's path at any depth (*.md, ignoring case)
ordering within a bandthe longer matched pattern wins, then registration order
vetocanOpen(address) returning false removes a candidate

The two shipped types:

PackagekindprioritypatternsRole
ui-sidebar-filesfilesbuiltinnone (a page, it claims no address)the workspace file tree; guide entry at order 10
ui-sidebar-textpreviewtextfallbackdsh-resource://file/**the plain-text viewer; any more specific type can take those addresses

ui-sidebar-files is a page: it claims no address, offers one entry box in the guide, and a file row opens through tab.actions.openResource for the file viewers to claim.

7. Worked example: the life of one dsh-resource://file/… address

The division of labour is the point: metadata rides the resource stream, content rides paged calls. A file resource's value is only { absolutePath, version, bytes?, changed }; the viewer reads the text itself one page at a time, so when the host reports a write the text on screen is never silently replaced — a reloadable notice bar appears instead.

8. Source evidence

LocationSymbol / fact
packages/client/resources/src/client/contract.tsResourceProvider, Resources, ResourceSnapshot, UseResource, ResourceStatus, ResourceOpenContext, the ctx.resources declaration merge
packages/client/resources/src/client/resources.tsRESOURCE_SCHEME, protocolOf, ResourceRegistry, ResourceRecord
packages/client/resources/src/client/index.tsinject = ['slots'], provideRoot({ keyedHooks: { resource } })
packages/client/resources/src/index.tsthe host half is empty (apply(): void {}) — the resource model lives in the browser only
packages/client/ui-slots/src/index.tsinterface ResourceProtocolMap {} (the declaration-merge host)
packages/client/modules/README.mdthe dsh.client declaration, platform: 'web', exports["./client"], dsh.client.external, window.__DSH_BOOT__, /plugins
packages/client/tsdown.client.tsstaticLinked, isStaticLinkedConfig, clientBundle
packages/client/ui-dockkit/src/contract/types.tsLayoutState, LayoutOp, PaneId/SplitId/TabId, TabRecord, DockMode, DockZone
packages/client/ui-dockkit/src/contract/adapter.tsDockLabels, TabRenderer, TabMenuExtras, DockIntents
packages/client/ui-sidebar-right/src/client/index.tsinject, ctx.reflect.provide('sidebarRight' / 'sidebarRightTabs'), the rightbar and conversation.session.header.corner seats
packages/client/ui-sidebar-right/src/client/service.tsthe ten ISidebarRight methods
packages/client/ui-sidebar-right/src/client/tab-registry.tsSidebarRightTabPriority (extension/builtin/fallback), register, candidates, claim, coexists
packages/client/ui-sidebar-right/src/client/contract/params.tsSidebarRightResourceParamsMap, SidebarRightTabParamsMap, SidebarRightNavigationParams
packages/client/ui-sidebar-files/src/client/definition.tsFILES_KIND = 'files', FILES_ID, priority: 'builtin', guide order 10
packages/client/ui-sidebar-textpreview/src/client/definition.tsTEXTPREVIEW_KIND = 'text', patterns: ['dsh-resource://file/**'], priority: 'fallback', canOpen
packages/client/ui-sidebar-textpreview/src/client/rpc.tshostFileOf, createReadPage, WorkspaceFilesReadRemote
packages/bundle/web-app/cordis.patch.ymlthe resources / ui-sidebar-right / ui-sidebar-textpreview / ui-sidebar-files rows

9. Verification

# 1. The browser plugin roster and load order (resources before ui-sidebar-right)
dsh web --dump-config | grep -iE "modules|connection|resources|ui-sidebar|ui-dockkit"

# 2. dockkit declares no dsh.client: a static-linked library, not a plugin
grep -n '"dsh"' packages/client/ui-dockkit/package.json || echo "no dsh.client: static-linked library"
grep -n "staticLinked" packages/client/ui-dockkit/tsdown.config.ts

# 3. The scheme and the address grammar
grep -n "dsh-resource" packages/client/resources/src/client/resources.ts \
packages/api/workspace-files/src/client/types.ts

# 4. In the browser: open a session's right Sidebar → Files tab → click a text file.
# DevTools should show POST /api calls for workspaceFiles.stat / read,
# plus the per-session workspaceFiles.changes stream on /api/remote.mux.

One observable behaviour: the same file opened in two different sessions is two tabs (different addresses), while opening the same address again only focuses the existing tab and delivers new navigation.params — the content identity is the whole address.

Next steps