dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
侧栏「工作区」上方的一个置顶分组:跨工作区汇总所有置顶会话,可折叠、可就地搜索,点行打开、悬停出现取消置顶按钮。
npm package:
@vfvrpq/dsh-pinned-sessions· repo:dsh-pinned-sessionsA pinned-session group in the sidebar for DeepSeek Harness: 「置顶」 sits where a Workspace group sits — a 34px header with an outline pin glyph, a count and a search button, and 32px session rows under it for every pinned session, across all workspaces. Pin enough of them and you can search right there.
DSH already pins sessions (hover a row → pin glyph, or right-click → 「置顶会话」), but the result is only a re-order inside that session's own workspace group: a pinned session still scrolls away when you switch workspaces, collapse groups, or drown in a long list.
This plugin gives the pinned set its own group, above the Workspace browser:
插件
自动化任务
工作区 ← the native section header
置顶 1 dsh-pinned-sessions v0.4.1 🔍 ← this plugin, inside the list
插件笔记:dsh-cost-… 默认工作区 4 小时前
默认工作区
新会话
…
With the search open (the count becomes matched/total):
置顶 1/3 dsh-pinned-sessions v0.4.1 ✕
[ 搜索置顶会话 ]
插件笔记:dsh-cost-… 4 小时前
默认工作区) and carries the
session's own status dot from the same store the native rows read
(uiSession.sessionStatus): running is the native rotating arc, finished-but-unopened
is a green dot, and a pending approval / plan review / question is an amber dot whose
compact label (「待审批」) replaces the timestamp; title, workspace and time also ride
the row tooltip;dsh-pinned-sessions v0.4.1 —
as a muted chip that links to
@vfvrpq/dsh-pinned-sessions
on npm (target="_blank"; the desktop shell turns that into shell.openExternal,
so it opens in your browser instead of inside the app). The chip shows the
unscoped name because the sidebar is 256px wide; its tooltip/aria-label carry
the full scoped name, and clicking it never toggles the group;It owns no new state: pinning is the Host capability the native Workspace
browser already ships (dsh-api-workspace-controller → workspaces.pinSession /
unpinSession, persisted in the workspace storage unit), so the sidebar and the
group always agree — pin with either one and the other updates immediately.
The sidebar's browsing region between the global panel rows and the foot is
declared as one single slot (sidebar.workspaces) owned by
@deepseek-ai/dsh-client-ui-workspace, and it exposes no "extra group" seat:
| seat | owner | what it holds |
|---|---|---|
sidebar.panellist (list) | ui-sidebar | the global panel rows (插件, 自动化任务, …) — a row is a button, not a group |
sidebar.workspaces (single) | ui-workspace | the whole browsing region: header, search, tree, menus |
Shadowing sidebar.workspaces would mean reimplementing the entire browser
(search, flat/grouped views, drag ordering, menus, hover cards, archive flow), so
instead this plugin mounts next to the renderer's own outlet anchor
(div[data-slot="sidebar.workspaces"], display: contents) as its previous
sibling inside the region, and portals the group into that host node:
flex: none, so the native browser keeps flex: 1 and simply
gets a little less height — the pinned group stays put while the list scrolls;MutationObserver, so the group
survives re-renders and sidebar collapse/expand;Bugs and ideas: Issues.
dsh plugin --profile <your-profile> add @vfvrpq/dsh-pinned-sessions
The package is dual-face (dsh.bundle.patch + dsh.client): the Loader row
brings up the no-op Host half, and the browser half arrives through
exports["./client"].
cd ~/.dsh/profiles/desktop
# 1) make the package resolvable (pnpm hard-links a directory dependency, so a
# symlink keeps edits live). The install name comes from the package's own
# package.json, so a scoped package lands under node_modules/@vfvrpq/:
pnpm add file:/path/to/dsh-pinned-sessions # or hand-edit package.json
ln -sfn /path/to/dsh-pinned-sessions node_modules/@vfvrpq/dsh-pinned-sessions
# 2) activate the row in your own patch layer (hot: the loader watches it)
cat >> cordis.patch.yml <<'YAML'
- insert:
- id: dsh-pinned-sessions
name: '@vfvrpq/dsh-pinned-sessions'
YAML
Then reload the Web UI (⌘R / F5). The boot graph is rebuilt per page load, so
an edited lib/client.js shows up on the next reload — no application restart.
Do not also add @vfvrpq/dsh-pinned-sessions to dsh.profile.bundles at the
same time: insert is append-only and never de-duplicates ids, so the plugin
would be mounted twice and its slot registrations would collide. (Appending that
row to a patch file that still holds the template's bare [] produces invalid
YAML and is silently ignored — [] plus a sequence is two documents in one file.)
lib/client.js is the browser bundle, written by hand in the same
window.__ModuleLoader__.load({ id, factory }) shape the build emits — plain JS
with react.createElement, no build step, no JSX. apply(ctx) registers exactly
one entry:
| registration | slot | id |
|---|---|---|
| sidebar group (portals into the sidebar) | shell.overlay (list) | pinned-sessions-section |
Client services it injects: slots, sessions, workspaces, uiWorkspace,
locale. It reads the two stores (sessions.list, workspaces.list) through
useSyncExternalStore and never writes to them directly.
<bundled node> test/bundle-check.mjs
Registers the bundle against a fake window.__ModuleLoader__, materializes the
factory with stub require, runs apply() against a fake Client Context and
renders the group with a hook shim — asserting the entry id, the dictionaries'
key parity, the anchor selector, pin order, the count, the running dot, the
unpin toggle (and that it does not also open the row), collapse, the empty hint,
the archived note, subagent filtering, the English copy, and the npm chip (its
text, its href, target/rel, and that clicking it does not toggle the group).
Because a client bundle cannot read its own manifest, lib/client.js repeats the
package name and version (PACKAGE_NAME / PACKAGE_VERSION). The chip checks
compare them against package.json, so a version bump that forgets the bundle
fails the suite instead of shipping a stale chip.
The DOM-mounting half needs a real sidebar, so it is covered by the Playwright pass recorded in VERIFY.md.
lib/index.js Host half — no-op; exists so the Loader row resolves and the
client-modules scan can find the package
lib/client.js Browser half — the whole feature
cordis.patch.yml Bundle patch: one row, id === package name
test/bundle-check.mjs
origin: "subagent") never appear.div[data-slot="sidebar.workspaces"] would hide the group.