dsh-sidebar-entries
Sidebar overhaul for the dsh web UI: long-press drag to reorder the panel icon rail, one arrow to fold it away, whole-row height caps on both the panel list and the pinned section (30% of the rail, rounded down to whole rows), a pinned-session section that mirrors the shipped row's live status (spin
Install
dsh plugin --profile web add dsh-sidebar-entries
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-sidebar-entries

A DeepSeek Harness plugin that makes the
left sidebar behave the way a long list should: drag to reorder the panel icons, **one
arrow to fold them all away** so the workspace / session list gets its height back, a
pinned-session section at the top of the workspace list that keeps its live status,
whole-row height caps on both lists so a crowded rail can never eat the session tree,
archived Sessions sunk to the bottom instead of interleaving with live work, **draggable
conversation view tabs, and a generic lift** that pulls every plugin's sidebar-footer entry
up into the same rail so nothing is split between the top and the bottom of the column.
The sidebar panel list is a stack of icon rows (插件, 插件市场, 自动化任务, …). Once a
handful of plugins are installed the stack eats the column and pushes the session list into a
narrow strip. This plugin adds three things to that stack:
- Long-press drag to reorder. Press an icon for ~0.3 s, then drag. The row lifts and follows
the cursor; the neighbours slide out of the way with a short FLIP animation; the order is
written to localStorage on release. A short click still selects the panel — only a deliberate
long press starts a drag, so nothing navigates by accident.
- A fold arrow at the top of the list. A plain chevron (
vwhen everything is shown,^
when folded) that hides every panel icon in one click. The state survives a reload. The arrow
itself never moves and can never be dragged, so there is always a way back.
- Footer entries lifted into the rail. Any plugin that registers an action in the sidebar
footer gets a row up here automatically — see below.
And one thing to the list below it:
- A 置顶 section. Pinned Sessions are gathered into one group at the very top of the
workspace list and their originals are hidden, so a pinned Session appears exactly once no
matter which workspace it belongs to. Unpinning from the copy works; the copy is rebuilt
whenever the store changes.
Three things to the tab bar above the conversation:
- Drag the conversation view tabs to reorder them. 对话 / 轨迹 / 上下文 / … reorder by
dragging, with the same lift-and-FLIP feel as the rail. The order is kept in localStorage
and reapplied on every render. The 对话 tab cannot be moved — the shipped header pins it
first — and how many tabs fit is decided by the bar's own width, not by this plugin.
And two that are about not letting the list get long:
- Archived Sessions sink to the bottom of their group. The shipped row for an archived
Session never even attaches draggable, so those rows used to sit wherever they landed and
interleave with live work. This plugin reorders them by updatedAt (newest first) to the end
of each group and draws a hairline above them, leaving live rows in exactly the order the
Host chose.
- A whole-row height cap. Both the panel-icon stack and the 置顶 section are capped at 30%
of the rail's height, rounded down to whole rows — six 36px rows, not "six and a sliced
seventh". The cap is recomputed on resize, so folding the window never leaves a half row
hanging.
Layout
lib/index.js host half — diagnostics routes (see below), no runtime dependencies
lib/client.js browser half — wraps in window.__ModuleLoader__.load; requires('react') from the host
cordis.patch.yml bundle patch — inserts the host row under the id `sidebar-entries`
The two halves are independent: the browser half is the actual feature, the host half only
provides the /sidebar-entries/* routes used for self-diagnostics and for hot-reloading the
client bundle while developing.
How the reordering works (and why it is done in the DOM)
DeepSeek Harness ships a pin/unpin model for sidebar panels (createPanelPins), but it is
memory-only — it resets on every reload — and it permanently pins the plugins row. This plugin
therefore keeps its own order in localStorage (dse.sidebar.order.v2) and replays it onto the
list on every render:
- rows are identified by
aria-label, grouped so that two rows sharing a label move together and
keep their relative order;
- the "More" row is never draggable and never moves — it is the insertion anchor;
- a
MutationObserveron the list re-applies the saved order after React re-renders, with an
"already in order → do nothing" short-circuit so the plugin cannot ping-pong with itself.
Nothing in the shipped app is patched; the plugin only manipulates the rendered list.
How the pinned section works
The shipped list already sorts pinned Sessions to the front of their own group, but they stay
scattered across workspaces. This plugin lifts them into one group:
- a
MutationObserveron[class*=listArea]re-derives everything after each render; - whether a row is pinned is read from the pin indicator the row component itself renders, so no
cached list can drift out of date;
- the original rows are
display:none(not removed — React owns them), and a copy of each is
appended to a 置顶 section inserted as the first child of the list area;
- a copy carries the markup but none of React's handlers, so a click on one is replayed on the
same element of the original by walking the child index path;
- the exception is the pin toggle: clicking it in a copy calls
uiWorkspace.unpinSession(id)
directly, the same service the shipped button calls. Replaying a click into a row that is
currently display:none is exactly what made "取消置顶" dead from inside the section;
- the copy's
⋯menu button is h
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::21,最后更新 2026-11-05