Skip to content

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, commit are gated by a safety check → 403 if the session is deemed unsafe for destructive git ops.
  • push auto-sets upstream to origin/<branch> when none exists (no origin → no_upstream). pull is --ff-only; fetch is --prune. checkout with dirty_mode="block" aborts on a dirty tree (use stash-checkout to 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.