Skip to content
dsh.fish
Bundle

dsh-drop-any-file

Extend the DeepSeek Harness (DSH) web chat drag-and-drop to accept every file type: dropping any file in the composer saves it into the current session's workspace so the agent can read and use it.

Source
Zenjibad
stars
1 stars
License
MIT
Updated
Updated yesterday

Readme

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

```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:<repo-path>"`, 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)

Install

dsh plugin --profile web add github:Zenjibad/dsh-drop-any-file

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source