All-in-one inline media previewer for mip-img/mip-video/mip-audio fences with streaming playback, ffmpeg transcoding fallback, lightbox, and trust-fenced LAN/mobile access.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:seeingrain/dsh-media-inline-preview
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
🇨🇳 中文: README.md
The all-in-one media viewer for DeepSeek Harness (DSH) chat — one plugin, three fences (```mip-img / ```mip-video / ```mip-audio), covering 36 local media formats. Images render as cards with a full-screen lightbox, video/audio render as native players — all inline inside chat messages: no sidebar, no new tabs, mounting even while the message is still streaming. Merges and supersedes the separate img-preview (image fence) and video/audio fence plugins.
📱 Preview
| Image cards (JPG / PNG / SVG / TGA…) | Animated GIF + vector SVG inline | Native MP4 / MP3 players |
|---|---|---|
![]() |
![]() |
![]() |
Supported formats (36)
| Category | Formats | Notes |
|---|---|---|
| Images (10) | .png .jpg .jpeg .webp .gif .avif .bmp .svg .ico .tga |
Cards + lightbox; .tga auto-converted to PNG host-side (pure-JS decoder: uncompressed/RLE, 8/15/16/24/32-bit, palette); GIF plays inline as-is |
| Video · native (7) | .mp4 .webm .mov .m4v .ogv .ts .3gp |
Browser-native, HTTP Range streaming |
| Audio · native (9) | .mp3 .wav .m4a .aac .ogg .oga .flac .opus .weba |
Native <audio> players |
| Video · transcode (6) | .mkv .avi .flv .wmv .mpg .mpeg |
On playback failure the client auto-retries with ?tc=1; host transcodes via ffmpeg to H.264/AAC mp4 |
| Audio · transcode (4) | .wma .mid .midi .m4b |
Same fallback; transcode results cached content-addressed for 7 days |
Features
- Images (
img) — thumbnail cards (caption shows only the file name), single-click fullscreen lightbox (desktop: wheel zoom / drag pan / double-click toggle 100%↔2.5x / ESC close; mobile: two-finger pinch zoom (around the midpoint) + two-finger drag pan, single finger drags too; top toolbar with −/+/fit/100%/✕);{"images":[...]}grids; externalurlpassthrough. - Video / audio (
video/audio) — native<video>/<audio>players rendered right in the message (seek, fullscreen,playsinlineon mobile), file-name caption; no sidebar needed. - HTTP Range streaming (media route):
createReadStreampiping with full 206/416 handling — large files seek smoothly with constant memory; 2 GiB cap. - Transcode fallback: containers browsers cannot play (mkv/avi/flv/wmv/wma…) trigger an automatic client retry with
?tc=1; the host transcodes via ffmpeg and caches to~/.dsh/media-cache/(content-addressed, 7-day TTL). - Bandwidth-friendly: ETag /
If-None-Matchconditional GET on both routes (unchanged files revalidate with a 304 and zero transfer); TGA→PNG and ffmpeg transcode results disk-cached; players lazy-load via IntersectionObserver until scrolled into view. - Fence syntax persists — the fence is just Markdown, so session history keeps it natively; reloading never loses media.
- Streaming early mount — once a fence inside a still-streaming message is settled (full JSON spec + a following sibling proves closure), it mounts and plays immediately instead of waiting for the message to finish.
- Zero configuration — after install, when media files appear or are produced in a session the agent calls
mip_img/mip_mediafirst and emits the fence; cards/players show up right in the message. Hand-written fences work too (below).
Fence syntax
{"path":"/absolute/path/to/image.png","label":"optional"}
{"path":"/absolute/path/to/video.mp4","label":"optional"}
{"path":"/absolute/path/to/music.mp3","label":"optional"}
The fence tags are namespaced (
mip-*) since v1.4.0 — a breaking change. Generic words likeimg/video/audioare claimed by other plugins too (the third-party img-preview plugin ships ```img itself), and nothing arbitrates a fence tag: DSH core has no fence registry, every client plugin scans the DOM on its own, so two claimants both mount and a single fence shows two figures. Hencemip-img/mip-video/mip-audio; the old bare tags are no longer recognized and stay plain code blocks.
The label field is still parsed (hand-written fence compatibility), but the UI caption always shows the file name. Multi-image grid: {"images":[{"path":"..."},{"path":"..."}]}.
Architecture (house pattern)
host (
lib/index.js) —systemPrompt.sectioninjects the unified fence teaching (three fences + active triggering + dedup);mip_img/mip_mediatools validate paths and return{ok, ..., path, url}; routes:/plugins/dsh-media-inline-preview/files— image route (whole-read, 128 MiB cap, TGA→PNG on the fly with disk cache,ACAO *)/plugins/dsh-media-inline-preview/media— media route (Range streaming, 2 GiB cap,?tc=1transcode fallback)/plugins/dsh-media-inline-preview/token+__media_auth— file-scoped HMAC tokens (?dsh_tok=): a credential bound to a single file's realpath, verified as an nginxauth_requesttarget, so cookieless system media pipelines (e.g. Android'sstagefright, which fetches media in its own process without the browser's cookies) can stream through a cookie-gated reverse proxy; the secret is auto-generated per deployment (0600, outside the workspace) and rotation is live (a rotated secret self-heals via a forced re-mint on the failure retry).
client (
lib/client.js) —window.__ModuleLoader__registration; incremental MutationObserver scan (rAF-coalesced, plus a 3s full-pass fallback) with three-tier collision-free detection:div.infostringtext exactlymip-img/mip-video/mip-audio(primary; empty while streaming so half-streamed fences never match)- Text opens with
```mip-img/```mip-video/```mip-audio(surface fallback) - JSON spec kind inferred from the
pathextension (disjoint extension sets — a bare JSON body never cross-triggers another media kind)
Nested
preinside a wrapper is skipped (single mount target, no double-mount); playback failure auto-retries?tc=1, a second failure swaps in an error card.Security (see DESIGN.md):
- Both routes pass a browser trust fence on every request: Host must be loopback or one of
webRuntime.trustedHosts(LAN IPs sampled at boot +--trusted-hostentries),sec-fetch-site: cross-siterefused, a present Origin must match the Host — LAN/mobile GUI access keeps working while cross-site fetches are blocked (stricter than the original img-preview). - Workspace containment re-checked per request,
realpathcloses symlink escapes (including the missing-file fallback), extension whitelist only, 128 MiB / 2 GiB caps.
- Both routes pass a browser trust fence on every request: Host must be loopback or one of
Usage
After installation nothing to configure: when images/videos/audio appear or are produced in a session, the agent calls mip_img / mip_media first, then emits the fence — cards/players show up right in the message. You can also hand-write the fences above.
Tests
npm test # host + cache + client smoke (all suites)
node test/host.mjs # host unit: trust fence / path escape / TGA→PNG / type mapping
node test/cache.mjs # cache: ETag/304 conditional GET + TGA disk cache
node test/client-smoke.mjs # stubbed-DOM smoke: detection / cross-kind / mount / lazy-load / tokens
Installation
dsh plugin --profile web add github:seeingrain/dsh-media-inline-preview
# host half needs a web restart; client half loads on the next page refresh
License
MIT — see LICENSE.
Links
More in this category
zhu1090093659/dsh-web#packages/dsh-task-board★ 8978
Task board for the dsh web GUI: a sidebar multi-column kanban whose cards run in real DSH agent sessions and can also be scheduled with cron expressions, executed host-side even with the browser closed.
zhu1090093659/dsh-web#packages/dsh-web-all★ 8978
Plugin and skin collection for the DSH Web UI: task board, Git graph, right-side panel, remote mobile UI, pet, live token stats, and a skin center.
MeteorNOX/DeepSeek-Balance-Whale-Widget★ 5292
A fixed-corner whale widget for the DSH web GUI — balance, today's usage and per-turn cost with peak/off-peak pricing, editable balance-alert and daily-budget bubbles, a module-based custom bubble queue with A/B weighted choices and random lines or images, 30+ vendor templates (OpenAI, OpenRouter, Kimi, SiliconFlow, Ark, Zhipu, MiniMax and more) with per-model balance and subscription quota, plus task-end sound, imported audio, custom roles and a resource manager. Local-only, no telemetry.
ccch1mneyyy/dsh-TUI★ 4819
Claude Code-style full-screen terminal UI: pixel-whale header, live status line, and streaming thought expansion.
omdsh-dev/DSH-better-sidebar★ 4290
Full sidebar workbench with file rendering and editing, terminal, Git, and subagents; third-party plugins can register new tabs.
Devin-AXIS/deepseek-design#deepseek-idesign★ 1478
Visual design studio for websites, app prototypes, posters, cards, reports, and magazines, with templates, direct element editing, selection-aware AI draft handoff, and export.



Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.