dsh-session-map
Adds a third tab to the DSH conversation view: a draggable, zoomable map of your session tree.
It answers the question the native session list cannot:
Which message, in which conversation, did this thread branch off from — and how many branches hang under it?
The native list is a line. Parent/child shows up as indentation, and once a tree has several branches the children all look the same. This plugin draws the tree as a map: click a node, you switch to that session.
What it looks like
[root · linear transform]
│
[linear map] ← parent, always directly above
│
┌───────────┴───────────┐
[image & kernel] [isomorphism test] ← children, spread across the lower half
\ /
[injective & surjective] [kernel & image] … ← shrinks automatically at 3+ children
The centre is the current session (the focus), the parent sits directly above, and children spread across the lower semicircle — the entire upper half is reserved for the parent. That rule is not just aesthetics: node cards are wide (276 px at the centre), so a child landing at dead-left or dead-right has its connecting line completely swallowed by the card, and the picture reads as "no line at all". (That was a real defect in v0.1.0, fixed in v0.1.1 — see the regression guard under Known limitations.)
- Click a node → switch to that session. (A click that ends a drag does not count, so panning never switches you by accident.)
- Whole-tree toggle lays the entire tree out at once (radial tidy-tree + layered guide rings).
- Drag = pan; wheel = zoom anchored at the pointer (same feel as a game map). There are also
− / +buttons and a "fit everything" button. - A breadcrumb across the top shows the path from the current focus to the root; the right side shows
depth N · M branches. - Status dot in each node's upper-right corner: running (cyan) / waiting for you (amber) / done (dim).
Install
dsh plugin --profile web add dsh-session-map
Uninstall
dsh plugin --profile web remove dsh-session-map
This plugin does not modify any built-in code. It is purely additive; once removed, the environment is exactly as it was.
Structure
dsh-session-map/
├── package.json # manifest: dsh.bundle.patch + dsh.client
├── cordis.patch.yml # bundle layer: inserts itself into the config tree
├── dsh/
│ ├── index.js # host half (Node) — deliberately empty, see below
│ └── client.js # client half (browser) — all of the implementation
└── README.md
Why the host half is empty
dsh/index.js exports only name / inject = [] / apply(_ctx) {} and does nothing.
There is exactly one reason: the Cordis config tree needs a resolvable module entry point for the bundle to attach. Every capability this plugin has lives on the UI side; the host side has nothing to do. So it registers no routes, no services, reads no config — what it changes = zero.
Why the client half looks the way it does
An external plugin's client bundle goes through the DSH shell's lazy-CJS protocol:
window.__ModuleLoader__.load({
id: 'dsh-session-map',
factory: function (require) {
var React = require('react') // use the shell's copy; do not bundle your own
// ...
return { apply: apply, inject: ['slots', 'sessions'], __internals: { ... } }
}
})
React must come from require('react') — the shell's own copy. Bundling your own React means hooks cannot see the shell's runtime context.
Mount point
Registered into the conversation.view seat (a list-type seat; the built-in UI ships two tabs, "Conversation" and "Traces"):
scope.slots.inject('conversation.view', function () {
return scope.slots.register({
name: 'conversation.view',
id: 'session-tree',
order: 20, // after Traces
label: 'Tree map',
inject: function (sessionId) {
return {
sessionId: sessionId,
open: function (id) { scope.sessions.open(id) }
}
}
}, TreeMap)
})
Known limitations
Ordered by severity.
- 🔴 It gets crowded at
n≥5. If the centre session has more than 5 direct branches, the lower semicircle cannot hold cards that wide and adjacent nodes overlap. The root cause is a geometric hard limit: cards are 208 px wide, the container is only ~720 px, and 5 cards need 1040 px. But real data almost never hits this — across 69 real sessions scanned, a user's own conversation had at most 2 branches; the sessions with many children (e.g. 23) were all subagents. - 🟠 Subagent sessions appear in the tree. The data-side reason for the previous point: DSH's
useSessions.idsincludes all 69 sessions, 34 of which are subagents. The plugin currently treats them all alike. - 🟡 Whole-tree view is bounded by window size. Large trees require zooming out, which shrinks the text — an inherent cost of a canvas, not a bug. Use focus mode for detail.
- 🟡 Dragging has no inertia or bounds. The feel is "tracks your hand", not "flings and coasts". Deliberate.
- 🟡 Only
parentIdis honoured. An empty session whose parent has not been persisted yet shows as "upstream" but cannot be clicked through; say something inside it and it persists. - ⚪
labelis not localised — hardcoded English "Tree map". - ⚪ No HMR wiring: editing
client.jstakes effect on page refresh (the bundle route derives its rev from the content hash). "No DSH restart needed" was measured, not documented.
A regression guard
v0.1.0 had a real defect: at n=2 the children landed at dead-left and dead-right (±90°), and with Rx=223 against "centre half-width 138 + child half-width 104 = 242", the entire connecting line hid under the cards — 3 nodes on screen, 0 lines, reading as "feature not implemented". There is now a dedicated assertion guarding it (a visibleLen sampling method requiring every centre↔child visible segment to exceed 22 px, scanned over n=1..4). Whoever next touches the angles in layoutFocus will trip it first.
Other limits, stated plainly
- The virtual root is a display-only grouping node, not a session that exists in DSH. It adds one node at the "root" level (the real root count is the N written in its title).
- Forking from a source session with no completed turn is rejected by the host (
session/fork-unavailable); the plugin reports this honestly and retries once against the current main session, recording the fallback rather than hiding it.
License
MIT
No comments yet. Be the first to write one.