# dsh-plugin-archive-manager A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) Web plugin that makes session archiving reversible and visible: - **View archived sessions in place** — archived sessions reappear in the workspace list at their original position (workspace accounting slots are preserved by DSH), with their titles dimmed compared to unarchived ones. - **Filter group in the view options** — the workspace view options menu gains a **Filter** group with two independently checkable items: **Archived** and **Not archived** (default: Not archived checked). Check *Archived* to see archived sessions; uncheck *Not archived* to see archived sessions only. - **Unarchive from the row menu** — for an archived session the row action becomes **Unarchive** (with a dedicated unarchive icon) instead of *Archive session*; clicking it restores the session durably. - **Workspace-level archive** — the workspace row menu gains **Archive workspace / Unarchive**: archiving hides the whole group (header + its sessions) per the filter; unarchiving restores the workspace while **each session keeps its own archive state** (the two are independent). > 中文说明见 [README.md](./README.md)。 ## Screenshots **Filter group in the view options** — the workspace view options menu gains a **Filter** group with two independently checkable items: **Archived** and **Not archived** (default: Not archived checked). Check *Archived* to see archived sessions in place; uncheck *Not archived* to see archived sessions only. ![Filter group in the view options — archived / not archived](screenshots/archived_options.png) **Unarchive from the row menu** — an archived session's row action becomes **Unarchive** (with a dedicated unarchive icon) instead of *Archive session*; one click restores the session durably, at its original position. ![Unarchive row action on an archived session](screenshots/unarchive.png) ## Why this exists DSH already archives sessions (they disappear from every grouping surface), but the shipped UI has **no way to view or unarchive archived sessions** — the official `dsh-client-ui-workspace` README states: *"sessions can be archived, but archived sessions have no viewing or unarchive surface."* The underlying data model was built for unarchiving (an archived session keeps its workspace accounting slot; the registry comment says *"unarchiving restores its position"*), but no API or UI exists. This plugin fills that gap. ## How it works Two halves, one npm package: | Half | File | What it does | | --- | --- | --- | | Host (node) | `lib/index.js` | Patches `ctx.workspaceRegistry` with the missing `unarchiveSession(sessionId)` (mirrors the official `archiveSession`: idempotent, durable, same `domain/changed` → `host/archived-sessions-changed` broadcast chain); maintains the **workspace-level archive state** (`$DSH_HOME/storages/dsh-plugin-archive-manager.json` — DSH has no workspace-archive concept); registers 4 exact HTTP routes for the browser half. | | Browser (web) | `lib/client.js` | **Shadows** the `sidebar.workspaces` slot (priority `-1` vs the official `0`; lowest priority renders — a framework-supported mechanism) with a faithful re-implementation of the workspace browsing region: section header + view options, search, grouped/flat lists, drag reorder, rename/fork/archive dialogs, workspace rename/delete — plus the Filter group, dimmed archived titles, session/workspace Unarchive row actions. | ### HTTP routes | Route | Method | Body | Purpose | | --- | --- | --- | --- | | `/api/dsh-archive-manager/state` | GET / POST | — | Returns `{ ok, archivedWorkspaceIds }` (workspace archive set) | | `/api/dsh-archive-manager/unarchive` | POST | `{ sessionId }` | Unarchives a session; returns the latest `archivedSessionIds` | | `/api/dsh-archive-manager/workspace/archive` | POST | `{ workspaceId }` | Archives a whole workspace (idempotent); returns the latest `archivedWorkspaceIds` | | `/api/dsh-archive-manager/workspace/unarchive` | POST | `{ workspaceId }` | Unarchives a workspace (idempotent); returns the latest `archivedWorkspaceIds` | All routes require the custom header `x-dsh-archive-manager: 1` and a loopback Host. ### Workspace-level archive - **Archive workspace** — the workspace id is added to the plugin state file (idempotent); the client derivation hides the whole group (header + sessions). With *Archived* checked, the group reappears as a dimmed row. - **Unarchive workspace** — the id is removed from the state file and the workspace returns exactly as before. **Each session keeps its own archive state** (workspace archive and session archive are independent). - Deleting an archived workspace only removes the registration; the id left in the state file is a harmless orphan (re-adding the same directory creates a brand-new workspace with a new id, unaffected). ### The unarchive data flow 1. Click **Unarchive** on an archived session row. 2. The browser half `fetch`es `POST /api/dsh-archive-manager/unarchive` (same origin) with `{ sessionId }` and the required custom header `x-dsh-archive-manager: 1`. 3. The host route calls the patched `unarchiveSession`, which writes the durable registry state (removing the id from the global archive set) through the **same** `setState` path the official archive uses. 4. The registry write emits `domain/changed`; the host API proxy diffs the archive set and broadcasts `host/archived-sessions-changed` to **every connected client** (including other tabs). 5. The client runtime updates `archivedSessionIds`; the browsing region re-renders — the row un-dims, the menu swaps back to *Archive session*, and the filter rules apply immediately. Because the mutation goes through the official persistence path, everything is durable across restarts and consistent across tabs — no custom RPC protocol, no event plumbing of our own. ### Viewing an archived session's conversation Stock DSH force-clears the conversation selection whenever the current session is archived (that is part of "archived sessions have no viewing surface"), so clicking an archived row would instantly bounce back to the empty New Session view. This plugin disables that rule (a small patch on the client `WorkspaceRuntime.project()` projection, removed again on uninstall), so **clicking an archived session opens and reads its conversation normally**. Behavior change that comes with it: archiving the *current* session no longer auto-returns the conversation to New Session — the session stays open and remains visible as a dimmed row. ### Filter semantics A session is visible when `(archived && showArchived) || (!archived && showUnarchived)`. Defaults: `showUnarchived: true`, `showArchived: false` — identical to the stock behavior. Both can be checked at once (see everything); both unchecked shows an empty list. Filter state is persisted in the plugin's own `localStorage` key and survives reloads. Grouping/ordering preferences are migrated once from the official `dsh.workspace.view.v5` key. ## Install The plugin targets DSH `0.1.0-rc.x` (tested on `0.1.0-rc.6`). ### From GitHub (recommended) ```bash npx @deepseek-ai/dsh plugin --profile web add github:piaohua/dsh-plugin-archive-manager # or dsh plugin --profile web add github:piaohua/dsh-plugin-archive-manager ``` ### From npm (once published) ```bash npx @deepseek-ai/dsh plugin --profile web add dsh-plugin-archive-manager ``` Then **restart `dsh web`**. The plugin is a profile *bundle* (it ships a `cordis.patch.yml` that inserts its loader row), so it is picked up by the normal `dsh plugin` reconcile flow. To uninstall, run `dsh plugin --profile web remove dsh-plugin-archive-manager` and restart — the stock workspace browser returns exactly as before (shadowing is reversible by design). ## Local development ```bash # in this repository npm run build # assemble lib/client.js from src/ (client bundle + embedded CSS) npm run verify # pre-publish structural checks node scripts/smoke.mjs # execute the bundle factory in a stubbed browser environment # install the local checkout into your web profile (path specs are anchored to your cwd) npx @deepseek-ai/dsh plugin --profile web add file:/absolute/path/to/dsh-plugin-archive-manager ``` ## Package layout ``` dsh-plugin-archive-manager/ ├── package.json # dsh.bundle.patch + dsh.client declaration ├── cordis.patch.yml # composition patch: inserts this plugin's loader row ├── lib/ │ ├── index.js # Host half: registry unarchiveSession + exact HTTP route │ └── client.js # Browser half: __ModuleLoader__ bundle (shadow browser + new UI) ├── scripts/ │ ├── build.mjs # assemble lib/client.js from src/ │ ├── verify.mjs # pre-publish checks │ └── smoke.mjs # bundle factory smoke test ├── src/ │ ├── client.src.js # browser bundle source (CSS placeholder) │ ├── rows.css # official row styles (re-tagged, self-contained) │ ├── browser.css # official browser styles (re-tagged, self-contained) │ ├── picker.css # official picker styles (re-tagged, self-contained) │ └── extra.css # plugin-only styles (dimmed archived titles) ├── README.en.md # this file (English) ├── README.md # 中文说明(GitHub 默认显示) └── LICENSE # MIT ``` ## Publishing The built artifacts (`lib/`) are committed, so the repository itself is installable via the GitHub route above. To also publish to the npm registry: ```bash npm publish ``` Install from npm as shown above. ## Known trade-offs & compatibility - **The browsing region is a faithful re-implementation, not the official component.** DSH 0.1.0-rc.x exposes no extension slots inside the workspace browser (only the two directory-flow holes), so the only supported way to change its behavior is to shadow the slot. The implementation copies the official logic and styles (MIT, re-tagged CSS injected independently), so visuals and interactions match; but **future DSH UI changes to the workspace browser will not automatically flow into this plugin** — update the plugin to track DSH releases. - **"Add workspace" uses the host native directory picker** (`pickDirectory`) with a path-input dialog fallback, instead of the official directory-flow hole (which is declared by the shadowed entry and therefore cannot be rendered by this plugin). The conversation empty-state picker is unaffected. - The row menu keeps **Rename / Fork** for archived sessions (both remain valid on archived sessions). - Removing the plugin restores the stock browser; the plugin's filter state lives in its own `localStorage` key and leaves no residue. ## License MIT