# dsh-plugin-show-image
[δΈζ](README.md) | English
An inline image rendering plugin for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness):
the conversation agent calls the `show_image` tool to **display local images inline** in the
browser session UI. Image bytes travel through an on-demand HTTP route β they never enter the
session log or the model context. Lightweight and zero-bloat.
## Features
- πΌ **Inline image rendering**: display local images directly in the conversation flow without
leaving the session UI
- π **Click to enlarge**: click the thumbnail to open a full-viewport lightbox overlay; click
anywhere or press Escape to close
- π **On-demand HTTP delivery**: image bytes flow through a dedicated HTTP route
(`GET /show-image?path=β¦`) β no base64 encoding, no session log bloat
- π¦ **Zero dependencies, zero build**: hand-written plain JavaScript β no npm dependencies, no
build step, no TypeScript
- π **Safety guardrails**: 8MB file size cap, extension allowlist (prototype-chain safe), and
unsupported formats are rejected outright
- π§Ή **Fully reversible**: every side effect (tool registration, HTTP route, card renderer) is
bound to the plugin fiber and cleaned up on uninstall
## Supported formats
`png` Β· `jpg` / `jpeg` Β· `gif` Β· `webp` Β· `svg` Β· `bmp` Β· `avif` Β· `ico`
## Installation
Prerequisites: [DSH](https://github.com/deepseek-ai/deepseek-harness) installed and `pnpm` on
PATH.
```sh
# Install from GitHub (no build step, so no allowBuilds configuration is needed)
dsh plugin --profile web add github:justhalfbit/dsh-plugin-show-image
# Restart dsh web to take effect
```
`web` is the profile name behind `dsh web` (the browser UI).
`dsh plugin add` writes the dependency into the profile and appends it to
`dsh.profile.bundles` automatically β no manual editing.
Uninstall with `dsh plugin --profile web remove dsh-plugin-show-image` and restart.
For local development: clone, then
`dsh plugin --profile web add /absolute/path/dsh-plugin-show-image`.
> Compatibility: developed against DSH `0.1.1-rc.x`; upstream APIs may still move during rc.
### Interface support
| Runtime | Tool + route (Host half) | Card rendering (Client half) |
|---|---|---|
| `dsh web` (browser GUI) | β
| β
|
| `tui` / `headless` | β depends on `webServer`; the row stays inactive without it | β |
This is by design: registering an image tool in a terminal that cannot render images would only
mislead the model.
## Usage
After installation and restart, every session (any preset) automatically gains the `show_image`
tool. Just tell the agent:
```
Show me /path/to/image.png
```
The agent calls `show_image`, and the image renders inline in the conversation card. Click the
image to view it full-screen.
> **Tip**: prefer absolute paths. Relative paths resolve against the DSH process working
> directory, not the session working directory.
## How it works
### Architecture
The plugin consists of two halves, loosely coupled through an HTTP route:
```
ββ Host half (lib/index.js) ββββββββββββββββββββββββββββββ
β β
β show_image tool β
β model calls β execute() β fs.readBytes() β
β β returns {path, mime, bytes} metadata (no pixels) β
β β
β /show-image route β
β browser GET β handler() β serves image bytes β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP (image bytes on demand)
ββ Client half (lib/client.js) βββββββββββββββββββββββββββ
β β
β ImageCard component β
β registered at tool.call.toolview[key="show_image"] β
β
inline rendering β
β click β DOM lightbox overlay (mounted on body) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
### Full call chain
1. **Model decision**: the agent loop encodes the `show_image` tool schema into the request;
the model decides to call it and returns `{path}`
2. **Tool execution**: the Host `execute()` reads file metadata via the `fs` service and
returns `{path, mime, bytes}` β image bytes **never enter the log**
3. **Card rendering**: session events sync to the browser; React renders `ImageCard`, which
outputs `
`
4. **On-demand fetch**: the browser `
` tag fires an HTTP GET that hits the `/show-image`
route; the handler serves back the image bytes
The three callers (agent loop, React renderer, HTTP server) are unaware of each other; they
connect through the session log and the URL.
### Lightbox implementation
When the thumbnail is clicked, a `useEffect` appends a plain DOM overlay directly to
`document.body` (instead of rendering inside the React tree), bypassing any ancestor CSS
(`transform` / `filter` / `overflow`) that would clip a `position: fixed` element. Page scroll
is locked (`overflow: hidden`), the Escape key closes the overlay, and all side effects are
cleaned up on close or component unmount.
## File structure
```
dsh-plugin-show-image/
βββ package.json # Package manifest: dsh.bundle.patch + dsh.client + exports
βββ cordis.patch.yml # Composition patch: one insert row into the host tree
βββ lib/
β βββ index.js # Host half: show_image tool + /show-image HTTP route
β βββ client.js # Client half: ImageCard component + lightbox + slot registration
βββ LICENSE # MIT
βββ README.md
```
## Design decisions
**Why an HTTP route instead of base64 RPC?**
Base64 encoding inflates the payload by 33%, and the entire blob must pass through JSON
serialization/deserialization. An HTTP route lets the browser fetch raw bytes directly β
smaller and faster.
**Why the host plane?**
`show_image` should be visible to every session (a global tool), not limited to one preset.
`tools.register()` on the host plane registers a global tool visible to all sessions regardless
of preset. The `webServer` service is also only available on the host plane.
**Why does the row stay inactive when `webServer` is missing?**
`inject: ['tools', 'fs', 'webServer']` declares a hard dependency. In TUI/headless profiles
where `webServer` does not exist, Cordis puts the row in a permanent waiting state β this is
more honest than registering a broken tool that pretends it can render images.
**Why native DOM instead of React for the lightbox?**
React-rendered elements are subject to ancestor CSS properties in the DOM tree. `position:
fixed` is downgraded to relative positioning by ancestors with `transform`, `filter`, etc.
A DOM element mounted directly on `document.body` is immune to these constraints, guaranteeing
the lightbox always covers the full viewport.
## Known limitations
- Only available in the `dsh web` (browser GUI) profile; not functional in TUI / headless
environments
- Relative paths resolve against the DSH process working directory, not the session working
directory (the tool description instructs the model to prefer absolute paths)
- Any local process can read local image files through the GUI port β consistent with DSH's
existing local trust model
## Development
This plugin has zero dependencies and zero build steps. Edit the source, restart `dsh web`,
and the changes take effect (local-linked installs do not need `dsh plugin add` again).
## License
[MIT](LICENSE)