Bundle
dsh-sidebar-superdoc-docx
DSH web plugin: open and edit .docx files in the better-sidebar editor through SuperDoc (superdoc.dev) — a browser-native DOCX editor. Assets are self-hosted from this package's node_modules; edited documents export back to disk atomically.
- Source
- chendefine
- License
- MIT
- Updated
- Updated 6 days ago
Readme
# dsh-sidebar-superdoc-docx
English | [中文](README.zh-CN.md)
Open and edit **.docx** files directly in the [DSH](https://github.com/deepseek-ai/DeepSeek-Harness) web GUI's
**better-sidebar** editor, powered by [SuperDoc](https://github.com/superdoc/docx-editor) — a browser-native DOCX
editor that reads and writes the real OOXML (no server-side document service needed).
> **Dependency**: this plugin registers its file viewer with
> [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) (`>= 0.13.0`) — a **mandatory peer
> dependency**. Without it, the viewer never appears. Install it first (or together with this plugin).
>
> **⚠️ License notice**: this plugin's own code is MIT, but it integrates AGPL-3.0 `superdoc` and the
> proprietary-licensed `@superdoc/docx-engine` at runtime — installing it means accepting those terms. See
> [License](#license) below and [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
## Features
- **Browser-native DOCX editing** — view and edit `.docx` in the sidebar with comments and tracked changes; three
open modes selectable in the settings page: editing / suggesting / viewing.
- **Save back to disk** — the `Save` button exports the edited document and **atomically overwrites** the opened
file through a dedicated route (tmp + rename, never a half-written state); unsaved edits show a `●` dot.
- **Follows outside edits** — a 3s disk poll: a clean editor auto-swaps to the new version in place
(`replaceFile`); a dirty editor only shows a banner with a manual `Reload` — your edits are never silently
dropped.
- **Fully self-hosted & offline-capable** — the SuperDoc editor build and the DOCX engine (web workers included)
are served same-origin from this package's `node_modules`: no CDN traffic, no third-party document service,
telemetry off by default. Fully offline after `pnpm install`.
- **Download fallback** — every surface (including all error states) keeps a plain download link.
- **Sidebar-adaptive** — the toolbar folds into an overflow menu as the pane narrows; pages fit-to-pane zoom;
readable in light and dark themes.
## Use cases
- **Human–AI collaboration on one Word file**: the AI agent edits a `.docx` in the session; you see the new
version in the sidebar within seconds, polish it by hand, save — and the agent's next turn works from the
latest version. A two-way loop without interruptions.
- **Intranet / offline / compliance environments**: deployments that must not reach CDNs such as jsdelivr or
SaaS document services; every asset is self-hosted same-origin.
- **No document server wanted**: unlike OnlyOffice / Collabora, which need a separate Document Server, this
plugin has zero service dependencies — install and go.
- **Document review workflows**: open in `suggesting` mode; comments and suggestions land as tracked changes.
- **Quick preview**: replaces the built-in code/download viewers — click a `.docx` in the file tree and see the
paginated document, with a download link one click away.
## Installation
### Prerequisites
- Node.js `>= 20`;
- the DSH web GUI and its `web` profile;
- `dsh-better-sidebar >= 0.13.0` installed in the same profile (see the dependency note above).
### From npm (recommended)
The package is published on npmjs as `dsh-sidebar-superdoc-docx`:
```sh
dsh plugin --profile web add dsh-better-sidebar dsh-sidebar-superdoc-docx
```
Or by hand: edit the profile's `package.json` (e.g. `~/.dsh/profiles/web/package.json`), add the two
npm dependencies and the bundle entries, then run `pnpm install` in the profile directory:
```jsonc
{
"dependencies": {
"dsh-better-sidebar": ">=0.13.0",
"dsh-sidebar-superdoc-docx": "^0.1.0"
},
"dsh": { "profile": { "bundles": ["…", "dsh-better-sidebar", "dsh-sidebar-superdoc-docx"] } }
}
```
Finally **restart `dsh web`** (the host half must reload) and hard-refresh the browser (Ctrl/Cmd+Shift+R).
### From GitHub (source / development)
```sh
dsh plugin --profile web add github:chendefine/dsh-sidebar-superdoc-docx
```
For local development, clone, build, and link:
```sh
git clone https://github.com/chendefine/dsh-sidebar-superdoc-docx
cd dsh-sidebar-superdoc-docx
pnpm install
pnpm build # → lib/index.js + lib/client.js + lib/types
```
then, in the profile's `package.json`, point the dependency at the clone and run `pnpm install`
in the profile directory:
```jsonc
{
"dependencies": {
"dsh-sidebar-superdoc-docx": "link:/absolute/path/to/dsh-sidebar-superdoc-docx"
}
}
```
### Plugin config (`cordis.patch.yml` of the profile)
```yaml
- id: dsh-sidebar-superdoc-docx
config:
fileLimitMb: 100 # save-route size cap in MB, default 100
allowOutsideWorkspace: false # allow saving files resolved outside the session cwd, default false
```
## Usage
### Opening a document
Click any `.docx` in the sidebar file tree — it opens in the **DOCX (SuperDoc)** viewer instead of the built-in
code/download viewers.
### Switching the open mode
Under **Settings → Side cards → File preview → DOCX (SuperDoc)**, the gear exposes the "open mode" select:
editing / suggesting / viewing. The choice persists in `pluginSettings['superdoc:docx'].mode` and takes effect
immediately (the editor remounts in the new mode); `viewing` also hides the save button.
### Editing & saving
- The top toolbar is SuperDoc's native one (bold, lists, comments, …) and folds automatically as the pane narrows;
- after edits, the header shows `● Unsaved changes`; click **Save** to export and atomically write back to the
opened path;
- state machine: Saving… → Saved / Save failed (failures carry a reason and can be retried); edits made while a
save is in flight keep the unsaved hint so you can re-save.
### Following outside edits (e.g. an AI agent edited the file)
- **clean** editor: the 3s poll detects the disk change → re-fetches and swaps in place via `replaceFile`,
re-fitting the zoom;
- **dirty** editor: only a "File changed on disk" banner appears — you decide whether to `Reload` (reloading
discards the current unsaved edits).
### Coexistence with other viewers
| Viewer | id | priority |
|---|---|---|
| built-in code viewer | `code` | -100 |
| built-in download viewer | `binary-download` | -50 |
| office preview plugin | `docx` | 0 |
| OnlyOffice plugin | `onlyoffice:docx` | 10 |
| **this plugin** | `superdoc:docx` | 10 |
Ties (e.g. with OnlyOffice) break by registration order. Every viewer can be toggled individually under
Settings → Side cards → File preview without affecting the others.
## Architecture
### Two halves
```
browser (client half, tiny CJS bundle via window.__ModuleLoader__)
└─ ctx.betterSidebar.registerFileViewer('superdoc:docx', exts:['docx'], priority:10, fetchStrategy:'mediaUrl')
└─ SuperDocView: <script src="/sidebar/superdoc/assets/superdoc.min.js"> (global `SuperDoc`)
reads: fetch(/sidebar/file?sessionId=&path=) → Blob → new SuperDoc({ document: blob, contained: true })
saves: superdoc.export({triggerDownload:false}) → Blob → PUT /sidebar/superdoc/save?sessionId=&path=
node (host half, 4 fenced routes)
├─ GET /sidebar/superdoc/info versions / health / cache-bust seed
├─ GET /sidebar/superdoc/assets/<file> superdoc/dist-cdn (closed whitelist)
├─ GET /sidebar/superdoc/engine/dist-cdn/<path> @superdoc/docx-engine/dist-cdn mirror (engine + workers)
└─ PUT /sidebar/superdoc/save raw DOCX bytes → atomic write inside the session cwd
```
- the client half does exactly three things: register the viewer, fetch the file bytes through better-sidebar's
fenced media route, and mount the SuperDoc instance into the sidebar pane;
- the host half runs no document logic — it only **serves assets same-origin** and **fenced saves**;
- vendored versions: `superdoc@2.10.0` + `@superdoc/docx-engine@0.9.0` (per `package.json`; the `info` route
reports the installed versions and doubles as the cache-bust seed).
### Why the engine mirror exists
Before the script loads, the client sets `globalThis.SUPERDOC_ENGINE_CDN_BASE_URL = '/sidebar/superdoc/engine'`,
pointing SuperDoc's engine resolver at this plugin's route. The engine then dynamic-imports
`…/dist-cdn/docx-engine.es.js` and resolves its web workers relative to that **same-origin** URL — browsers
refuse cross-origin worker creation, and otherwise jsdelivr would become a runtime dependency. That is the entire
reason the host half mirrors the `dist-cdn` tree.
### Security boundaries
- **Trust fence** (`src/trust-fence.ts`, behaviorally identical to better-sidebar's): the Host header must be
loopback or a trusted authority from `webRuntime.trustedHosts`; `sec-fetch-site: cross-site` and mismatched
Origins are refused — a DNS-rebinding / cross-site defense, not authentication.
- **Workspace fence** (`src/paths.ts` + the save route): absolute paths only; `isWithin` compares path *segments*
(`/a/bc` is not within `/a/b`); the parent directory is `realpath`-checked to close symlink escapes; `.docx`
only; bodies above `fileLimitMb` get a 413; writes go through tmp + rename atomic replacement.
- **Asset whitelisting** (`src/assets.ts`): the superdoc build exposes a closed whitelist of 3 files; engine
subpaths pass a shape check, reject `.`/`..` segments, and must `realpath` inside `dist-cdn` as regular files.
- **No leakage**: telemetry off by default (`telemetry: { enabled: false }`); the plugin keeps no state on disk.
## Development details & conventions
### Directory layout
```
src/
index.ts host half: build & register the 4 routes (buildRoutes is pure, test-friendly)
assets.ts node_modules asset discovery / whitelists / realpath containment / content types
config.ts config resolution (fileLimitMb, allowOutsideWorkspace; plain TS, zero deps)
paths.ts absolute-path requirement + segment-aware containment + symlink-safe parent realpath
trust-fence.ts browser trust fence (copied, not imported — plugins must not depend on upstream internals)
wire.ts {ok,...} / {ok:false,error:{code,message}} JSON shape + size-capped raw-body reader
client/
index.ts client half: register the superdoc:docx viewer + attach dictionaries
SuperDocView.tsx the editor component (mount / save state machine / disk poll / fit-to-pane zoom)
loader.ts runtime loader (script/stylesheet singletons, engine base URL, contained layout CSS)
settings.ts open-mode read (validated, falls back to editing)
urls.ts URL builders for /sidebar/file and the save route (better-sidebar request contract)
i18n.ts / locales.ts / icons.tsx zh/en dictionaries, registration, icons
tests/ vitest: routes / save-flow / viewers / trust-fence / locales
```
### Build artifacts
- **host**: `lib/index.js`, ESM (es2023), zero runtime dependencies;
- **client**: `lib/client.js` — a CJS bundle registered via `window.__ModuleLoader__.load({ id, factory })`, the
official external client-plugin delivery format (same shape as `dsh-sidebar-onlyoffice` and
`dsh-web-search-aggregation`);
- the SuperDoc editor itself is **not bundled**: the host routes inject it at runtime as a classic `<script>`
(structurally identical to how onlyoffice loads its `api.js`).
### Client purity gate
`tsdown.config.ts` ships a rolldown plugin that fails the build when the client bundle imports any Node builtin
or value-imports `@deepseek-ai/*`; React / react-dom / cordis stay external and come from the host's module
table. The browser half must be self-contained.
### Code conventions
- **No imports of monorepo-internal types**: both halves declare structural context faces (`RouteContext`,
`ClientContextFace`) — external plugins must not reach into the monorepo's Context augmentation graph;
- browser JSON is always `{ok:...}` / `{ok:false,error:{code,message}}` (aligned with better-sidebar's wire
format); error codes: `forbidden` / `method-error` / `bad-request` / `not-found` / `fs-error` / `internal`;
- the viewer id is namespaced (`superdoc:docx`) to avoid clashes with the builtins and `onlyoffice:docx`;
priority 10 beats the builtins; `fetchStrategy: 'mediaUrl'`;
- the zh/en dictionaries must carry exactly the same key set (enforced by the locales test) under the plugin's
single `dshSidebarSuperdoc` namespace;
- every page-level injection is idempotent (stylesheet, layout CSS, editor script are singletons — remount-safe);
- the save route is the **only** fs write surface; better-sidebar's own `fs.write` is UTF-8-text-only, so binary
exports must go through it.
### Tests
`pnpm test` (vitest run) covers:
| File | Coverage |
|---|---|
| `routes.test.ts` | the 4 routes: whitelist hits / traversal & symlink refusals / workspace fence on & off / 413 / 405 / 403 |
| `save-flow.test.ts` | save state machine: hint clears after a successful save, in-flight edits stay unsaved, header buttons position-stable |
| `viewers.test.ts` | viewer contract: id / exts / priority / fetchStrategy / settings row; no id clash with existing viewers |
| `trust-fence.test.ts` | loopback & trusted authorities pass; unknown hosts / cross-site markers / mismatched origins refused |
| `locales.test.ts` | zh/en key parity, non-empty values, unique namespace |
### Common commands
```sh
pnpm typecheck # tsc --noEmit
pnpm test # vitest run
pnpm build # host ESM + client ModuleLoader bundle (purity gate enforced)
```
## Known limitations
- Only `.docx` (SuperDoc does not open legacy `.doc`).
- Closing the tab with unsaved edits cannot be intercepted — watch the `●` unsaved dot.
- If better-sidebar's `workspaceFence` is disabled, files outside the workspace can be *opened* through the media
route, but *saving* them still requires `allowOutsideWorkspace: true` here.
- Fonts: SuperDoc core ships no fonts; documents render with system fonts unless you additionally integrate
`@superdoc-dev/fonts` (not wired up here).
## License
This plugin's code is MIT. It integrates (unmodified, installed by `pnpm install` and served verbatim by its
routes) two SuperDoc components:
| Package | License | Note |
|---|---|---|
| `superdoc` | **AGPL-3.0** | unmodified npm artifact; network-service use triggers AGPL source-offer duties |
| `@superdoc/docx-engine` | **Proprietary** ([DOCX Engine Proprietary License](https://docs.superdoc.dev/resources/docx-engine-license)) | without a commercial agreement, use is limited to SuperDoc-dependent, AGPL-permitted purposes (evaluation/development/testing); commercial use needs a SuperDoc license |
See [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md).
## Credits
- [SuperDoc](https://github.com/superdoc/docx-editor) by Harbour Enterprises — the editor itself.
- [dsh-sidebar-onlyoffice](https://github.com/chendefine/dsh-sidebar-onlyoffice) — the plugin shape (runtime
script injection, trust fence, host routes) this package follows.
Install
dsh plugin --profile web add github:chendefine/dsh-sidebar-superdoc-docx
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-sidebar-superdoc-docx from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.