dsh-drop-any-file · Accept Any File Type in the DeepSeek Harness (DSH) Web Chat
Extend the DeepSeek Harness (DSH) Web UI chat's drag-and-drop so that every file type is accepted — the built-in composer only lets images (PNG/JPG/WebP/GIF) attach inline and rejects everything else. Dropping a non-image file saves it into the active session's workspace so the agent can read and use it. 让 DSH 聊天支持拖拽任意类型文件:非图片文件将保存到当前会话工作区,供 Agent 读取使用。
中文文档: README.zh.md · LLM index: llms.txt · Agent guide: AGENTS.md
Keywords: dsh-plugin · deepseek-harness-plugin · drag-drop · file-upload · workspace · attachment · any-file · 拖拽 · 任意文件
📑 Table of Contents
- ✨ Features
- 🏗️ How it works
- 🚀 Quick start
- ⚙️ Configuration
- ❓ FAQ
- ⚠️ Security notes
- 📦 Project structure
- 🙏 Credits
✨ Features
| Feature | Description |
|---|---|
| 📥 Any file type | Drop .pdf, .zip, .txt, .csv, .bin, … — nothing is rejected with an "only PNG/JPG" error |
| 💾 Saves into the workspace | Each dropped non-image file is written into the active session's workspace directory, where the agent can read/search/use it with its normal file tools |
| 🖼️ Images untouched | Drops that contain only images keep the built-in inline-attach behaviour — nothing changes for what already worked |
| 🚫 Composer rejection bypassed | Capture-phase window listeners intercept the drop before the composer's image-only handler runs, so its rejection toast never fires |
| 🫳 Drop overlay + toast | A themed full-window overlay appears while dragging ("Drop to save to workspace"); a toast confirms what was saved (or why it failed) |
| 🛡️ Name sanitisation + size caps | File names are reduced to a basename and stripped of illegal path characters; payload/file sizes are capped (80 MB body / 60 MB file) |
| 🌗 Theme-aware | Overlay and toast use --dsw-alias-* design tokens; follows light/dark automatically |
| ♨️ Survives restarts | Real profile-bundled plugin: install once with dsh plugin add, auto-loads on every DSH boot — no per-session define, no cordis_define |
🏗️ How it works
User drags a non-image file over the chat window
│
Client bundle (browser) ▼
└─ capture-phase listeners on window: dragenter / dragover / dragleave / drop
(registered from the conversation.input.dock seat, which carries sessionId)
└─ drop contains a non-image file? → preventDefault + stopPropagation
(the composer's image-only handler never sees the event)
└─ FileReader → base64 → fetch POST /drop-any-file/api
body: { sessionId, fileName, base64 }
│
Host half (DSH process) ▼
└─ webServer route POST /drop-any-file/api
└─ resolve target dir: sessions.get(sessionId).header.cwd (the workspace)
└─ sanitise basename → decode base64 → node:fs writeFile
└─ returns { ok, saved: { name, path, bytes } }
│
Client bundle (browser) ▼
└─ toast: "Saved to workspace: <name>" (or the error)
- Pure pull from the browser: no events, no push — the client posts only when the user drops a file; the host answers with JSON (
{ok:false,error}on any failure, never a non-JSON 500). - Image-only drops pass through: the handler returns early without
stopPropagation, so the composer's own inline-attach path still runs for images. - Persistence: ships
dsh.bundle(cordis.patch.yml) +dsh.client(exports["./client"], bundled) so it installs as a real profile plugin that the DSH client-modules scanner loads on every boot.
🚀 Quick start
Standard install: dsh plugin add (persists across restarts)
Install the package from this GitHub repo:
# local directory (from the parent of this repo):
dsh plugin --profile web add ./dsh-drop-any-file
# or directly from GitHub (any DSH machine):
dsh plugin --profile web add github:Zenjibad/dsh-drop-any-file
# or:
dsh plugin --profile web add git+https://github.com/Zenjibad/dsh-drop-any-file.git
dsh plugin add is a pnpm add into the profile plus a dsh.profile.bundles reconcile: seeing this package's dsh.bundle declaration, it appends dsh-drop-any-file to the bundle stack. Restart DSH, then hard-refresh the browser tab (Ctrl+F5). On boot the client-modules scanner resolves exports["./client"] and the drop handler activates. No per-session define, survives restarts.
⚠️ Important: after installing (or updating) a client plugin, a hard page refresh (
Ctrl+F5) is required — the DSH client HMR only hot-swaps already-loaded bundles and does not pull in new bundles into an open tab.
Manual profile mount (alternative)
git clone https://github.com/Zenjibad/dsh-drop-any-file.git(any location).- Add to
~/.dsh/profiles/web/package.jsondependencies:"dsh-drop-any-file": "link:<repo-path>", thenpnpm installin the profile dir. - Restart DSH.
Requirements
- Any DSH web session with an active workspace (the drop target is the current session's
cwd). - No session open → nothing to save into → the plugin is inert (the composer keeps its default behaviour).
⚙️ Configuration
No config file, no persisted settings. Behaviour is fixed by constants in the source:
| Knob | Location | Default |
|---|---|---|
| Max POST body (base64) | MAX_BODY_BYTES in src/index.ts |
80 MB (≈ 60 MB file) |
| Max saved file | MAX_FILE_BYTES in src/index.ts |
60 MB |
| Save target | resolved per drop | sessions.get(sessionId).header.cwd (the session's workspace) |
| Drop interception | src/client/index.tsx |
capture-phase dragenter/dragover/dragleave/drop on window |
| Toast duration | src/client/index.tsx |
6 s |
❓ FAQ
Q: I dropped a file and it still says "Only PNG, JPG, WebP, and GIF images are supported"?
A: The plugin isn't loaded in your current page. Restart DSH (if the host half isn't mounted yet), then hard-refresh the browser tab (Ctrl+F5). New client bundles only appear on a full page reload — the HMR client does not add new bundles to an already-open tab.
Q: I dropped a file but nothing happened / no toast?
A: The drop must happen over the chat window with a session open, and must contain at least one non-image file. If the drop is image-only, the built-in attach path runs instead (by design). Check the browser console for drop-any-file save error entries.
Q: Where does the file go?
A: Into the active session's workspace directory (sessions.get(sessionId).header.cwd) — the same directory the agent works in, so the agent can read it with read/glob/grep etc. The response includes the absolute path in the success toast context.
Q: Does it handle multiple files at once? A: Yes — every non-image file in the drop is saved in order; the toast lists each saved name and counts failures.
Q: Can I drop a file larger than the cap?
A: No — the host rejects files over 60 MB with 413 and the client shows a failure toast. Raise MAX_FILE_BYTES (and MAX_BODY_BYTES) in src/index.ts and rebuild if you need bigger files.
Q: My old dynamic plugin is still active after installing this?
A: This plugin is packaged, not dynamic — there is no dynamic twin. If you previously installed a dynamic drop-handler, stop it (cordis_stop / cordis_undefine) to avoid double interception.
Q: How do I remove it?
A: dsh plugin --profile web rm dsh-drop-any-file (or delete the profile dependency + bundle entry) and restart DSH.
⚠️ Security notes
- Path traversal blocked: the file name is reduced to
path.basename()and every illegal path character (<>:"/\\|?*and control chars) is replaced, so a hostile name can never escape the workspace. - Scoped writes: files are written only into the resolved session's
cwd— never to arbitrary paths. - Size caps: the request body is capped at 80 MB and the decoded file at 60 MB, so the host never buffers unbounded payloads.
- No extra I/O: the plugin performs no network calls, no shell execution, and no disk scanning — only the requested file write.
- Base64 over same-origin HTTP: the file bytes travel as base64 JSON to a same-origin route (the DSH webServer); no third-party endpoint is involved.
📦 Project structure
dsh-drop-any-file/
├── src/
│ ├── index.ts # host half: body read, session-cwd resolve, sanitise, writeFile, /drop-any-file/api route
│ └── client/index.tsx # client bundle: capture-phase drop listeners, overlay, toast, POST to the host route
├── cordis.patch.yml # dsh.bundle patch (inserts the plugin row on boot)
├── tsdown.config.ts # bundles host (node ESM) + client (CJS ModuleLoader)
├── package.json # name, exports["./client"], dsh.client + dsh.bundle
├── lib/ # build output (index.js, client.js)
├── AGENTS.md # repository guide for AI agents
├── llms.txt / llms-full.txt
├── README.md / README.zh.md
└── LICENSE
🙏 Credits
- DeepSeek Harness — the DSH plugin/dynamic runtime, Slots, theme, webServer, client-modules.
- headroom-stats-plugin — reference for the packaged client-plugin build pattern (tsdown host/client split,
cordis.patch.yml,dsh.client). - DSH-better-sidebar — another reference for the packaged client-plugin build pattern.
No comments yet. Be the first to write one.