Workspace, files & git¶
The workspace explorer and full source-control surface. Every git and file endpoint is scoped by session_id — the server resolves that session's workspace itself, so a client never passes a filesystem path for the repo root. Verified against api/routes.py, api/workspace_git.py, api/workspace.py.
Session → workspace
Pass session_id; the server maps it to session.workspace. A missing/unknown session → 404 { "error": "Session not found" }. Git errors return { "error", "code" } (the code is a stable machine-readable reason like not_a_repo, no_upstream).
Workspace & files¶
GET /api/workspaces¶
{ "workspaces": [ { "path", "name" } ], "last": "<path>", "terminal_remote_backend": bool }.
GET /api/workspaces/suggest¶
Query prefix (default "") → { "suggestions": ["<path>"], "prefix" }. Path autocomplete for a workspace picker.
GET /api/list — directory listing¶
Query: session_id (req), path (default .). → { "entries": [Entry], "signature", "path" }.
Entry = { "name", "path", "type": "file"|"dir"|"symlink", "size": int|null, "mtime_ns": int|null } (symlinks add target, is_dir, target_outside_workspace). 400 no session_id; 404 not-a-dir / traversal.
GET /api/file — read a text file¶
Query: session_id (req), path (req). → { "path", "content": "<utf-8>", "size": <bytes>, "lines": <int> }. Office files (.docx/.xlsx/.pptx) return an extracted-preview payload instead. 404 if not a file or over the size cap; 503 if office-extraction libs are missing.
GET /api/file/raw — raw bytes¶
Query: session_id (req), path (req), download=1 (force attachment), inline=1 (permit sandboxed HTML inline). Returns the raw file bytes with a Content-Type from the extension. Dangerous types (HTML/SVG) are forced to attachment unless explicitly inline. Use this for images, downloads, and binary previews.
GET /api/media — inline chat media¶
Query: path (absolute, req), session_id (optional media-token grant). Returns raw bytes for inline display in the transcript. Path must resolve within allowlisted roots; Hermes state/secret files are hard-denied (403/404).
GET /api/folder/download — zip a folder¶
Query: session_id (req), path (dir, req). Streams application/zip as an attachment. 413 if the folder exceeds the file-count or size cap (the error body names the limit and how to configure it).
POST /api/upload — upload a file (multipart)¶
multipart/form-data with a text field session_id and a file field file. → { "filename": "<sanitized>", "path": "<absolute dest>", "size", "mime", "is_image": bool }. 413 over the max upload size (checked via Content-Length first); 400 if no file/filename. (The reference client caps at 20 MB client-side.)
Git¶
All read endpoints take ?session_id=…. Non-repo workspaces return { "git": { "is_git": false } } (HTTP 200) for status, but not_a_repo (400) for branches/diff.
Reads¶
| Endpoint | Returns |
|---|---|
GET /api/git-info |
compact { "git": { branch, dirty, modified, untracked, ahead, behind, is_git } \| null } |
GET /api/git/status |
{ "git": GitStatus } — full status (see below) |
GET /api/git/branches |
{ "branches": { current, detached, head, local[], remote[], upstream, ahead, behind } } |
GET /api/git/diff |
?path=&kind=unstaged\|staged → { "diff": { path, kind, binary, too_large, additions, deletions, diff } } |
GitStatus — { is_git, branch, upstream, ahead, behind, totals: { changed, staged, unstaged, untracked, conflicts }, files: [GitFile], truncated }. File list capped at 500 (truncated: true).
GitFile — { path, old_path, status ("M"/"A"/"D"/"R"/"??"/…), staged, unstaged, untracked, conflict, additions, deletions, binary }.
Actions (all POST, body includes session_id)¶
| Endpoint | Body | Returns |
|---|---|---|
/api/git/stage · /unstage |
{ session_id, paths: [] } |
{ ok, git: GitStatus } |
/api/git/discard |
{ session_id, paths: [], delete_untracked? } |
{ ok, git } |
/api/git/commit |
{ session_id, message } |
{ ok, commit: "<sha>", status: GitStatus } |
/api/git/commit-selected |
{ session_id, message, paths: [] } |
{ ok, commit, paths, status } |
/api/git/fetch · /pull · /push |
{ session_id } |
{ ok, message, status } |
/api/git/checkout |
{ session_id, ref, mode: "local"\|"remote"\|"new", new_branch?, track?, dirty_mode="block" } |
{ ok, git, branches, current_branch, message } |
/api/git/stash-checkout |
{ session_id, ref, mode, new_branch?, track? } |
{ ok, git, branches, current_branch, stash_name, stashed, restored_stash } |
- Destructive-safe gate:
pull,push,checkout,stash-checkout,stage,unstage,discard,commitare gated by a safety check →403if the session is deemed unsafe for destructive git ops. pushauto-sets upstream toorigin/<branch>when none exists (no origin →no_upstream).pullis--ff-only;fetchis--prune.checkoutwithdirty_mode="block"aborts on a dirty tree (usestash-checkoutto auto-stash).
AI commit messages¶
POST /api/git/commit-message{ session_id }→{ ok, message, truncated }— an LLM-generated message from the staged diff.POST /api/git/commit-message-selected{ session_id, paths: [] }→ same, from the selected paths' diff.
These run an LLM server-side (slow — the reference client uses a 120s timeout) and are not destructive-gated.