DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

jackovibe /

jackovibe/dsh-codex-pin

Verified

Codex 式会话置顶与归档风格的 DSH Web 插件(置顶=标记不是排序,宿主持久,零顺序改动)

★ 1 Stars1 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@3a27ceae

dsh-codex-pin

Codex-style pinning for the DeepSeek Harness Web sidebar — a pin is a flag, not a position.

Pinning a session lifts it into a single Pinned section above the whole workspace list; pinning a workspace lifts its whole group to the front of the tree. Neither ever calls workspace.insertSessionBefore or workspaces.insertBefore, never writes the workspace view store's order, and never switches the sidebar's sort mode: the durable order in ~/.dsh/storages/workspace.json is not touched. That promise is guarded by a static check in this repo (npm test), not just by documentation.

The Pinned section above the workspace groups

Independent community plugin. Not an official DeepSeek product, and not affiliated with OpenAI — “Codex-style” describes the interaction model it copies, not any official connection.

What you get

  • A pinned flag. Hover a session row and click the pin (📌). The session moves into 已置顶 / Pinned at the very top of the sidebar — above the host's 「工作区 / Workspaces」 section header, not merely above the tree — newest pin first.
  • The pinned row leaves its group, so nothing is duplicated: click the row in the section to open the session, click the pin control there to unpin, and the row is back where the list order says it belongs.
  • Pinned workspaces. Right-click a workspace group header and choose 置顶工作区 / Pin workspace. Pinned groups rise to the front of the tree under a 「置顶 / Pinned · N」 heading, newest pin first — Codex Desktop's projects model exactly: the pin only changes where the project is shown, and the row itself gains no button.
  • Host-durable state. Both ordered lists live in the host settings namespace codex-pin (~/.dsh/settings.yaml), so they survive restarts and are shared by every browser whose settings transport reaches the host.
  • Honest degradation. A browser that cannot reach the namespace keeps its own localStorage list — one key per list — and the section header says 仅本浏览器 / This browser only. That fallback is a fail-safe, not a one-way lock: a failed host write is retried once, and even after the page has fallen back every later pin keeps probing the host, so the note clears itself at the first write that lands.
  • No model-visible effects. No session events, no tokens, no telemetry and no external network calls — the browser half reaches only same-origin host surfaces: the settings namespace, the row menu's workspaces.openSession / forkSession / archiveSession, and the session's own rename.

How it differs from the other pinning plugins

dsh-codex-pin dsh-pin dsh-session-pin
Pin levels two flags: sessions (a Pinned section) + workspaces (whole groups lifted) two: in-workspace + global tray two: workspace + session
Durable order never written — no ordering API is called at all in-workspace pins go through workspace.insertSessionBefore and switch the sort mode to manual; the global tray is display-level pinned sessions move to the front of their account
How a workspace pin shows the pinned groups are drawn in a plugin-owned 「置顶 / Pinned · N」 block above the 「工作区」 section header, with the real wrappers kept in the tree and positioned by CSS (position: fixed); React's parent-child bookkeeping is never touched durable reorder via the ordering API durable reorder via the ordering API
Pin state host settings namespace codex-pin.pinned + codex-pin.pinnedWorkspaces, one localStorage fallback key each host order anchors + local records host settings, per-browser durable
Extras deliberately none anchor-exact restore, sort-mode restore 8 colours, boards, tags, saved views, /goto
Footprint one bundle entry; no HTTP routes client-only host + browser

Pick this one if you want pinning to be a purely visual, always-reversible annotation. Pick the others if you want pins to also drive ordering, colours or boards.

Requirements

  • DeepSeek Harness with the web profile (the plugin is platform: web).
  • Verified against dsh web 0.1.5-rc.1, and re-verified against 0.1.6-alpha.2 (that host rewrote the sidebar, so the browser half now probes host capability shapes instead of one build's API); the declared peer range is >=0.1.2-rc.1 <0.2.0 and other builds in it are untested (see Compatibility).
  • Node >=22.19.0 (only for building from source).
  • dsh plugin add prints a peer-dependency warning on every known DSH build — the ecosystem's peer declarations are optional overlays, so the warning is expected and harmless.

Install

# straight from GitHub — the path that works today
dsh plugin --profile web add github:jackovibe/dsh-codex-pin

# pin a release instead of tracking main (any tag works)
dsh plugin --profile web add github:jackovibe/dsh-codex-pin#v0.2.7

# from the tarball attached to a release (Releases → Assets)
dsh plugin --profile web add ./dsh-codex-pin-0.2.7.tgz

# from a checkout
npm run build && npm pack && dsh plugin --profile web add ./dsh-codex-pin-0.2.7.tgz

# from npm (once published)
dsh plugin --profile web add dsh-codex-pin

The github: spec is what this plugin's own development profile uses, so it is exercised on every local run; the npm name is reserved for a later publish and does not resolve yet.

Then restart dsh web (the loader composes bundle layers at boot) and hard-refresh the browser (Ctrl+Shift+R). Open the sidebar: session rows now carry the pin control on hover.

How it mounts

The package ships a bundle patch (cordis.patch.yml) that inserts exactly one loader entry:

- insert:
    - id: codex-pin
      name: dsh-codex-pin

dsh plugin add reconciles dsh.profile.bundles by itself, so do not also add this insert to your profile's own cordis.patch.yml: two layers inserting the same loader id fail the boot. Check the composition before restarting:

dsh --profile web --dump-config | grep -c 'id: codex-pin'   # must print 1

Upgrade / uninstall

# upgrade: re-run the same spec (adds the newest commit on main, then restart)
dsh plugin --profile web add github:jackovibe/dsh-codex-pin
# or move to a specific release
dsh plugin --profile web add github:jackovibe/dsh-codex-pin#v0.2.7

dsh plugin --profile web remove dsh-codex-pin       # uninstall, then restart

Session ids stay in settings.yaml under codex-pin after uninstalling; delete that block to clean up.

Usage

Action How
Pin / unpin a session hover the row, click the pin control (📌 toggles)
Pin / unpin a workspace right-click the group header → 置顶工作区 / Pin workspace (there is deliberately no hover button, as in Codex)
Open a pinned session click its row in the 已置顶 / Pinned section
Unpin from the section click the filled pin on the section row
Rename / fork / archive / delete / copy title or id right-click any session row for the row menu
Pin / rename / archive the current session Ctrl+Alt+P / Alt+Ctrl+R / Ctrl+Shift+A

Pinned and unpinned rows carry the same pair of controls — the pin (outlined when unpinned, filled when pinned) and archive — and the rows inside the Pinned section carry them too, so archiving stays available while a session is pinned. The menu's red Delete session entry calls the archive manager's workspaceRegistry.deleteSession behind a confirmation dialog; without that plugin the entry reports why instead of failing silently.

Right-clicking a workspace group header opens a smaller menu with the workspace pin toggle (置顶工作区 / 取消置顶工作区) and a copy-title row — never the session-only actions, because a header is not a session. A header the plugin cannot attribute to a durable workspace gets no menu at all: the plugin would rather do nothing than pin the wrong project. The group's own row menu (rename / delete the workspace) is the host's and is left untouched.

Once at least one workspace is pinned, a 「置顶 / Pinned · N」 heading appears inside the tree directly above the pinned groups (same light-grey label style as the session section), and disappears with the last unpin. It is a plugin-owned node, never a lift unit: the ordering planner only ever sees group wrappers, so the heading can neither be reordered by a pin nor perturb one (a React re-render that inserts a new workspace gets the heading put back where it belongs on the next sync).

Menu copy and tooltips follow the GUI language: the plugin registers zh/en dictionaries through ctx.inject(['locale']), so a Chinese GUI shows 重命名 / 置顶聊天 / 归档会话 / 删除会话 and an English GUI shows Rename / Pin chat / Archive session / Delete session — switching the language re-labels a running page with no reload.

Ctrl+Shift+A asks for confirmation before archiving the current session (a single global keystroke should not archive silently); the row menu's archive entry is a deliberate click and archives directly. Right-clicking a session row opens this plugin's menu — a Codex-style overlay. It only intercepts rows it can act on; a row without a session identity keeps whatever context menu it had. The app's own surfaces (multi-select, search, the settings pages) are untouched.

Configuration

Key Default Meaning
maxPins 0 Cap on pinned sessions; 0 = unlimited. Workspace pins are not capped. Set it on the loader entry:
# profile cordis.patch.yml
- id: codex-pin
  config:
    maxPins: 5

When the cap is reached, the oldest pin is dropped as a new one is added, and a lowered cap prunes the view on the next read. Changing the cap re-composes the host plugin: an already-open page picks the new value up when the host pushes it (observed to take tens of seconds, and writes in that window can race the re-registration) — so refresh the page after changing the cap. The browser-local fallback has no policy to read, so the cap applies to the host-backed list. The cap is a session-only knob: workspace pins accumulate in codex-pin.pinnedWorkspaces and are never pruned automatically.

Where state lives

What Where
Pinned session ids (newest first) ~/.dsh/settings.yaml → codex-pin.pinned
Pinned workspace ids (newest first) ~/.dsh/settings.yaml → codex-pin.pinnedWorkspaces
Browser-local fallback (sessions) localStorage key dsh.codex-pin.pinned
Browser-local fallback (workspaces) localStorage key dsh.codex-pin.pinned-workspaces
Session and workspace order untouched — ~/.dsh/storages/workspace.json is never written by this plugin

Compatibility

DSH Web Status
0.1.5-rc.1 verified (2026-09-16) — see Verification
0.1.2-rc.1 … 0.1.5-rc.2 inside the declared peer range, not tested by hand
any 0.2.x not claimed; the peer range stops at <0.2.0

Tested alongside @michengai/dsh-archive-manager, which takes over the sidebar slot (sidebar.workspaces, priority −0.5): the Pinned section still rendered above the tree with zero page errors. Sidebar-rewriting plugins are the most likely source of breakage, because this plugin finds rows by React fiber props (see Limits).

Verification

Static checks need nothing but Node:

npm run check   # lib/client.js is byte-identical to the assembled sources
npm test        # invariants, host contract, core units, and the bundle run in a VM

The e2e suites drive a running dsh web GUI with Playwright's Chromium. Point them at your instance with DSH_E2E_URL (a full URL including ?token=…) or let them read the URL the launcher logged into ~/.dsh/dsh-web.log; DSH_E2E_CHROMIUM overrides the executable path (any chromium-* build in the ms-playwright cache is found automatically). They need playwright-core from the dev dependencies:

node e2e/pin-e2e.mjs 3080                 # session pin -> tray -> open -> unpin -> persist, then the workspace lift
node e2e/pin-e2e.mjs 3080 --leave-pinned  # keep the session pin for a restart-durability check
node e2e/pin-e2e.mjs 3080 --leave-workspace-pinned
node e2e/state-check.mjs 3080             # read-only: what the section shows now

The workspace half of pin-e2e.mjs pins a workspace, asserts that its group is the first unit in the tree, that the unpinned groups keep their relative order and that each group's own rows stay with it, that the id reaches codex-pin.pinnedWorkspaces, and that unpinning restores the original order — and it asserts nothing at all when the page under test serves an installed build older than 0.2.8 (it records reason: … no workspaceState seam and prints the report instead).

Result of the run recorded in verification/ (redacted: tokens, session/workspace ids and titles are stripped before the records are written) against dsh web 0.1.5-rc.1 on 2026-09-16 (Windows, 5 session rows, 2 workspace groups, plugin installed from the packed tarball). That record is the 0.1.x session-pinning run. The workspace-pinning block added in 0.2.8 has not been run against a live GUI from this checkout: the local dsh web (0.1.6-alpha.2) serves a profile-installed 0.2.6 bundle without the workspaceState seam. The e2e run asserts every row of this table (and, on a 0.2.8+ page, the workspace rows) and exits non-zero when one breaks:

Check Result
Client bundle in the boot graph …,dsh-codex-pin/client.js,…; served after client-HMR re-hash
Pin controls injected on session rows 4 of 5 rows — the provisional New Session row carries no row actions
Pinned section above the workspace list aboveTree: true
Pinned session leaves its group hiddenRows: 1, back to 0 after unpin
Section row opens the pinned session the click goes through sessions.open (single-page app: there is no URL change to assert)
Unpin restores the row tray gone, hiddenRows: 0
DOM order unchanged by pinning identical title sequence before and after
Stored on the host settings.yaml → codex-pin.pinned, id read out of the row's React fiber props
Host order untouched workspace.json sessionIds arrays identical before/after
Rendered from the host, not the browser fresh headless context, no local state: 已置顶 · 1, noteShown: false
Tray controls present markers tray, tray-head, tray-row, tray-pin, tray-archive, wrap tray-ctl
maxPins: 1 two pins leave only the newest, on write and on read; see verification/maxpins-check.md
Console / page errors none
Workspace headers carry no plugin button (0.2.8) headerButtons: 0; the pin lives in the header's right-click menu
Right-click opens the workspace menu (0.2.8) menu-ws with menu-pin-ws (pin icon, label follows the pin state), no session-only rows
Workspace pin lifts the group first (0.2.8) the pinned id is workspaceIds()[0] after the sync tick
「置顶 / Pinned · N」 heading above the block (0.2.8) the heading leads the tree and the pinned wrapper follows it
No heading without a pin (0.2.8) absent before pinning, removed again on the last unpin
Unpinned groups keep their order (0.2.8) the id-filtered sequence is identical before and after the lift
A group's rows stay with it (0.2.8) the lift moves the wrapper, so each group's row order is untouched
Workspace id reaches the host (0.2.8) settings.yaml → codex-pin.pinnedWorkspaces
Unpin restores the group (0.2.8) the unit sequence is identical to the pre-lift sequence

Development

lib/index.js        host half: registers the codex-pin settings namespace
src/client-src.js   browser half (the authored source)
src/core/           DOM-free core: the dual-list store and the lift math (ES modules)
lib/client.js       served bundle — `src/client-src.js` + `src/core/*`, built by `npm run build`
scripts/build-client.mjs  assembles the bundle at the marker line and syntax-checks it
e2e/                Playwright suites against a running dsh web (dev dependency)
test/               dependency-free tests (`npm test`): invariants, contract, core units, bundle-in-VM
verification/       redacted results of the runs quoted above
shots/              e2e screenshots (local only — not shipped, they show real titles)

The loader wants a classic script registering window.__ModuleLoader__.load, so the shipped bundle cannot be an ES module — but the store and the lift math are pure enough to deserve real unit tests. They live in src/core/*.js as ES modules; scripts/build-client.mjs strips their export keywords and splices them into src/client-src.js at the //#region core marker. Edit the core modules (or the client half) and run npm run build; npm test re-derives the bundle with the same build code and fails when lib/client.js is stale.

Limits

  • Row controls are found through React fiber props and the row's own action button. A DSH UI rewrite — or another plugin replacing the sidebar slot — can strand the session row controls; the Pinned section keeps working, and the workspace half disables itself (it only reports once) when no header can be identified. The workspace half injects nothing at all, so it has no controls to strand: only the right-click menu and the 「置顶」 heading.
  • A workspace pin never moves a wrapper to another parent. The pinned block is a visual portal: the real wrapper stays a child of its own div[role='tree'] and is only drawn at the block's coordinates with position: fixed (its old inline style is handed back on unpin). A cross-parent move was deliberately rejected — React would later call removeChild/insertBefore with the tree as the parent and throw NotFoundError, which no plugin-side try/catch can intercept. A plugin-owned spacer keeps the flow clear, and the drawing is refreshed on scroll/resize (and on sidebar resizes when ResizeObserver exists; otherwise the next 1.5s sync tick catches up).
  • Workspace identity is read from the fiber props of the group header (group.workspaceId) with a fallback to an unambiguous title match against workspaces.list(). Two workspaces sharing one title cannot be told apart, so neither is pinnable until one is renamed.
  • Pinned ids are never pruned automatically: a deleted session's or workspace's id stays in the stored list but stops moving anything.
  • Pinned session rows are hidden with display: none, which also removes them from their group's “show more” count. The tray itself is re-parented into the workspace root (plugin-owned DOM, so this is safe); the workspace block keeps its wrappers in the tree and only draws them with position: fixed.
  • The row menu is a plugin-rendered overlay, not DSH's native context menu.

License

MIT — see LICENSE. The DOM layer is adapted from dsh-pin (MIT) and the host-backed store pattern from dsh-session-pin (Apache-2.0); see NOTICE.md and THIRD_PARTY_LICENSES.md.

—/ 5

No ratings yet

Verified DSH bundle

Commit 3a27ceaecc14

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout