dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
English | 简体中文
The management plugin for the Panellist in the DSH (DeepSeek Harness) client.
The Panellist is the column on the left of the session UI: the global entries contributed by individual plugins — plugins, automation tasks, skill center, memory system, SSH, MCP connectors… They come from different plugins, each declares its own order, and each decides its own icon and name.
This plugin lets the user take over: which Panel entries appear on the rail, in what order they appear, what they are called, what icon they use, and which entries are folded into a Group. All configuration is done in a new tab in Settings, and changes take effect immediately.
Status: implemented. This README pair is the whole specification — there is no separate design document: behaviour is §2–§3, the configuration and the interfaces are §4, the compatibility contract is §5, how to install it is §6, and how it is built is §7. The normative configuration shape is docs/schema/config.schema.json.Three refinements to the original design (all discovered during implementation and backfilled into this document): the rail id of a Group isdsh-panellist:; unconfigured Panel entries are always listed on the rail, but anorderis written only once the Sequence is non-empty (i.e. after your first structural change); and the/panellist/api/revealendpoint was added to implement "Open config file".
Settings → Panel list · A Group menu on the rail :-: · :-: ·
The DSH Panellist is a read-only projection: the shell enumerates the registrations in the sidebar.panellist slot and renders them sorted by the order each item reported at registration time. No official API is provided for rewriting someone else's registration:
order of its own item;label;So this plugin has to use two levers at the same time, and keep DOM intervention to a minimum:
The Sequence is a series of ordered items, each of which is either a single Panel entry or a Group:
The design conversation settled the notation this way (G{…} = one Group):
Notation · Interpretation a c b d e · [a][c][b][d][e] a b c · [a][b][c] a dropdown(b c d e) · [a][G{b,c,d,e}] a dropdown(b d e c) · [a][G{b,d,e,c}] a dropdown(b e c)d · [a][G{b,e,c}][d] dropdown(a b c)d e · [G{a,b,c}][d][e]
Three sources, and you can switch back to the original icon at any time:
Icon*OutlineRegular weight: `viewBox="0 0 16 16"`, `fill="none"`, `stroke="currentColor"` at 1px**, geometry on the 16 grid at half-step coordinates inside roughly 2…14, round caps and joins, aria-hidden, no ids and no external references. Three of them are the shipped artworks of the rail's own defaults, so a panel given one of them looks identical to the entry it replaces: puzzle is the plugin pinwheel (IconPluginPinwheelOutline), clock the clock (IconClockOutline), more the ellipsis (IconEllipsisOutline). The set is checked mechanically at 16px next to those shipped artworks, and its ink extents are measured against them;script, foreignObject, event attributes, and external references (cases where href/xlink:href point outside);The settings navigation row has its own dedicated glyph (a panel list, at the heavier Icon*OutlineMedium 1.3px weight that column is drawn at) — see §3.
main fallback (so selecting the Group id programmatically cannot error) but has no entry point of its own.createGroup anchors on the *identity* of the row above the topmost member), so it holds for any gesture.Enter/Space expands, Esc closes, and the up/down arrow keys move within the Group.Config changes take effect immediately: no need to restart DSH, no need to reload any plugin, and no need to reload the page. That holds for *every* edit, including the ones that write nothing at all: the rewrite pass compares a digest of everything the rail renders (order, name, icon, shown/hidden) and re-projects when it moves, so a second icon pick or a second rename lands on the rail just like the first one did.
The management panel lists the whole rail from the first time it is opened: there is nothing to "adopt" before you can touch a Panel entry, and there is no "not taken over yet" list. (An earlier design had a "not configured" list with per-item take-over / release buttons; it was removed on user feedback — it only added a step. A banner plus a "use the current rail order as a starting point" button went the same way: the list is the rail either way, so the gate is invisible.)
What still holds is the minimal-intervention property in what gets WRITTEN, not in the UI:
sequence is partially authoritative: it records only what you actually changed.sequence that is empty means no `order` is written at all (the state right after installation, a pure no-op), and an unconfigured Panel's name/icon/hidden state is left entirely alone; it keeps the contributor's own order.Implementation (src/shared/model.ts): rows is always the whole rail (an unconfigured row's order is null, meaning "do not write"), while reordering decides whether an order is written at all.
The shell may apply a client bundle more than once on one page (it re-evaluates a bundle whose file changed, an HMR pass re-applies it). Two controllers over one ledger fight: the second one captures the first one's glyph wrappers as if the contributors had installed them, and both write the same entries. apply therefore retires the previous mount before building its own (src/client/mount.ts), and every wrapper this plugin installs is recognisable, so an entry still holding one is unwrapped rather than adopted (§7.2).
The rail's last row, More, is not a Panellist registration: DSH renders it itself, from the sidebar.panellist.actions slot, and portals the menu it opens to document.body. So it is not a sidebar.panellist entry — no order, no label thunk, no glyph component for this plugin to rewrite (§7.2). It is listed in the sequence all the same, because it is an ordinary flex child of the same nav:
shell:more) takes a place in the sequence like any other and can be dragged or moved up and down. The rewrite pass writes the rail in DOM order, so its index says exactly how many rows precede it; the injected rule gives the More anchor order: 1 and every row from that position on order: 2, which parks it exactly there with flexbox. No other row selector is touched, and while the row is already last nothing is written at all.By design it cannot be renamed, re-iconed or grouped: its name, glyph and behaviour belong to the shell, and it has no page to open, so it can neither start a group nor be folded into one — its appearance editor says exactly that instead of offering the controls. One caveat: what moves is its painted position; keyboard order does not follow, so Tab still reaches it last, where the DOM puts it. (Every other row is different: their order is written into the registration itself, so the shell renders them in the new order and their tab order follows.)
Older shells are handled by measurement, not by a version check. Probe 5 (§5) reports what the rail actually contains, and only two of its answers mean "there is no row": a panel list holding nothing but rows, and a list whose non-row child no longer matches. In both cases the row is left out of the sequence, so an unsupported build shows no dead entry. If the profile already configures it — a stored position, or a saved Hide — the row stays listed and carries a Not painted here badge instead, because dropping it would hand that setting to orphan cleanup. A rail that is merely not mounted (collapsed sidebar) is *unknown*, not absent, and unknown keeps the row.
Adds one item to the left side of the Settings dialog:
面板列表); position: near the end of the settings navigation;Icon*OutlineMedium (1.3px) weight every other row in that column is drawn at, painted over the kernel's fallback gear as a currentColor mask. The settings navigation has no icon field in its contract, so the row is located by its visible text and marked; if that probe fails, the native gear stays (without reporting an error). See §7.5.Page content:
Shell row; only its position and its visibility are editable there (§2.12).Import validates the document before writing it; export serialises the whole file (all profiles). A failed import reports the field-level reason and changes nothing.
POST /panellist/api/reveal opens $DSH_HOME/panellist.json in the system file manager. The path is also printed next to the button.
Configured ids with no registration are collected here with a count, and "Clean up missing panels" removes them — after an explicit confirmation, never automatically.
The result of the four startup probes (§5) with a "Re-run self-check" button. A failed probe shows what it measured, what is therefore disabled, and the DSH version the plugin measured.
$DSH_HOME/panellist.json (permissions 0600, written as "temp file + atomic rename", falling back to a direct write when a Windows file lock conflicts);schemaVersion, and an older version is migrated as needed when read;{
"schemaVersion": 2,
"profiles": {
"desktop": {
"sequence": [
{ "type": "entry", "id": "plugins" },
{
"type": "group",
"id": "group:work",
"label": "更多",
"icon": { "kind": "original" },
"members": ["schedules", "skills", "memory"]
},
{ "type": "entry", "id": "ssh" }
],
"entries": {
"plugins": { "label": "插件市场", "icon": { "kind": "builtin", "name": "puzzle" } },
"mcp-connector": { "hidden": true }
},
"meta": { "updatedAt": "2026-10-10T03:00:00.000Z" }
}
}
}The authoritative definition is docs/schema/config.schema.json (JSON Schema 2020-12; the host validates the whole document with ajv before every write and after every read, and tests/shared-schema.spec.ts runs this very example through it). sequence is a partial authority: the items it lists follow this order, and items it does not list are appended to the end by the program.
Purpose · Identifier Settings navigation tab · settings.section id = dsh-panellist Group row (rail) · sidebar.panellist id = dsh-panellist:, for example dsh-panellist:group:work Group page (main fallback, so selecting the Group id cannot error) · main key = the same string Host-half local route · /panellist/api
Host-half routes (reachable only via local loopback, with a trusted-host fence):
Method · Path · Purpose GET · /panellist/api/config?profile= · Read the config for that profile PUT · /panellist/api/config?profile= · Write the whole thing (server-side validation + atomic write to disk) POST · /panellist/api/config/reset?profile= · Revert to defaults POST · /panellist/api/reveal · Reveal the config file in the system file manager (the "Open config file" button) GET · /panellist/api/health · Config storage and field-level self-check status
?profile= only accepts ids that actually exist under $DSH_HOME/profiles/; without the parameter, the config is written to the host's own profile (DSH_PROFILE, else default). /health and /reveal are whole-file level and ignore the parameter.