# Terminal: File Drag-and-Drop

**Page:** api/terminal/file-drop

[Download Raw Markdown](./api/terminal/file-drop.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



# Terminal: File Drag-and-Drop

The drag-and-drop endpoints inject a file (or a tree of files) into a running terminal session as a single OSC 8472 escape frame, the way a graphical terminal emulator would interpret a real OS drag-and-drop event. All four endpoints are scoped to a single terminal session and require that session to be a PTY session; non-PTY sessions are rejected with `409`.

There are two ways to drop a file, and choosing the right one matters:

- **One-shot drop** — `POST /api/v1/terminal/drop` performs begin, stage, and commit in a single request. Use this for small files that fit comfortably in one JSON body.
- **Staged transaction** — for large files, or several files dropped together, run the three-step sequence in order: begin, upload (one or more raw-slice uploads), then commit.

The drop only becomes visible to the program running in the terminal at the commit step (or at the end of the one-shot call). Committing is what injects the OSC escape frame that the terminal interprets as a drag-and-drop event. An uncommitted transaction stages bytes and changes nothing on screen.

The base URL for all endpoints is the per-container terminal subdomain:

```
https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.icu
```

---

## One-shot drop

### `POST /api/v1/terminal/drop`

Stages a small drop inline and commits it in a single request through the same server code paths as `begin` / `upload` / `commit`. The `items` array carries each file's base64 content inline, or a nested directory tree via `{name, dir: true, items: [...]}` entries. PTY sessions only; this endpoint never blocks.

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID (numeric `1`–`65535`) |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `ctx` | string | Yes | Drop context: `"drop"` or `"paste"` |
| `r` | integer | No | Drop cell row |
| `c` | integer | No | Drop cell column |
| `items` | array | Yes | File/dir items (`[{name, b64}]` for files, `{name, dir: true, items: [...]}` for nested directories) |



```bash
curl -X POST \
  "https://proj-acme-ctr-7f3a-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/drop?terminal_id=12" \
  -H "Content-Type: application/json" \
  -d '{
    "ctx": "drop",
    "r": 4,
    "c": 12,
    "items": [
      {
        "name": "README.md",
        "b64": "IyBSZWFkbWUKClRoaXMgaXMgYSBzYW1wbGUgZHJhZy1hbmQtZHJvcCBmaWxlLg=="
      },
      {
        "name": "src",
        "dir": true,
        "items": [
          {
            "name": "index.js",
            "b64": "Y29uc29sZS5sb2coJ2hlbGxvJyk7Cg=="
          }
        ]
      }
    ]
  }'
```


```ts
await client.terminal.terminalDragAndDrop.oneShotTerminalDrop(
  {
    ctx: "drop",
    r: 4,
    c: 12,
    items: [
      { name: "README.md", b64: "IyBSZWFkbWUKClRoaXMgaXMgYSBzYW1wbGUgZHJhZy1hbmQtZHJvcCBmaWxlLg==" },
      {
        name: "src",
        dir: true,
        items: [
          { name: "index.js", b64: "Y29uc29sZS5sb2coJ2hlbGxvJyk7Cg==" }
        ]
      }
    ]
  },
  { terminal_id: "12" }
);
```



#### Responses



Drop sealed and frame injected.
```json
{}
```


Invalid JSON or parameters.
```json
{}
```


Session not found.
```json
{}
```


Non-PTY session, staging unavailable, or verification failure.
```json
{}
```


A staging cap was exceeded.
```json
{}
```



---

## Staged transaction walkthrough

For a large file, or several files dropped together, run these three calls in order, carrying the same `drop` id and `token` through all three:

1. `POST /api/v1/terminal/drop-begin` — opens the transaction and returns the drop handle.
2. `POST /api/v1/terminal/upload` — uploads one raw slice of bytes. Call repeatedly to stream a file slice by slice.
3. `POST /api/v1/terminal/drop-commit` — finalizes the transaction and injects the OSC frame.


`upload` accepts raw `application/octet-stream` bytes, not JSON. The `offset` you pass MUST equal the file's current staged size — if it doesn't, the server returns `409` with the current size so the client can resume.


---

### `POST /api/v1/terminal/drop-begin`

Mints a server-side drop id and a high-entropy drop token, and creates the staging directory for the terminal. The `max_slice` field in the response echoes the server's effective request-body cap so the client never triggers a `413` mid-drop. Returns `409` when the terminal session has no exported staging base (e.g. staging disabled or a fail-closed spawn).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID (numeric `1`–`65535`) |

This endpoint accepts no request body.



```bash
curl -X POST \
  "https://proj-acme-ctr-7f3a-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/drop-begin?terminal_id=12"
```


```ts
const begin = await client.terminal.terminalDragAndDrop.beginTerminalDrop({
  terminal_id: "12"
});
// begin.drop    -> drop id, used in all subsequent calls
// begin.token   -> drop token, required by /upload and /drop-commit
// begin.max_slice -> server's effective request-body cap in bytes
```



#### Responses



Drop transaction opened.
```json
{
  "drop": "dr_01HMZ3K9C5VQF0RJX8WT6E2N4B",
  "token": "tk_8c4f1d2e9b0a47f6a3c1e9d4b5f7a2c8e1d4b6f9a3c2e7d1b4f8a5c9e2d6b3f1",
  "max_slice": 262144
}
```


Missing `terminal_id`.
```json
{}
```


Request method is not POST. The endpoint emits `method_not_allowed` with an `Allow: POST` header.
```json
{}
```


The terminal has no exported staging base.
```json
{}
```



---

### `POST /api/v1/terminal/upload`

Appends the raw request body to the staged file at `path` starting at byte `offset`. The body is **not** JSON — it is the raw slice bytes (up to the server's `--max-body-size`). Call this endpoint repeatedly to stream a large file slice by slice.

- `offset` MUST equal the file's current staged size; otherwise the request returns `409` with the current size in the JSON error body so the client can resume.
- Missing or wrong `token`, an unknown drop, or a sealed drop all return `409`.
- Byte/count caps return `413`.


The slice payload is the FIRST argument to the SDK call (`sliceBytes`) and the query parameters go in the SECOND trailing `options` object. Collapsing them into one flat object, or omitting the body argument, uploads nothing.


#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID (numeric `1`–`65535`) |
| `drop` | query | string | Yes | Drop id from `/drop-begin` |
| `token` | query | string | Yes | Drop token from `/drop-begin` |
| `path` | query | string | Yes | Sanitized relative path of the staged file (no `..`, not absolute) |
| `offset` | query | integer | Yes | Byte offset to write at (must equal the current staged size) |

#### Request Body

Raw `application/octet-stream` bytes. The request body schema is empty — pass the slice bytes directly, with no JSON wrapping.



```bash
# Upload slice #1 of "build.log" — bytes 0..262143 (offset must equal current staged size)
curl -X POST \
  "https://proj-acme-ctr-7f3a-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/upload?terminal_id=12&drop=dr_01HMZ3K9C5VQF0RJX8WT6E2N4B&token=tk_8c4f1d2e9b0a47f6a3c1e9d4b5f7a2c8e1d4b6f9a3c2e7d1b4f8a5c9e2d6b3f1&path=build.log&offset=0" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @build.log.part1

# Upload slice #2 — offset is now the size of slice #1
curl -X POST \
  "https://proj-acme-ctr-7f3a-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/upload?terminal_id=12&drop=dr_01HMZ3K9C5VQF0RJX8WT6E2N4B&token=tk_8c4f1d2e9b0a47f6a3c1e9d4b5f7a2c8e1d4b6f9a3c2e7d1b4f8a5c9e2d6b3f1&path=build.log&offset=262144" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @build.log.part2
```


```ts


// slice #1 — offset 0
const slice1 = readFileSync("build.log.part1");
await client.terminal.terminalDragAndDrop.uploadTerminalDropSlice(
  slice1,
  {
    terminal_id: "12",
    drop: "dr_01HMZ3K9C5VQF0RJX8WT6E2N4B",
    token: "tk_8c4f1d2e9b0a47f6a3c1e9d4b5f7a2c8e1d4b6f9a3c2e7d1b4f8a5c9e2d6b3f1",
    path: "build.log",
    offset: 0
  }
);

// slice #2 — offset equals the current staged size
const slice2 = readFileSync("build.log.part2");
await client.terminal.terminalDragAndDrop.uploadTerminalDropSlice(
  slice2,
  {
    terminal_id: "12",
    drop: "dr_01HMZ3K9C5VQF0RJX8WT6E2N4B",
    token: "tk_8c4f1d2e9b0a47f6a3c1e9d4b5f7a2c8e1d4b6f9a3c2e7d1b4f8a5c9e2d6b3f1",
    path: "build.log",
    offset: 262144
  }
);
```



#### Responses



Slice staged.
```json
{}
```


Missing or invalid parameters, or invalid `path`.
```json
{}
```


Request method is not POST.
```json
{}
```


Offset mismatch (carries `current_size`), bad token, sealed, or unknown drop.
```json
{
  "error": "offset_mismatch",
  "current_size": 262144
}
```


A staging byte/count cap was exceeded, or the body exceeded `--max-body-size`.
```json
{}
```



---

### `POST /api/v1/terminal/drop-commit`

Verifies the JSON manifest draft (`items`) against the staged bytes (existence, size, and sha256 `h` when present; zero-size items are created; `d: 1` entries are empty directories), writes the canonical `manifest.json`, seals the drop, and injects **one** OSC 8472 frame into the terminal's PTY input carrying the base64url staging root and the manifest sha256. PTY sessions only (else `409`). A duplicate commit of an already-sealed drop with a valid token re-injects the same frame and returns `200` (idempotent retry rescue).

#### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `terminal_id` | query | string | Yes | Terminal session ID (numeric `1`–`65535`) |
| `drop` | query | string | Yes | Drop id from `/drop-begin` |
| `token` | query | string | Yes | Drop token from `/drop-begin` |

#### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `ctx` | string | Yes | Drop context: `"drop"` or `"paste"` |
| `r` | integer | No | Drop cell row (Chat grid pane mapping) |
| `c` | integer | No | Drop cell column |
| `cr` | string | No | Clip-read correlation nonce (`[A-Za-z0-9_-]{1,64}`); echoed verbatim as the injected frame's `cr` field so the TUI can match a clipboard-read landing. Invalid or oversized values are ignored. |
| `items` | array | Yes | Manifest entries `[{p, d, s, name, h?}]` |



```bash
curl -X POST \
  "https://proj-acme-ctr-7f3a-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/drop-commit?terminal_id=12&drop=dr_01HMZ3K9C5VQF0RJX8WT6E2N4B&token=tk_8c4f1d2e9b0a47f6a3c1e9d4b5f7a2c8e1d4b6f9a3c2e7d1b4f8a5c9e2d6b3f1" \
  -H "Content-Type: application/json" \
  -d '{
    "ctx": "drop",
    "r": 4,
    "c": 12,
    "cr": "clip_8f3a",
    "items": [
      {
        "name": "build.log",
        "p": "build.log",
        "d": 0,
        "s": 524288,
        "h": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
      },
      {
        "name": "empty",
        "p": "empty",
        "d": 1
      }
    ]
  }'
```


```ts
await client.terminal.terminalDragAndDrop.commitTerminalDrop(
  {
    ctx: "drop",
    r: 4,
    c: 12,
    cr: "clip_8f3a",
    items: [
      {
        name: "build.log",
        p: "build.log",
        d: 0,
        s: 524288,
        h: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
      },
      {
        name: "empty",
        p: "empty",
        d: 1
      }
    ]
  },
  {
    terminal_id: "12",
    drop: "dr_01HMZ3K9C5VQF0RJX8WT6E2N4B",
    token: "tk_8c4f1d2e9b0a47f6a3c1e9d4b5f7a2c8e1d4b6f9a3c2e7d1b4f8a5c9e2d6b3f1"
  }
);
```



#### Responses



Drop sealed and frame injected.
```json
{}
```


Invalid JSON or missing parameters.
```json
{}
```


Session not found.
```json
{}
```


Request method is not POST.
```json
{}
```


Manifest mismatch, bad token, unknown drop, or non-PTY session.
```json
{}
```


The manifest or item count exceeded a cap.
```json
{}
```


Frame injection or filesystem failure.
```json
{}
```