dsh-ipynb-preview
A DeepSeek Harness plugin that renders Jupyter notebooks in the document preview pane: syntax-highlighted code, real LaTeX, and zoomable figures — instead of raw JSON.
Without it, opening a .ipynb in DSH shows its JSON source. With it, the file
opens as a notebook.
| Feature | Notes |
|---|---|
| Syntax highlighting | Python, JavaScript/TypeScript, JSON, Bash, and INI, with per-language grammars |
| LaTeX math | Vendored KaTeX, so it works offline; inline $…$ and display $$…$$, including inside tables and lists |
| Markdown subset | Headings, lists, tables with column alignment, blockquotes, task lists, fenced code, links, bold and italic, code spans |
| Figure zoom | Click any output figure to open it, then scroll or + / − to zoom continuously, drag to pan, 0 to fit |
| Save figures | Names each figure after the notebook, e.g. sample-notebook-cell3-out1.png |
| Outputs | Streams, display_data, execute_result, and error tracebacks with ANSI escapes stripped |
Everything is self-contained: the KaTeX engine and its web fonts are inlined into the client bundle, so no request leaves your machine.
Install
dsh plugin --profile <your-profile> add dsh-ipynb-preview
Then restart DSH. The plugin is a client-side renderer, so it needs a restart rather than a page refresh: the shell computes its client bundle revision at startup.
To install from a checkout or a git URL instead:
dsh plugin --profile <your-profile> add ./dsh-ipynb-preview
dsh plugin --profile <your-profile> add github:WuShichao/dsh-ipynb-preview
The git form makes pnpm ask for permission to run this package's prepare
script, because pnpm >= 10 refuses a git dependency's install-time script until
it is allowlisted. The built lib/client.js is committed, and the npm package
ships it too, so neither a tarball nor a registry install asks for anything.
How it works
DSH discovers the package through the profile's bundle list, reads dsh.client
from this manifest, and serves lib/client.js to the browser as part of the
client roster. That bundle registers a document-preview renderer for the ipynb
extension:
lib/index.js host half; deliberately empty, the plugin is browser-only
lib/client.template.js the editable client source, with two KaTeX placeholders
lib/client.js the generated artifact the shell serves
cordis.patch.yml the bundle layer that inserts the plugin row
tools/build-client.mjs generates lib/client.js from the template
lib/client.js is generated, not hand-edited. Change lib/client.template.js
and run npm run build.
Development
pnpm install # installs KaTeX (build), React (tests)
pnpm run build # regenerate lib/client.js
pnpm test # build, then the verifier, the bundle lint, and the mount harness
The repository ships a pnpm-lock.yaml, and pnpm is what the DSH CLI itself uses
to install plugins, so CI installs with pnpm install --frozen-lockfile. npm and
yarn also work: every script is a plain node invocation rather than a chained
npm run, so no script depends on which package manager started it.
pnpm test runs three suites, all offline:
tests/verify.mjs— materializes the bundle the way the shell's module loader does, then checks the manifest contract, the registrations, the highlighter, the math renderer, the Markdown subset, the zoom arithmetic, the stylesheet, and a full render of the fixture notebook. Around 170 assertions.tests/lint-bundle.mjs— sweeps both the template and the built bundle for identifiers that are referenced but never declared. A free variable makes the factory throw while the shell materializes it, and the whole client roster then fails to load — a blank window thatnode --checkcannot detect.tests/mount.mjs— mounts the plugin against a DOM stub so effects and event wiring actually run, and asserts the notebook tree survives opening the lightbox.
The fixture notebook is generated by tests/make-fixture.mjs, and the assertions
read their expected counts from that fixture, so the input and the expectation
cannot drift apart.
Interaction testing in a real browser
React's synthetic event system does not work against a hand-written DOM stub, so the controls' click handlers are verified in a real browser instead:
node tests/make-page.mjs # writes test-page.html at the repo root
node tests/make-page.mjs out.html # or choose the output path
Open the page, click Open the lightbox, and press every control; each
interaction, the zoom readout, and any error is written to the log at the top of
the page. Append ?auto=1 to the URL and the page drives itself, which is how a
headless run reads the result back:
msedge --headless=new --virtual-time-budget=15000 --dump-dom \
"file:///path/to/test-page.html?auto=1"
That page also hit-tests every control with document.elementFromPoint, which is
how a stacking-order bug was found: a fixed application header with a higher
z-index than the lightbox covered the middle of the toolbar, so only its edges
responded to a click.
Requirements
- Node 18 or newer to build and test.
- React is not a dependency: the shell supplies it from its own frozen module table. It is a dev dependency only, for the tests.
Notes for plugin authors
Two things this plugin learned the hard way, both now covered by assertions:
- A client bundle is materialized inside the shell's module loader. If the factory throws, the row has no exports and the whole roster fails — which looks like a blank window rather than a plugin error. Keep the factory free of undeclared identifiers.
- A full-screen overlay needs a
z-indexabove every host layer.9999is not enough: application chrome is often a fixed header with its own stacking context, and it will silently swallow clicks aimed at your controls.
License
MIT — see LICENSE. KaTeX is bundled under its own MIT license; see THIRD_PARTY_NOTICES.md.
Published to npm by @nightwingng.
No comments yet. Be the first to write one.