npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-image-preview

v0.2.1

Published

Inline previews for the images an agent already produced in DeepSeek Harness: read_image attachments and local paths in any format (HEIC/TIFF/PSD/SVG/BMP/ICO/TGA/PNM/JP2 …), rendered through the official gallery and lightbox.

Readme

dsh-image-preview

npm license

Inline previews for the images your agent already produced — in the message flow, in any format.

The problem

An image the agent reads never reaches the Chat flow. read_image (including through run_code) delivers its result as a synthetic user/message whose source.kind is not "user", and @deepseek-ai/dsh-client-ui-chat classifies every such message as a context node:

if (event.data.source.kind !== "user") return { kind: "context", ... }

ContextInjectionRow has no image seat, so the image block is dropped and only its descriptor text survives:

<path>/tmp/shot.png</path><type>image</type><content>image/png image, 840x2289 px, 972428 bytes</content>

Your own pasted images are fine (source.kind === "user" → UserMessageNodeView → thumbnails + lightbox). Only the agent's are lost. And because read_image admits only PNG/JPEG/WebP/GIF, a HEIC, TIFF, PSD, SVG, ICO, TGA or Netpbm file never becomes an attachment at all — it can only ever appear as a path.

What this plugin does

| Case | Seat | Bytes | |---|---|---| | Agent image attachment (read_image) | new agent-image chat node | already in the durable attachment store; no extra I/O | | Message that only names an image file | new agent-image-path chat node | host routes /image-preview/meta + /image-preview/file |

Both render through renderMessageImages — the seat ChatView passes to every keyed node renderer — so the thumbnail sizing, the load/retry state and the full-size lightbox are the official ui-attachment ones. No image UI is reimplemented, and no official component is overridden: conversation Definitions are not exclusive, so these rows are added beside the existing text rows.

Refresh the page and past sessions light up too: the projection is rebuilt from the session log.

Screenshots

Every format, one host route — each thumbnail below is the real /image-preview/file response rendered in a browser (native formats byte-for-byte, the rest transcoded to PNG):

formats

Pixel fidelity of the same round trip, decoded and compared against the source (see Measured fidelity):

pixel fidelity

Formats

Served byte-for-byte (the browser renders them): PNG · JPEG · GIF · WebP · AVIF · BMP · ICO/CUR · SVG

Transcoded to PNG on the host: HEIC/HEIF · TIFF · PSD · JP2/J2K · TGA · Netpbm (PBM/PGM/PPM/PAM) · EXR · DDS · JXL

The chain is sips (macOS, always present) → magick → convert → ffmpeg, plus built-in zero-dependency TGA and Netpbm decoders so those two keep working on a machine with none of them. Results are cached under $DSH_HOME/plugins-cache/image-preview, keyed by path + mtime + size, pruned at 240 entries. A format with no available transcoder answers no-transcoder and renders nothing rather than a broken frame.

Measured fidelity

test/visual.test.mjs builds one source pattern (pure R/G/B/Y bands, a diagonal, a checker), converts it to every format, fetches it back through the real route over HTTP, decodes the served payload and compares it to the source. Mean absolute error per channel (0 = identical), from this machine:

| Format | Served as | MAE | | |---|---|---|---| | PNG · GIF · BMP | native | 0 | lossless | | TIFF · PSD · TGA · Netpbm | PNG | 0 | lossless transcode | | EXR | PNG | 0.08 | HDR round trip | | JP2 | PNG | 3.32 | lossy codec | | JPEG | native | 7.30 | lossy codec | | HEIC | PNG | 7.55 | lossy codec |

Every format is also compared against a vertically flipped and a red/blue-swapped copy of the source; in each case the straight comparison wins by 15–40×, which is what rules out the two mistakes a hand-written decoder actually makes. The built-in TGA decoder (bottom-left origin and BGR order) and the Netpbm decoder are measured separately at MAE 0, because the chain always reaches sips first on macOS and would otherwise hide them.

Not verified here: DDS (this macOS build's sips writes a zero-byte file, so no fixture could be produced — the suite reports it as skipped) and JXL (sips --formats lists it readable but not writable). PDF was removed in 0.2.0: a document's rasterization DPI is a choice, not a property, and a measured round trip came back at MAE 34.

Security model

The browser cannot read local disk; this route can, so it refuses by default:

  • the resolved realpath must sit under an allowed root — every workspace the host knows about (ctx.workspaceRegistry), the OS temp dirs, $DSH_HOME, plus any allowRoots you configure. Symlinks are resolved before the check, so a link out of the workspace does not escape it.
  • the extension must be in the allowlist and the leading bytes must sniff as a real image — a shell script named .png is refused (not-an-image).
  • a relative path is only resolved when the client supplies the conversation cwd.
  • maxBytes (default 128 MiB) caps the source file; responses carry x-content-type-options: nosniff and content-disposition: inline.
  • cross-site requests are refused. Plugin routes are mounted outside dsh web's cookie gate — @liustack/modlens's paste route answers an unauthenticated request too — so this route does not lean on it. The server binds loopback, which leaves one reachable attacker: a web page in your browser pointing an <img> at the route. It cannot read the pixels (tainted canvas), but load/error timing would let it probe which files exist, so a request labelled cross-site by Fetch Metadata is rejected. A caller sending no Fetch Metadata (curl, a local script) is served: it can already read those files directly.

Install

# from this repository
dsh plugin --profile web add github:tianxia--/Dsh-ImageProview

# or from npm, once published
dsh plugin --profile web add dsh-image-preview

# or from a local checkout
dsh plugin --profile web add link:/path/to/Dsh-ImageProview

Or wire it by hand:

# ~/.dsh/profiles/web/cordis.patch.yml
- insert:
    - id: image-preview
      name: dsh-image-preview
      config:
        maxBytes: 134217728      # optional, default 128 MiB
        allowRoots: []           # optional extra roots
        transcode: true          # false = native formats only

The host half mounts the routes; the browser half (lib/client.js) is hand-written in the lazy-CJS client plugin protocol — no build step, no dependencies.

Compatibility

The plugin imports no dsh package — it reaches dsh through cordis services — so its dependencies are declared by hand in peerDependencies, the only field dsh's compatibility check reads:

| dsh | Status | |---|---| | 0.1.x | compatible (developed on 0.1.2-alpha.2) | | 0.2.x | compatible (verified on 0.2.0-rc.2) | | 0.3.0-rc.1 and later | flagged by dsh until re-verified |

After a dsh upgrade, check the 13 runtime contracts against the installed dsh before widening the range:

npm run check:dsh

Verify

npm test   # 164 checks: 50 host-route, 15 real-HTTP, 46 client-bundle, 53 pixel-fidelity

The host suite generates 13 fixtures (via sips where available) and drives the real handlers with fake req/res pairs; the client suite loads the real bundle with a stubbed module loader and drives both Definitions with event shapes copied out of a durable session log.

Known limits

  • The text row stays. The image row is added next to the original <path> context row rather than replacing it: overriding the official context key would mean reimplementing ContextInjectionRow (provenance chips, collapse state) for no visual gain.
  • Tool results are not scanned. A path printed by bash inside a tool result is not previewed; only user/assistant message text and attachments are.
  • The lightbox is the official one — fit-to-viewport, no zoom or pan. If you want a zooming viewer with its own fence syntax, img-preview covers that ground and the two coexist.
  • Transcoding depends on the host machine. Without sips/ImageMagick/ffmpeg, only the built-in TGA and Netpbm decoders remain.

License

MIT