Terminal: File Drag-and-Drop
Section titled “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/dropperforms 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.icuOne-shot drop
Section titled “One-shot drop”POST /api/v1/terminal/drop
Section titled “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
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | query | string | Yes | Terminal session ID (numeric 1–65535) |
Request Body
Section titled “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) |
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==" } ] } ] }'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
Section titled “Responses”Drop sealed and frame injected.
{}Invalid JSON or parameters.
{}Session not found.
{}Non-PTY session, staging unavailable, or verification failure.
{}A staging cap was exceeded.
{}Staged transaction walkthrough
Section titled “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:
POST /api/v1/terminal/drop-begin— opens the transaction and returns the drop handle.POST /api/v1/terminal/upload— uploads one raw slice of bytes. Call repeatedly to stream a file slice by slice.POST /api/v1/terminal/drop-commit— finalizes the transaction and injects the OSC frame.
POST /api/v1/terminal/drop-begin
Section titled “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
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
terminal_id | query | string | Yes | Terminal session ID (numeric 1–65535) |
This endpoint accepts no request body.
curl -X POST \ "https://proj-acme-ctr-7f3a-terminal-1.us-east-1.containers.hoody.icu/api/v1/terminal/drop-begin?terminal_id=12"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 bytesResponses
Section titled “Responses”Drop transaction opened.
{ "drop": "dr_01HMZ3K9C5VQF0RJX8WT6E2N4B", "token": "tk_8c4f1d2e9b0a47f6a3c1e9d4b5f7a2c8e1d4b6f9a3c2e7d1b4f8a5c9e2d6b3f1", "max_slice": 262144}Missing terminal_id.
{}Request method is not POST. The endpoint emits method_not_allowed with an Allow: POST header.
{}The terminal has no exported staging base.
{}POST /api/v1/terminal/upload
Section titled “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.
offsetMUST equal the file’s current staged size; otherwise the request returns409with 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 return409. - Byte/count caps return
413.
Parameters
Section titled “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
Section titled “Request Body”Raw application/octet-stream bytes. The request body schema is empty — pass the slice bytes directly, with no JSON wrapping.
# 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 #1curl -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.part2import { readFileSync } from "node:fs";
// slice #1 — offset 0const 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 sizeconst 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
Section titled “Responses”Slice staged.
{}Missing or invalid parameters, or invalid path.
{}Request method is not POST.
{}Offset mismatch (carries current_size), bad token, sealed, or unknown drop.
{ "error": "offset_mismatch", "current_size": 262144}A staging byte/count cap was exceeded, or the body exceeded --max-body-size.
{}POST /api/v1/terminal/drop-commit
Section titled “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
Section titled “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
Section titled “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?}] |
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 } ] }'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
Section titled “Responses”Drop sealed and frame injected.
{}Invalid JSON or missing parameters.
{}Session not found.
{}Request method is not POST.
{}Manifest mismatch, bad token, unknown drop, or non-PTY session.
{}The manifest or item count exceeded a cap.
{}Frame injection or filesystem failure.
{}