# 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)