Skip to content
dsh.fish
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

  • 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.
Source