Skip to content
dsh.fish
Bundle

dsh-workspace-upload

A workspace file manager plugin for the dsh web GUI: browse, upload, download, rename, create and delete files in the session workspace.

Source
LI-Huaa
stars
1 stars
License
MIT
Updated
Updated yesterday

Readme

# dsh-workspace-upload

English | [中文](README-zh.md)

A workspace file-manager plugin for the [dsh](https://github.com/deepseek-ai/deepseek-harness)
Web profile — browse, upload, download, rename, create and delete files in the
session's workspace, right from the chat UI.

## Features

* A **文件 (Files) button** in the composer's left controls opens a file-manager
  dialog over the app:
  * browse the workspace and its subdirectories (breadcrumb navigation, up /
    refresh);
  * **upload** — multi-file, chunked, any size (640 KiB chunks, adaptive shrink
    under proxy body limits);
  * **download** files;
  * **rename** files and folders;
  * **create folders**;
  * **delete** files and folders (two-step confirm).
* Every operation is scoped to the workspace root: host-side path containment
  rejects any `..` or absolute-path escape.
* The workspace resolves to the session's workspace (session → cwd → first
  registered workspace).

## Directory structure

```
dsh-workspace-upload/
├── cordis.patch.yml      # profile patch layer: inserts the plugin row
├── package.json          # dsh.bundle / dsh.client declarations, exports
├── lib/
│   ├── index.js          # host half: /api/workspace-upload file-manager route
│   └── client.js         # client half: browser bundle (button + dialog)
├── test/
│   ├── protocol.mjs      # host protocol tests (real handler, no dsh needed)
│   └── simulate.mjs      # client kernel simulation (slot registration fake)
├── LICENSE               # MIT
├── .gitignore
├── README.md             # English
└── README-zh.md          # 中文
```

## Architecture

One package, one profile row, two halves:

| Half | File | Role |
| --- | --- | --- |
| Host | `lib/index.js` | Registers `GET/POST /api/workspace-upload` on the dsh web server: list / rename / mkdir / delete / download / chunked upload, all with workspace containment. |
| Client | `lib/client.js` | Browser bundle (`window.__ModuleLoader__.load`): the trigger button in `conversation.input.left`; the dialog renders from the button itself (plain local state, `position: fixed` overlay). |

Wiring:

* `package.json` → `dsh.bundle.patch` points at `cordis.patch.yml` (the profile
  layer that inserts the row) and `dsh.client.platform: "web"` marks the
  package as a browser-roster entry whose bundle ships via `exports["./client"]`.
* `cordis.patch.yml` inserts one row, `workspace-upload`, into the web
  composition.

## API

```
GET  /api/workspace-upload
       → { "workspace": "<resolved workspace dir>" }        (no params)
       → file download with attachment headers              (?sessionId&path&name)
```

File-manager modes (JSON POST; `path` is a workspace-relative directory
`""` / `"sub"` / `"sub/deep"`, `name` is always one path segment):

```
{ "mode": "list",   "sessionId"?, "path"? }                    → { workspace, path, entries:[{name,type,size,mtime}] }
{ "mode": "rename", "sessionId"?, "path"?, "name", "newName" } → { ok, from, to }
{ "mode": "mkdir",  "sessionId"?, "path"?, "name" }            → { ok, path }
{ "mode": "delete", "sessionId"?, "path"?, "name" }            → { ok, deleted }
```

Chunked upload (any file size; the GUI sends 640 KiB chunks — base64 bodies
~853 KiB, under the nginx default `client_max_body_size` of 1 MiB; on a bare
`413` the client halves the chunk size down to 64 KiB and retries):

```
{ "mode": "chunk",  "sessionId"?, "path"?, "transferId", "name", "offset", "data", "total" } → { received }
{ "mode": "finish", "sessionId"?, "path"?, "transferId", "name", "total", "overwrite"? }     → { status, path, bytes }
{ "mode": "abort",  "transferId" }                                                            → { aborted }
```

Chunks are appended to a hidden `.dsh-upload-<transferId>` temp file inside the
target directory and renamed on finish. Chunks must arrive in order from
offset 0; a repeated chunk at an already-received offset is answered
idempotently (safe client retry). Orphaned transfers are swept after
30 minutes.

A legacy single-shot batch mode is kept for API/curl compatibility:

```
{ "sessionId"?, "files": [{ "name", "data": "<base64>", "overwrite"? }] }
  → { "workspace": string, "results": [{ name, path?, status, bytes?, error? }] }
```

Limits: 32 MiB per request (one chunk + overhead), 8 MiB decoded per chunk.
`overwrite: true` replaces an existing file; otherwise the file is skipped with
`status: "skipped"`.

## Install

The package installs as a dsh profile bundle. From the checkout that owns the
package directory:

```sh
dsh plugin --profile web add ./dsh-workspace-upload
# or from a clone:  dsh plugin --profile web add /path/to/dsh-workspace-upload
```

Then restart the web profile (`dsh web ...`) so the loader picks up the new
row, and refresh the browser. Removing the plugin:

```sh
dsh plugin --profile web remove dsh-workspace-upload
```

## Development

Both tests run without a dsh instance (they exercise the real host handler and
client bundle factory directly):

```sh
node test/protocol.mjs   # host protocol: list/rename/mkdir/delete/download/chunked upload
node test/simulate.mjs   # client kernel: slot registration against a faithful slots fake
```

## Security notes

* All `path` / `name` values are sanitized and containment-checked on the host;
  uploads use hidden temp files that never escape the target directory.
* The route inherits the dsh web server's bind (localhost by default); when
  exposing the GUI remotely, put it behind the same TLS/Basic-Auth reverse
  proxy as the rest of the app (and raise `client_max_body_size` if you want
  fewer, larger chunks).

Install

dsh plugin --profile web add github:LI-Huaa/dsh-workspace-upload

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source