Terminal: File Drag-and-Drop
Section titled “Terminal: File Drag-and-Drop”The Terminal Drag-and-Drop endpoints inject a file (or a tree of files) into a running PTY terminal session by staging the bytes on the server, sealing a JSON manifest, and emitting a single OSC 8472 escape frame on the terminal’s input. The program running inside the terminal then interprets that frame as a drag-and-drop event in the same way it would for a native file drop.
There are two ways to deliver a drop:
- One-shot drop —
POST /api/v1/terminal/drop. Begin, stage, and commit in a single request. Use this for a small file that fits comfortably in one request body. - Staged transaction — for a large file, or several files dropped together, run the three-step sequence in order:
POST /api/v1/terminal/drop-begin— opens a staging transaction and returns the drop handle.POST /api/v1/terminal/upload— uploads one raw slice of file bytes. Call it repeatedly to stream a large file slice by slice.POST /api/v1/terminal/drop-commit— finalizes the transaction and injects the OSC frame.
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. An uncommitted transaction only stages bytes and changes nothing on screen.
All four endpoints are PTY-only and operate on the container-scoped terminal host. Replace {projectId}-{containerId} with your project and container ids in every URL.
One-shot drop
Section titled “One-shot drop”POST /api/v1/terminal/drop
Section titled “POST /api/v1/terminal/drop”Begin, stage, and commit a small drop in a single request. Items are {name, b64} for files (base64 content) or {name, dir:true, items:[...]} for nested directories. PTY sessions only. 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} | {name,dir:true,items:[...]}]) |
Response
Section titled “Response”Drop sealed and frame injected.
{}Invalid JSON / parameters.
{}Session not found.
{}Non-PTY session, staging unavailable, or verification failure.
{}A staging cap was exceeded.
{}SDK Usage
Section titled “SDK Usage”curl -X POST "https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/drop?terminal_id=1" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "ctx": "drop", "items": [ { "name": "hello.txt", "b64": "aGVsbG8gd29ybGQ=" } ] }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalDragAndDrop.oneShotTerminalDrop( { ctx: 'drop', items: [ { name: 'hello.txt', b64: 'aGVsbG8gd29ybGQ=' }, ], }, { terminal_id: '1' },);Staged transaction
Section titled “Staged transaction”Use the three-step sequence below for any drop that is too large to fit in one request body, or when you are streaming several files in one logical drop. Each step carries the same drop handle and token returned by /drop-begin. The drop becomes visible to the terminal at the commit step.
Step 1 — POST /api/v1/terminal/drop-begin
Section titled “Step 1 — POST /api/v1/terminal/drop-begin”Mint a server-side drop id and a high-entropy drop token, and create the staging directory for the terminal. The client uploads file slices via /upload (authenticated by the token) and finalizes via /drop-commit. max_slice 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 takes no request body.
Response
Section titled “Response”Drop transaction opened.
{}Missing terminal_id.
{}Request method is not POST. Emits method_not_allowed with an Allow: POST header.
{}The terminal has no exported staging base.
{}SDK Usage
Section titled “SDK Usage”curl -X POST "https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/drop-begin?terminal_id=1" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalDragAndDrop.beginTerminalDrop({ terminal_id: '1' });Step 2 — POST /api/v1/terminal/upload
Section titled “Step 2 — POST /api/v1/terminal/upload”Append 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). offset must equal the file’s current staged size, else 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 return 409. Byte/count caps return 413.
Call this endpoint repeatedly to stream a large file slice by slice.
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 file-slice bytes — sent as application/octet-stream, up to the server’s --max-body-size. The body is NOT JSON.
Response
Section titled “Response”Slice staged.
{}Missing/invalid parameters or path.
{}Request method is not POST.
{}Offset mismatch (carries current_size), bad token, sealed, or unknown drop.
{}A staging byte/count cap was exceeded, or the body exceeded --max-body-size.
{}SDK Usage
Section titled “SDK Usage”curl -X POST "https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/upload?terminal_id=1&drop={dropId}&token={dropToken}&path=hello.txt&offset=0" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/octet-stream" \ --data-binary @hello.txtimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const sliceBytes = new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]);
await client.terminal.terminalDragAndDrop.uploadTerminalDropSlice( sliceBytes, { terminal_id: '1', drop: '{dropId}', token: '{dropToken}', path: 'hello.txt', offset: 0 },);Step 3 — POST /api/v1/terminal/drop-commit
Section titled “Step 3 — POST /api/v1/terminal/drop-commit”Verify 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), write the canonical manifest.json, seal the drop, and inject 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/oversized values are ignored. |
items | array | Yes | Manifest entries ([{p,d,s,name,h?}]) |
Response
Section titled “Response”Drop sealed and frame injected.
{}Invalid JSON / 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.
{}SDK Usage
Section titled “SDK Usage”curl -X POST "https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/drop-commit?terminal_id=1&drop={dropId}&token={dropToken}" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "ctx": "drop", "items": [ { "p": "hello.txt", "d": 0, "s": 11, "name": "hello.txt" } ] }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.terminalDragAndDrop.commitTerminalDrop( { ctx: 'drop', items: [ { p: 'hello.txt', d: 0, s: 11, name: 'hello.txt' }, ], }, { terminal_id: '1', drop: '{dropId}', token: '{dropToken}' },);