# dsh-drop-any-file · Accept Any File Type in the DeepSeek Harness (DSH) Web Chat > Extend the [DeepSeek Harness](https://github.com/deepseek-ai/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](README.zh.md) · LLM index: [llms.txt](llms.txt) · Agent guide: [AGENTS.md](AGENTS.md) ![dsh-plugin](https://img.shields.io/badge/dsh--plugin-ready-4c8dff) ![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-✓-0f1115) ![license](https://img.shields.io/badge/license-MIT-green) ![install](https://img.shields.io/badge/dsh%20plugin%20add-✓-22c55e) **Keywords**: `dsh-plugin` · `deepseek-harness-plugin` · drag-drop · file-upload · workspace · attachment · any-file · 拖拽 · 任意文件 ## Repo GitHub: [Zenjibad/dsh-drop-any-file](https://github.com/Zenjibad/dsh-drop-any-file) · Requires DSH ≥ 0.1 with the web profile. --- ## 📑 Table of Contents - [✨ Features](#-features) - [🏗️ How it works](#️-how-it-works) - [🚀 Quick start](#-quick-start) - [⚙️ Configuration](#️-configuration) - [❓ FAQ](#-faq) - [⚠️ Security notes](#️-security-notes) - [📦 Project structure](#-project-structure) - [🙏 Credits](#-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: " (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: ```bash # 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) 1. `git clone https://github.com/Zenjibad/dsh-drop-any-file.git` (any location). 2. Add to `~/.dsh/profiles/web/package.json` `dependencies`: `"dsh-drop-any-file": "link:"`, then `pnpm install` in the profile dir. 3. 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](https://github.com/deepseek-ai/deepseek-harness) — the DSH plugin/dynamic runtime, Slots, theme, webServer, client-modules. - [headroom-stats-plugin](https://github.com/Zenjibad/headroom-stats-plugin) — reference for the packaged client-plugin build pattern (tsdown host/client split, `cordis.patch.yml`, `dsh.client`). - [DSH-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) — another reference for the packaged client-plugin build pattern. ## 📄 License [MIT](LICENSE)