forked from bots-garden/ori
| ✨ Workspace panel, selectors, previews, desktop app, sandbox template, resizable file tree, light/dark theme | 1 | # Reference: workspace API |
| 2 | ||
| 3 | > Neutral, exhaustive description of the endpoints behind the workspace panel and the composer's selectors: the file API, the skills endpoint and the terminal WebSocket. Relative paths resolve against the server's `--cwd`; absolute paths are used as-is (access is unrestricted by design — ori targets already-isolated sandboxes). | |
| 4 | ||
| 5 | ## GET /api/files | |
| 6 | ||
| 7 | Lists a directory. | |
| 8 | ||
| 9 | | Query parameter | Type | Default | Description | | |
| 10 | | --- | --- | --- | --- | | |
| 11 | | `path` | string | the workspace root | Directory to list. | | |
| 12 | ||
| 13 | Response `200`: | |
| 14 | ||
| 15 | ```json | |
| 16 | { "path": "/work", "entries": [ { "name": "src", "path": "/work/src", "isDir": true, "size": 0 } ] } | |
| 17 | ``` | |
| 18 | ||
| 19 | Entries are sorted directories first, then files, each group alphabetically. | |
| 20 | ||
| 21 | ## GET /api/files/search | |
| 22 | ||
| 23 | Searches files recursively under the workspace root, for the composer's `@` selector. | |
| 24 | ||
| 25 | | Query parameter | Type | Default | Description | | |
| 26 | | --- | --- | --- | --- | | |
| 27 | | `q` | string | `""` | Case-insensitive substring matched against the path relative to the root. Empty returns the first files. | | |
| 28 | | `limit` | integer | 50 | Maximum number of hits; capped at 500. | | |
| 29 | ||
| 30 | Response `200`: | |
| 31 | ||
| 32 | ```json | |
| 33 | { "root": "/work", "files": [ { "name": "main.go", "path": "/work/src/main.go", "relPath": "src/main.go" } ] } | |
| 34 | ``` | |
| 35 | ||
| 36 | `path` is absolute (usable in the other endpoints), `relPath` uses forward slashes. Hits are ranked: files whose base name contains the query first, then shorter relative paths, then alphabetically. The walk never enters `.git` or `node_modules`, skips unreadable entries, and stops after 50 000 directory entries. `files` is always an array (`[]` when nothing matches). A missing root is a `404`. | |
| 37 | ||
| 38 | ## GET /api/file | |
| 39 | ||
| 40 | Reads a text file. | |
| 41 | ||
| 42 | | Query parameter | Type | Default | Description | | |
| 43 | | --- | --- | --- | --- | | |
| 44 | | `path` | string | — | File to read. | | |
| 45 | ||
| 46 | Response `200`: `{ "path": "/work/README.md", "content": "…" }` | |
| 47 | ||
| 48 | ## PUT /api/file | |
| 49 | ||
| 50 | Writes a text file, creating parent directories as needed. | |
| 51 | ||
| 52 | | Query parameter | Type | Default | Description | | |
| 53 | | --- | --- | --- | --- | | |
| 54 | | `path` | string | — (required) | File to write. | | |
| 55 | ||
| 56 | Body: `{ "content": "…" }` — response `200`: `{ "path": "…", "status": "saved" }` | |
| 57 | ||
| 58 | ## GET /api/raw | |
| 59 | ||
| 60 | Streams a file's bytes as-is — how the preview pane shows images. No size cap, no text requirement; `Range` requests are honoured (`http.ServeContent`). | |
| 61 | ||
| 62 | | Query parameter | Type | Default | Description | | |
| 63 | | --- | --- | --- | --- | | |
| 64 | | `path` | string | — (required) | File to stream. | | |
| 65 | ||
| 66 | Response `200` with the file bytes, `X-Content-Type-Options: nosniff`, and a `Content-Type` chosen in this order: | |
| 67 | ||
| 68 | | Extension | Content-Type | | |
| 69 | | --- | --- | | |
| 70 | | `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg` `.bmp` `.ico` `.avif` | pinned image types (`image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/svg+xml`, `image/bmp`, `image/x-icon`, `image/avif`) | | |
| 71 | | any other known extension | the host's mime table (e.g. `text/markdown; charset=utf-8`) | | |
| 72 | | unknown | sniffed from the first 512 bytes | | |
| 73 | ||
| 74 | ## GET /api/skills | |
| 75 | ||
| 76 | Lists the Claude Code skills available to the session, for the composer's `/` selector. | |
| 77 | ||
| 78 | Response `200`: | |
| 79 | ||
| 80 | ```json | |
| 81 | { "skills": [ { "name": "quality", "description": "Audit code quality with qlty.", "path": "/work/.claude/skills/quality/SKILL.md", "source": "project" } ] } | |
| 82 | ``` | |
| 83 | ||
| 84 | A skill is a directory holding a `SKILL.md`, looked up in `<cwd>/.claude/skills/*/` (`source: "project"`) and `~/.claude/skills/*/` (`source: "user"`). `name` and `description` come from the file's YAML frontmatter (`key: value`, quoted values and `>` / `|` block scalars are understood); a missing name falls back to the directory name, a missing description to `""`. A project skill shadows a user skill of the same name. Results are sorted by name; `skills` is always an array. | |
| 85 | ||
| 86 | ## File API errors | |
| 87 | ||
| 88 | All errors are JSON: `{ "error": "message" }`. | |
| 89 | ||
| 90 | | Status | Cause | | |
| 91 | | --- | --- | | |
| 92 | | 400 | Missing `path` on PUT or `GET /api/raw`, invalid JSON body, or reading a directory. | | |
| 93 | | 403 | Filesystem permission denied. | | |
| 94 | | 404 | Path (or the workspace root, for the search) does not exist. | | |
| 95 | | 413 | File larger than 5 MiB (`GET /api/file` only; `/api/raw` has no cap). | | |
| 96 | | 415 | File is not valid UTF-8 text (`GET /api/file` only). | | |
| 97 | ||
| 98 | ## GET /ws/terminal | |
| 99 | ||
| 100 | Upgrades to a WebSocket and starts one shell (from `$SHELL`, falling back to `bash` then `sh`, with `TERM=xterm-256color`) inside a pseudo-terminal, in the server's `--cwd`. | |
| 101 | ||
| 102 | | Direction | Frame type | Content | | |
| 103 | | --- | --- | --- | | |
| 104 | | server → client | binary | Raw terminal output bytes. | | |
| 105 | | client → server | text | `{"type":"input","data":"…"}` — keystrokes written to the pty. | | |
| 106 | | client → server | text | `{"type":"resize","cols":N,"rows":N}` — viewport change. | | |
| 107 | ||
| 108 | The socket closes with a normal-closure status when the shell exits; closing the socket kills the shell. Malformed text frames are ignored. |