← Back to home@Noob-stupid

dsh-connection-card-host

DSH session connection card host - stateful connections between sessions, with connection-level card plugins

Stars
1
Language
TypeScript
Created
Oct 1, 2026
Updated
Oct 5, 2026
GitHub repo

Introduction

dsh-connection-card-host

English | 中文

Makes a connection a first-class object in DSH: sessions are nodes, a connection is the container, cards are connection-scoped plugins — a "connection-level plugin host".

Why this plugin: other plugins hard-code their capabilities inside the plugin; this one turns capability into cards you install on a connection — mounting, unmounting and isolation are all at connection granularity.

Running several DSH sessions at once (one researching, one coding, one running experiments) is normal. What's missing isn't "sessions can see each other" as a feature — it's a programmable relationship layer between DSH sessions: without a "relationship object" to hang things on, permission boundaries, shared premises and loadable capabilities have nowhere to live.

A connection is a platform object: session A and session B are joined by one connection carrying an event bus, a permission boundary, a convention box and a card host; cards mount on the connection and are constrained by its permissions, and the connection reaches DSH through an adapter layer

On top of that relationship layer, the two ends see each other, talk to each other and share tools — without getting in each other's way.

In practice (screen recording)

Dragging a connection line out from the anchor beside the composer

Interaction structure (diagram)

Drag-to-connect structure: anchor → session row → rail

Left: the real thing · Right: the same action as an interaction diagram, showing the toggle semantics


Contents


What it does

See each otherLook up which files the other session is editing, how far its plan has got, what tools it last used — collected automatically, no effort required from the other side
Talk to each otherSend a message with an urgency you choose: notify only (no interruption) / queue / interject / preemptive interrupt (fourth tier, off by default)
Share toolsMount cards on a connection: a card can provide tools to the sessions, even with tens of MB of real dependencies
Shared premisesA "convention box" holds what you agreed on: interfaces, units, naming, who owns what
Stay out of the wayAwareness is pull-based — zero cost unless the other side asks. Unrelated connections never interrupt you

Two examples of the same relationship layer applied: session A asks session B what it is doing right now, or session B hands session A a tool that only exists on that connection.


Quick start

  1. Connect: hold the circle to the left of the composer (or the … on a session row) and drag it onto a row in the session list.

How to start, and the toggle semantics

  • The circle left of the composer → drag onto a row
  • The … on a session row → drag onto another row
  • The drop target is the toggle: unconnected row = connect; already-connected row = disconnect (the hover hint tells you which)
  • You can also pick two sessions from the "Connections" panel in the sidebar

Drag from a session row → after connecting (recording)

Dragging from a session row; after release a rail appears with per-end permission dots
  1. Done. Both ends get one quiet notice (who you're connected to, what it enables) — nobody is interrupted.
  2. Want more detail? Open "Connections" in the sidebar, or have the session call connection_peer_work itself.

Once connected, a rail appears beside the session rows, with a coloured dot at each end — that's the permission for that direction:

Connections panel: permissions are set per direction, and cards, awareness and conventions all live under the same connection

Connections persist: they are restored on the next DSH start.


Three layers: awareness / conventions / messaging

These are separate, because their costs differ enormously:

LayerMechanismEnters the other's context?Forces the other to act?Cost
A. Work statepull (the other asks)only when it asks❌ no0
B. Convention boxpull (the other asks)only when it asks❌ no0
C. Messagingpush (into its inbox)unconditionally✅ alwaysevery message

The key fact: in DSH, delivering a message forces the other session to run a turn — the agent loop has no "saw it but ignored it" state. So "talking" and "being aware" have to be built separately: to make the other side know, use A/B; only to make it act, use C.

A. Work state (automatic, zero cost)

Collected from runtime events — it asks nothing extra of the model:

【session-1a2b3c4d】
status: running a command (2s ago)
recently touched: <workspace>/src/example.js
progress: turn 12 / step 4

B. Convention box (explicit, zero cost)

What you agreed on: interface signatures, units, coordinate systems, naming, who owns what. Editable in the panel; sessions read and write it with connection_conventions / connection_declare.

C. Messaging (four urgencies, chosen by the sender)

UrgencyUnder the hoodWhat the other sees
quietinjectplaced in context without waking it — it sees the message next time it works, uninterrupted
normalfollowupqueued — it sees the message once it finishes what it's doing
urgentsteerinterjected — inserted into the turn it's currently running, read immediately
preemptsteer + optional cancelpreempted — interrupts the turn it's running (fourth tier, off by default; falls back to urgent when the conditions aren't met, and the message is still delivered)

urgent on an idle peer degrades to queued automatically (the next turn starts immediately, so the effect is the same), and never fails.

preempt conditions — all of them must hold, otherwise the message degrades to urgent and is still delivered (it never fails):

ConditionValue
Connection permissionwrite required (a read-only connection must not be able to stop the peer's work)
Rate limitat most once per connection every 5 minutes
Peer is executing a toolnever interrupts — a cancelled half-finished tool leaves a dangling call
Peer idle, or its state unknowndoes not interrupt; delivers only

preempt is destructive by design: the interrupted turn loses the work it had already done. When you cancel a turn, pass keepInbox — the default clears the inbox, dropping the user's own queued input together with messages from other sessions.


Cards on a connection

A card = a plugin mounted on a connection, with per-side visibility.

DSH pluginCard
Mounted onthe whole DSH (profile)one connection
Who can call itevery sessiononly sessions on that connection
Visibilityglobalper side: both / A only / B only
LifetimeDSH start/stopmounted and unmounted with the connection

A session uses one resident bridge tool to discover and call tools provided by cards, subject to visibility scope

Cards can provide tools to sessions

Tools registered with api.registerTool(name, fn) inside a card are reachable by sessions on that connection through one resident bridge tool:

connection_card_tool                                  ← the only resident one (1 schema)
  ├─ no `tool` argument → list the cards and tools visible to *your* side of this connection
  └─ with `tool`        → call it

Why one bridge instead of one schema per tool: the latter would make every session pay a resident cost for every card tool, while cards are mounted and unmounted dynamically. The bridge costs one schema, and it's the natural place to enforce visibility.

Cards can carry real dependencies

A card may ship its own dependencies (tens of MB is fine) and is installed under $DSH_HOME/connection-cards/cards/, without touching the DSH profile.

Install and update from the panel

Card picker: built-in cards install in one click; you can also give a package name, a repo tgz URL or a local directory

A card installs from one of three sources — the same three as the card picker in the connection panel:

SourceWhat you give itHow it works
Package name (registry)monitor-card / @scope/monitor-cardpulls the tarball from the registry (one HTTPS GET)
Repo tgz URLhttps://example.com/card.tgzdownload, then unpack
Local directoryD:\my-cards\monitor-cardcopied directly
  • Install: package name / repo tgz URL / local directory → into our own directory, no pnpm, no profile changes; usable immediately, no DSH restart
  • Update: installed cards get a "check for updates" entry with three distinct states
    「检查更新」→「↑ 更新到 x.y.z」/「已是最新」/「无法检查」
    
    "Couldn't check" is never shown as "up to date" — that would be lying.
  • Uninstall: installed cards get an entry that names what will be removed (including the card's instances on connections) and asks once. Built-in cards ship with the plugin, so they are not offered for removal. The result reports what was removed and what is still in use.

Statement: third-party / community cards install and work straight away

You can download and install external DSH-session plugin cards directly, inside DSH, and use them immediately. This is not an "official card marketplace", and it is not a curated store you submit to — it is open distribution: any package written against the card protocol can be installed into your own DSH from the three sources above.

Installed means usable — once a card is mounted on a connection:

  • Sessions on that connection can use the tools it provides (called through the connection_card_tool bridge) immediately — no DSH restart, no DSH config change: no pnpm, nothing written to dsh.profile.bundles
  • The card lives in its own directory, $DSH_HOME/connection-cards/cards/<id>/, fully isolated from the DSH profile

Third-party / community cards are not affiliated with the DSH project; this plugin offers no review, no endorsement, and there is no such thing as an "official directory".


What a card can and cannot do

A card is an ordinary DSH plugin, so not every plugin is card material. Whether a candidate can be mounted on a connection, and how much of it survives, is decided by the specifications below. The card picker labels candidates adapted / capability / local / global / undecided in advance, and labels only — it never blocks: mounting is always allowed, and the verdict is heuristic.

Mounting: three gates

GateRequirementIf it fails
① ScopeRegisters into connection-scoped positions (conversation.* / message.* / input.*) or is pure capability (no client half)Registers into App-scoped positions (sidebar / layout / settings page / themes / title bar / workspace) → not recommended as a card: a global UI does not fit in a connection-scoped panel and collides with the App layout
② ModuleEvery dependency in the import closure of the host entry resolvesMount refused, classified as fatal (a host capability is missing — the card needs redesign) or optional (a third-party dependency is missing — adding it is enough)
③ Capabilityinject ⊆ {tools, effect, llm, prompt}Mount refused — the card asks for host services this plugin does not have. Refusing is the correct behaviour; the answer is not to widen the capability surface

Scope of gate ② — only files the entry actually loads at runtime count: tests/, bin/, client/build.mjs and *.d.ts imports are not runtime dependencies.

No pnpm, no build step: the host never installs dependencies and never builds a package. A card whose build artifacts are not committed (source only) therefore cannot be mounted — the refusal says so.

Client-side UI: what renders and what does not

mountable  ≠  source readable  ≠  slots registered  ≠  rendered
CaseResult
Uses only slots / effectRenders ✓
Needs client hooks (useScene / useEnabled / locale / configForms …)Mounts and registers its slots, but fails to render — the reason is shown, never a blank panel ✓
Client bundle sizeup to 32 MB (a self-contained bundle of a few MB is normal: inlined fonts and assets)
Component invocationcreateElement(component, {}) — no props are passed, so a component that needs slot props fails and is caught by the error boundary

Support matrix

SupportedNot supported
Host toolsapi.registerTool(name, fn) → sessions call it through connection_card_tooltools registered into DSH's global loader
Paneloptional HTML rendered host-sidearbitrary browser-side execution inside the panel
Client UIcomponents that use slots / effectcomponents that need client hooks, or slot props
Connection eventson(event, handler) / emit(event, data)cross-connection events
Messagingsend(kind, text) / read() as one sideautomatic mirroring of session content (see Configuration)
Dependenciesany dependency vendored inside the cardrunning pnpm or a build step on your machine
Distributionregistry / repo tgz / local directorya curated marketplace or an approval step
Isolationper-side visibility, version gating, crash isolation for import / applya card that throws taking down the host

Stated boundaries

BoundaryMeaning
Per-side visibilityScope is per side: both / A only / B only. Visible to A ≠ visible to B; the side that can't see it gets a refusal with the reason if it calls anyway
Version gatingA DSH version mismatch is refused clearly, with the reason — rather than installing and crashing later. Cards have a second guard: the CardAPI version (a card requiring a newer one is refused at mount time, with the reason)
Crash isolationAn exception from a card's import / apply does not take down the host
Not rewrittenWe do not rewrite DSH's transport, permissions or plugin system, and a card is not registered into DSH's global loader

Architecture and cost

Three layers, with hard boundaries — each talks only to the one below:

┌─────────────────────────────────────────────────────────────┐
│  Card layer (connection-scoped plugins)                     │
│  Depends only on CardAPI; NEVER imports @deepseek-ai/*      │
│  → DSH upgrades don't affect cards; our CardAPI changes do  │
│    (guarded by a version declaration)                       │
├─────────────────────────────────────────────────────────────┤
│  Connection layer (this plugin)                             │
│  connections / permissions / awareness / conventions /      │
│  card host / message delivery                               │
│  → on a DSH upgrade, this is the only layer to change       │
├─────────────────────────────────────────────────────────────┤
│  DSH adapter layer (DSHAdapter + allowlist + audit)         │
│  the single exit point for all DSH interaction              │
└─────────────────────────────────────────────────────────────┘

Permissions are per direction

One line, one dot at each end; the colour is the permission for that direction — the two directions are independent, so you can have "A may send, B may only watch":

Line colour shows the permission in that direction: grey = read-only, blue = can suggest, orange = can write; the two ends can differ

Direction permissionAllows
Read-onlyawareness and the convention box (both are pull-based, and independent of permission)
Suggestmessaging that does not modify the peer's work
Writemessaging that can change the peer's behaviour, including preempt

Lowering a permission takes effect immediately; raising one requires confirmation from the side being granted it. Refusals explain themselves.

What it costs

ItemCostNotes
Awareness (layers A + B)0 contextpull-based; costs nothing unless the peer queries
Card tools1 resident schemainstead of one per card tool
Unrelated connections0 interruptionsconnecting doesn't wake anyone; unrelated sessions carry on
Resident tool schemas5 connection_* tools, hidden from sessions with no connectionstool schemas are filtered per session scope, so a session with no connections carries none of them

A session that has at least one connection carries the five connection_* tool schemas.


Install

From npm (recommended, version-pinnable):

dsh plugin --profile web add @noob-stupid/dsh-connection-card-host

Pinned to a version — use the tgz attached to a Release:

dsh plugin --profile web add https://github.com/Noob-stupid/dsh-connection-card-host/releases/download/<tag>/noob-stupid-dsh-connection-card-host-<version>.tgz

# the same tgz, downloaded first — identical result
dsh plugin --profile web add ./noob-stupid-dsh-connection-card-host-<version>.tgz

Straight from GitHub (installs the latest commit on the default branch, not a pinned version):

dsh plugin --profile web add github:Noob-stupid/dsh-connection-card-host

# same thing, GitHub shorthand (the github: prefix is optional) — a slash means a GitHub repo
dsh plugin --profile web add Noob-stupid/dsh-connection-card-host

Prerequisites

  • pnpm on PATH — dsh plugin shells out to it.
  • Compatibility: peerDependencies declares @deepseek-ai/dsh >=0.2.0-rc.1 <0.3.0 (plus @deepseek-ai/cordis and the two @deepseek-ai/dsh-client-* packages). DSH gates on version at install time and refuses clearly, with a reason, rather than installing and crashing later.
  • All four peers carry peerDependenciesMeta.optional, so that installing by package name adds exactly one package and does not drag the @deepseek-ai/* dependency tree into your profile. This plugin imports no @deepseek-ai/* package at runtime (the two browser-side ones are injected by DSH's __ModuleLoader__), and marking them optional does not weaken the gate — DSH's evaluatePluginCompatibility reads only peerDependencies. Details in docs/compatibility.md.
  • lib/ is committed and shipped, so the install arrives ready to load — there is no build step and no build script to authorize.

Use

Build a connection — the quick start covers the two drag gestures and the sidebar panel. The drop target is the toggle, and connections are restored on restart.

Set permissions — per direction, in the connection panel. Lowering takes effect immediately; raising needs the other side's confirmation.

Let the sessions work — with a connection in place the sessions get the connection_* tools:

ToolPurpose
connection_peer_workread the peer's work state (layer A)
connection_conventionsread the convention box (layer B)
connection_declarewrite the convention box (layer B)
connection_sendsend a message, with an urgency (layer C)
connection_card_toollist and call the tools the cards on this connection provide

Awareness tools (connection_peer_work, connection_conventions) are read-only and cannot modify the peer.


Configuration

There is no config file for this plugin — it takes no settings schema. Everything that is configurable is configured in the UI, and everything else lives in one directory:

WhereHolds
Connection panel (sidebar)connections, per-direction permissions, the convention box, card mounting
Card picker (connection panel)installing and updating cards
$DSH_HOME/connection-cards/all persistent state: connections.json, installed cards, the tamper-evident audit.log

Two behaviours are deliberately off by default and are enabled by an explicit action:

BehaviourDefaultHow to turn it on
preempt (preemptive interrupt)offrequires write permission on the connection (see layer C)
Third-party card adaptationoffcreate $DSH_HOME/connection-cards/adapter.enabled; delete the file to turn it back off

Automatic mirroring of session content is permanently off — nothing is forwarded between sessions unless a session calls connection_send. Awareness (layers A and B) is the pull-based alternative.


Writing a card

A card is just an npm package with a dshCard manifest:

{
  "name": "my-card",
  "version": "1.0.0",
  "main": "index.js",
  "dshCard": { "id": "my-card", "name": "My card", "entry": "index.js", "api": 1 }
}
// index.js — NEVER import anything from @deepseek-ai/*; go through `api`
export function apply(api) {
  api.log(`mounted (scope=${api.scope})`)

  // provide a tool to sessions on the connection
  api.registerTool('greet', async (args) => {
    return `Hello, ${args?.name ?? 'world'}`
  })
}

// panel HTML (optional)
export function renderPanel(api) {
  return `<div>visible to: ${api.scope}</div>`
}
api memberMeaning
registerTool(name, fn)register a tool → sessions call it via connection_card_tool
send(kind, text) / read()send and receive connection messages as one side
on(event, handler) / emit(event, data)connection-scoped events
scopewhich side this instance serves (both / a / b)
log(...)write to the host log

dshCard.api declares the CardAPI version the card needs (defaults to 1):

  • Adding things does not bump the version — older cards keep working
  • Only removals or semantic changes bump it — the host refuses to mount a card that requires a newer version, and says why

This is our own compatibility guard: cards don't depend on DSH internals, so a DSH upgrade doesn't affect them; but changing our CardAPI does — and DSH's version gate can't see that layer.

See docs/card-protocol.md for details.


Uninstall

Remove the plugin

dsh plugin --profile web remove @noob-stupid/dsh-connection-card-host

Remove its state — the plugin keeps everything under one directory, so removing it also removes your connections, your installed cards and the audit log:

rm -rf "$DSH_HOME/connection-cards"

Keep that directory if you intend to reinstall and want your connections back.


FAQ

Do I need a connection before I can use cards? Yes. A card is mounted on a connection, and only sessions on that connection can call the tools it provides.

Does mounting a card change my DSH profile? No. Cards install under $DSH_HOME/connection-cards/cards/<id>/; no pnpm run, nothing written to dsh.profile.bundles, no restart.

Why can't I mount a popular plugin as a card? Most likely gate ② or gate ③ in what a card can and cannot do. The refusal names which gate failed and why. The most common cause is a package that ships source without committed build artifacts — the host never builds anything.

My card mounted but shows nothing. See the client-side UI table: a card that needs client hooks registers its slots but cannot render. The panel shows the reason instead of going blank.

Does a connection cost me context? Awareness (layers A and B) is pull-based and costs nothing until a session asks. Sessions with no connections carry none of the connection_* tool schemas; a session with a connection carries the five schemas, and card tools cost one bridge schema in total.

Can I use it with the peer session offline? Messages still arrive, but the peer's UI shows them as ordinary messages rather than as a connection card, and the content is prefixed to say so. Nothing is lost.

Are third-party cards reviewed? No. There is no marketplace, no review and no endorsement — install from sources you trust.


Docs

DocumentContents
docs/capabilities.mdCapability report: per-item results, total context cost, known limits
docs/card-protocol.mdCard protocol: manifest, CardAPI, install validation, distribution
docs/compatibility.mdCompatibility: how DSH's version gate works, the two lines of defence
docs/adapter-api.mdDSH adapter: the stable interface and its allowlist

Maintenance

This repository is the stable face and only receives promoted releases. Development happens on the preview line, dsh-connection-card-host-preview.

Working on this plugin
  • lib/ is committed and CI asserts source/artifact parity (scripts/ci/check-src-lib-parity.mjs), so a change to src/ must be accompanied by a rebuilt lib/.
  • Host-side changes only take effect after DSH restarts or the plugin is reloaded; client-side changes additionally need a page refresh.
  • CI runs four gates: syntax check, unit/contract tests plus artifact parity, the plugin patch manifest, and a scan for machine-specific paths in tracked files. All four are hard gates.
  • npm test needs no dependencies and no network.

MIT · not affiliated with the DSH project