Pin sessions and workspaces to the top of the Web sidebar with per-pin row colors, a header toggle and a pinned panel, persisted through a durable settings namespace.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:PerryLink/dsh-session-pin
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Pin the conversations that matter — and color them so you can find them at a glance. A dual-face (host + browser) plugin for DeepSeek Harness with two pin levels (workspaces and sessions), a per-pin color swatch that tints the row, and four pin surfaces: a hover [pin][swatch] pair on every row, a pin toggle in the session header, a sidebar foot action with a pinned panel, and per-browser durable pinning that keeps pins and colors across restarts.
Why pinning?
Session lists sort by recency: the conversation you rely on all week slowly sinks to the bottom, and every new chat buries it further. Dragging rows in the Manual sort mode works, but nobody discovers it — and pinned chats that still get re-sorted on activity are exactly what users of other coding agents complain about. dsh-session-pin gives you the one-click UX instead, plus row colors so important areas stand out:
┌─ Workspaces ────────────────────────────┐
│ 🎨 Workbench ███ │ ← pinned workspace, tinted red
│ 📌 Implement login flow 3h │ ← pinned session, tinted teal
│ Fix the auth bug 1h │ ← hover shows a gray pin + swatch
│ Refactor the DB layer 2d │
└─────────────────────────────────────────┘
✨ Features
- 🧷 Row controls — a gray pushpin fades in at the left of the session title on hover; pinned rows keep a solid amber pin. Where the build declares the upstream per-row slot (
sessions.row.action), the [pin][swatch] pair renders through it with the authoritative session id — and the DOM overlay skips session rows entirely, so a row can never show two pin sets. On baselines without the slot, the DOM overlay covers session rows by title. - 📂 Workspace pins — workspace header rows get the same [pin][swatch] pair (the upstream slot does not render there, so the overlay covers them, matched by the host-enforced unique workspace label). Pinning a workspace moves it to the front of the workspace list via the public
workspace.insertBeforeRPC. - 🎨 Row colors — the swatch after each pin cycles through an 8-color preset palette on click (Shift+click clears). The colored row gets a left accent bar plus a translucent tint — session rows and workspace rows independently, so you can spot a region at a glance. Colors persist with the pins and are pruned with deleted entities.
- 📌 Header toggle — the same session-pin control lives in the session header's action row (
conversation.session.header.actions), keyed by the framework-resolved session id: duplicate titles and blank sessions pin correctly here. - 🗂 Pinned panel — a sidebar foot action opens a floating panel listing pinned workspaces and pinned sessions (newest pin first) with each row's color dot; clicking one jumps to it. Escape or a click outside closes it.
- 📐 Top ordering — pinning moves the session to the front of its workspace account via the public
workspace.insertSessionBeforeRPC, and a pinned workspace moves to the front of the workspace list;reorderOnLoadre-asserts both pinned prefixes after the lists load (idempotent, so it never fights the core's own re-sorting). Under the core's Manual order the position stays put. - 💾 Persistent pinning — the host half registers the durable
session-pinsettings namespace (declared wire-exposed viasettings.register({ expose: true })on builds that support it); the browser half reads through the standardsettings.*RPCs. On builds whose web proxy does not serve plugin namespaces the browser half falls back to a versionedlocalStoragedocument (v1 documents migrate), with cross-tab sync throughstorageevents. - 📡 Log-backed write channel — on builds mounting the built-in
dsh-session-pinservice, every session toggle commits through thesession.setPinnedRPC first (thesession/pinevent log is the canonical residence) and mirrors the commit into the settings store, so the ordered list, panel, and reordering stay consistent. A failed or slow RPC degrades to a direct settings write; the next connection generation re-enables it. The session-header toggle reads thepinprojection when the host serves it — cross-device commits converge through it. Workspace pins and colors are plugin-local state and always write to the store. - 🔢 Optional limit —
config.maxPinscaps the pinned count per level (default0= unlimited); exceeding it shows an inline limit hint on the badge. - 🧹 Self-healing state —
pruneStaledrops pins and colors whose workspaces/sessions were deleted or archived once the lists are ready. - 🌍 Localized UI — badge, swatch, header, foot, and panel copy ship in 中文 and English through the locale service; compositions without it keep the English fallback. Readmes: English · 中文 · Español · Português · हिन्दी.
- 🧩 Zero core changes — a standalone plugin for the stock DSH Web GUI; every new surface degrades gracefully on older baselines.
🚀 Quick start
- Install — one command from npm (the package declares a
dsh.bundlemanifest, so the plugin row registers automatically):
dsh plugin --profile <your-profile> add dsh-session-pin
Or add the plugin to your profile's cordis.yml manually:
plugins:
'dsh-session-pin':
path: /path/to/dsh-session-pin
config:
maxPins: 5 # optional; 0 = unlimited per level (default)
reorderOnLoad: true # optional; re-assert pinned order after load (default)
pruneStale: true # optional; drop pins of deleted entities (default)
Loader entry id. The loader deduplicates entry ids across the whole root include tree. On harness builds whose
dsh-basebundle mounts the built-in host service@deepseek-ai/dsh-session-pin(entry idsession-pin— the log-backed pin state andsession.setPinnedRPC), give THIS plugin a distinct entry id, e.g.id: session-pin-ui, in the profile patch row. A duplicatesession-pinid fails the whole boot with "duplicate loader entry id". The plugin's internal cordisnameand its settings namespace staysession-pin— only the profile entry id must differ.
- Build (the web app refuses to start with a missing client bundle):
pnpm install
pnpm run build # lib/index.js + lib/client.js
- Restart
dsh weband hover any row in the sidebar — the pin badge (and the color swatch) appears at the left of the title. Click to pin; click the swatch to cycle colors; Shift+click the swatch to clear the color; toggle the pin again from the session header; open the pinned list from the sidebar foot.
Uninstall — remove the plugin row from cordis.yml and restart. The session-pin section can also be removed from settings.yaml; nothing else is written.
⚙️ Configuration
| Key | Type | Default | Meaning |
|---|---|---|---|
maxPins |
integer | 0 |
Maximum pinned entities per level (sessions and workspaces each have their own budget); 0 = unlimited. Unpinning always works. |
reorderOnLoad |
boolean | true |
Re-assert the pinned prefixes (newest pin first) once the session/workspace lists are ready and on workspace changes. |
pruneStale |
boolean | true |
Drop pins and colors for entities absent from a ready list (deleted/archived). |
🧠 How it works
- Host half (
src/index.ts) — registers thesession-pinsettings namespace ({ pinned, workspacePinned, colors, workspaceColors, maxPins, reorderOnLoad, pruneStale }), with the policy riding the composition base layer. No session events, no model traffic. - Browser half (
src/client.ts) — assembles a framework-freePinStore(settings transport, degrading to a versionedlocalStoragedocument with cross-tab sync), aPinController(two-level toggle / color cycle / prune / reorder state machine), and the UI: the row overlay (workspace rows always; session rows only while the row slot is undeclared), the optional row-slot registration, the header toggle, the sidebar foot action, and the overlay panel. Ordering goes throughctx.workspaces; the row tint is pure CSS (:has()keyed on the swatch'sdata-colorclass). - Build — esbuild emits the host ESM half and the client CJS half wrapped in the web boot factory (
window.__ModuleLoader__.load({ id, factory }));reactis externalized onto the module-table seed word so the bundle renders with the shell's own React. A purity gate fails the build if any@deepseek-ai/*value import leaks into the browser bundle.
Extension points used: settings (host); sessions, workspaces, settingsScope, connection, remote, slots (client); locale (client, optional); conversation.session.header.actions, sidebar.footer.action, shell.overlay, and the upstream sessions.row.action row slot when declared. Model-visible effects: none — this is a UI-only plugin: it adds no session events and no tokens to any model request.
📦 Compatibility
| Layer | Baseline |
|---|---|
| DeepSeek Harness | npm @deepseek-ai/dsh@0.1.0-rc.6 generation (client packages 0.1.0-rc.6); newer builds activate the row slot, wire-exposed settings, and the session/pin projection automatically |
| Cordis peer | @deepseek-ai/cordis: ^4.0.1 |
| Node (dev) | ≥ 22 |
| Browser | Modern Chromium/Firefox/Safari; the row tint needs CSS :has() (Chrome 105+, Firefox 121+, Safari 15.4+) — older browsers still get the swatch dot, just no row tint |
🧪 Development
pnpm install
pnpm run typecheck # tsc --noEmit
pnpm run test # vitest unit tests (pin-core, store, controller, overlay, host registration)
pnpm run build # dual-half build + client-bundle purity check
node scripts/verify-live.mjs # live check against a running `dsh web` (DSH_CHECKOUT env)
🗺️ Roadmap
- Right-click / row-menu "Pin" entry (needs a core row-level menu slot; the row badge slot is upstream now).
- Canonical residence: a log-backed
session/pinevent +pinprojection + write RPC (upstream) — the settings namespace then retires as the durable store and the plugin consumesuseProjection('pin'). - A full color-picker popover (custom colors) once the canonical residence exists; today's cycle swatch covers the preset palette.
⚠️ Known limitations
- Persistence scope — on builds whose web proxy does not serve plugin settings namespaces, the browser half stores pins and colors in a versioned
localStoragedocument (browser-local) until upstream exposes the namespace (declared viasettings.register({ expose: true })on newer builds). The host-side registration is already in place and becomes the durable store automatically. - Ordering scope — the pinned position is stable only under Manual order; under Updated order the core's activity promotion re-fronts active sessions, and
reorderOnLoadre-asserts the prefixes on load and workspace changes. Ungrouped and flat-list views have no host-side account, so session position is not persisted there (badges, colors, and pin state still work). Workspace reordering persists through the registry display order. - Remote browsers — settings RPCs are loopback-only on the baseline; remote browsers fall back to browser-local
localStorage. - Row badge fallback — where the upstream row slot is unavailable, session rows are matched by title text; with duplicate titles the badge shows on every matching row and toggles the first match (cosmetic). The header toggle is always id-keyed and unaffected. On builds WITH the slot, session rows render only through the slot — no fallback duplication is possible.
- Workspace-row matching — workspace controls are matched by label (host-enforced unique); renaming a workspace follows automatically. The ungrouped bucket and search-result rows intentionally get no controls.
- Row DOM dependency — the overlay relies on the core rows'
role="treeitem"/aria-selected/aria-expandedstructure and must follow upstream UI changes.
🌐 Community
- DeepSeek Harness Discord · official discussions
- Discover more plugins on the
dsh-plugintopic.
👥 Contributors
Thanks to everyone who has shaped this plugin:
- PerryLink — creator & maintainer: pin UX, durable persistence, workspace ordering, per-pin row colors, five-language docs, and community engineering (v0.1.0 → v0.3.0).
Contributions welcome — open an issue or start a discussion to get involved.
📜 License
Apache License 2.0 — see LICENSE. Copyright © 2026 dsh-session-pin contributors.
PerryLink DSH Plugin Family
This project is one of the 15 DeepSeek Harness plugins maintained by PerryLink. If this one helps you, the others likely will too:
| Plugin | One-liner |
|---|---|
| dsh-mcp-panel | Read-only MCP runtime panel: /mcp command + Settings tab with status, tools and errors |
| dsh-doublecheck | Engineering-discipline guard: requirements grill, test gates, adversary review |
| dsh-background-agents | Durable background child agents with a Web UI sidebar, messaging and interrupt |
| dsh-lsp-actions | LSP diagnostics, formatting, completion, code actions and rename over language servers |
| dsh-output-styles | Claude Code outputStyles-equivalent runtime style switching |
| dsh-checkpoint-rewind | Claude Code /rewind-equivalent: snapshots, session forks, one-shot restore |
| dsh-permission-rules | Claude Code-style declarative allow/deny/ask permission rules with audit |
| dsh-auto-review | Second-model auto-review on the approval chain, fail-closed by default |
| dsh-memento | Approval-gated cross-session memory: ctx.memory seam + SQLite + memory tool |
| dsh-skill-pack-security | Security-audit skill pack: secret scan, dependency and supply-chain review |
| dsh-session-pin | Pin sessions in the Web sidebar with durable ordering |
| dsh-composer-history | Terminal-style input history for the web composer: arrows, Ctrl+R search |
| dsh-github | GitHub PR/issues integration for DSH, every write gated by approval |
| dsh-plugin-guide | Plugin-development knowledge base as an on-demand agent skill |
| dsh-claude-move | Migrate Claude Code sessions, memory, skills and CLAUDE.md into DSH |
Links
More in this category
Anionex/dsh-turn-rewind★ 62
Rewind conversation and workspace state, powered by a persistent Change Ledger.
Nwflower/dsh-chat-import★ 43
Import full-fidelity chat histories from 13 coding agents (Claude Code, Codex, ChatGPT, Cursor, Gemini, opencode, and more) as resumable DeepSeek Harness sessions, with reverse export back to Claude Code.
whyihaveyou/dsh-suite#plugin-session-export★ 35
Export the append-only session log as human-readable Markdown or HTML, grouped by trajectory source.
Chinesezjc/dsh-interconnect★ 28
Cross-instance message and event handoff between DSH instances via an interconnect server.
Moeblack/dsh-message-edit★ 24
Branch-based message editing, reroll, retry, and a version timeline.
hellodigua/dsh-share★ 19
Share your conversations with one click.