HTTP API Reference
July 31, 2026 · View on GitHub
REST API served by the Axum HTTP server when MCP server is enabled. All Tauri commands are accessible as HTTP endpoints.
Base URL
- Local (Unix socket):
<config_dir>/mcp.sock— always started on macOS/Linux. No auth, MCP always enabled. Used by the local MCP bridge binary. - Remote (TCP):
http://<host>:{remote_access_port}— only started when remote access is enabled in settings. HTTP Basic Auth required.
Authentication
- MCP mode (localhost): No authentication
- Remote access mode: HTTP Basic Auth with configured username/password
Session Endpoints
List Sessions
GET /sessions
Returns array of active session info (ID, cwd, worktree path, branch, and
nested state). For detected agents, state.agent_state distinguishes PTY
silence (idle) from explicit protocol completion (completed); the latter
requires a parsed suggest: [ ... ] marker. Other values are starting,
working, and awaiting_input. state.background_work is true when meaningful
non-helper descendants keep autonomous work alive despite an input-ready
terminal (state.shell_state == "idle").
Create Session
POST /sessions
Content-Type: application/json
{
"rows": 24,
"cols": 80,
"shell": "/bin/zsh", // optional
"cwd": "/path/to/dir" // optional
}
Returns { "session_id": "..." }.
Create Session with Worktree
POST /sessions/worktree
Content-Type: application/json
{ "pty_config": { ... }, "worktree_config": { ... } }
Creates a git worktree and a PTY session in one call.
Spawn Agent Session
POST /sessions/agent
Content-Type: application/json
{ "pty_config": { ... }, "agent_config": { ... } }
Spawns an AI agent (Claude, etc.) in a PTY session.
Write to Session
POST /sessions/:id/write
Content-Type: application/json
{ "data": "ls -la\n" }
Resize Session
POST /sessions/:id/resize
Content-Type: application/json
{ "rows": 30, "cols": 120 }
Read Output
GET /sessions/:id/output?limit=4096&format=text
Returns recent output. Format controls what is returned:
format | Response shape | Description |
|---|---|---|
| (omit) | { "data": "<string>", "data_length": N, "total_written": N } | Raw PTY output as a lossy-UTF-8 string (not base64), read from the ring buffer |
text | { "data": "<string>", "data_length": N, "total_written": N } | One canonical terminal-grid snapshot, joined by \n (not from the ring buffer) |
log | { "lines": [...], "total_lines": N, "screen": [...], "input_line"? } | VT100-extracted clean lines (no ANSI, no TUI garbage) plus current screen rows and optional input line |
| Param | Default | Description |
|---|---|---|
limit | raw: 8192 bytes; text/log: all | raw: max bytes; text/log: max lines to return |
offset | (tail) | text/log: absolute start row/line offset. When omitted, returns the newest limit rows/lines. When provided, returns data starting from that offset |
format | (raw) | See table above |
format=log reads from VtLogBuffer — a VT100-aware buffer that extracts only scrolled-off lines, suppressing alternate-screen TUI apps (vim, htop, claude). Ideal for mobile clients.
format=text is a point-in-time canonical grid view. It does not concatenate
the finalized log cursor with the visible screen, because growing a viewport can
move history rows back onto the screen and make that concatenation overlap.
total_written is the snapshot's total grid-row count for this format.
total_lines in the response is a monotonically increasing counter — it never decreases when old lines are evicted from the buffer. Use it as a stable cursor for paginated reads. The offset parameter operates in the same coordinate space.
Kitty Protocol Flags
GET /sessions/:id/kitty-flags
Returns the current Kitty keyboard protocol flags (integer) for a session.
Foreground Process
GET /sessions/:id/foreground
Returns the foreground process info for a session.
PTY / Terminal Read State
GET /sessions/:id/shell-state -> { "state": "busy"|"idle"|null }
GET /sessions/:id/last-prompt -> { "prompt": string|null }
GET /sessions/:id/input-buffer -> { "content": string }
GET /sessions/:id/leaf-pid -> { "pid": number|null }
GET /sessions/:id/has-foreground -> { "process": string|null }
POST /sessions/:id/visible { "visible": bool } -> { "ok": true }
GET /sessions/:id/terminal/selection-text?startRow=&startCol=&endRow=&endCol= -> { "text": string }
GET /sessions/:id/terminal/logical-line?row=N -> [logicalStartRow, text]
GET /sessions/:id/terminal/hyperlink-span?row=R&col=C -> [startCol, endCol, url] | null
GET /process/stats -> ProcessStats[]
Read-only PTY/terminal state mirroring the desktop Tauri commands (story 062). The
{field}-wrapped responses are unwrapped by the frontend transport to match the
command's bare return (e.g. Option<String> → null). The desktop-only commands
themselves are absent from the remote binary, so these handlers read AppState
directly.
Pause/Resume
POST /sessions/:id/pause
POST /sessions/:id/resume
Rename Session
PUT /sessions/:id/name
Content-Type: application/json
{ "name": "my-session" }
Sets a custom display name for a session.
Close Session
DELETE /sessions/:id?cleanup_worktree=false
Streaming Endpoints
WebSocket PTY Stream
WS /sessions/:id/stream
Receives real-time PTY output as text frames. One WebSocket per session.
WebSocket JSON Framing (Mobile/Browser)
WebSocket connections to /sessions/:id/stream receive JSON-framed messages:
{"type": "output", "data": "raw terminal output text"}
{"type": "parsed", "event": {"type": "question", "text": "Allow?"}}
{"type": "exit"}
{"type": "closed"}
Frame types:
output— Raw PTY output (ANSI-stripped when?format=text)log— VT100-extracted clean lines batch (when?format=log):{"type":"log","lines":[...],"offset":N}parsed— Structured events (questions, rate limits, errors) from the output parserexit— Session process exitedclosed— Session was closed
WebSocket format=log
WS /sessions/:id/stream?format=log
When ?format=log is specified, the connection streams VT100-extracted log lines instead of raw PTY chunks:
- On connect: sends all accumulated lines as a single catch-up frame
- While running: polls every 200ms and sends new lines batched by offset
- PTY input passthrough is still available (write text/binary frames to send to PTY)
Server-Sent Events (SSE)
GET /events?types=repo-changed,pty-parsed
Broadcasts server-side events to all browser/mobile clients. Supports optional ?types= query parameter for comma-separated event name filtering. Uses monotonic event IDs and 15-second keep-alive pings.
| Event | Payload | Description |
|---|---|---|
session-created | {session_id, cwd, agent_type, display_name} | New session started; display_name is the optional stable assigned name |
session-closed | {session_id} | Session ended |
repo-changed | {repo_path} | Git repository state changed |
head-changed | {repo_path, branch} | Git HEAD changed (branch switch) |
pty-parsed | {session_id, parsed} | Structured output event from PTY parser |
pty-exit | {session_id} | PTY process exited |
plugin-changed | {plugin_ids} | Plugin(s) installed/removed/updated |
upstream-status-changed | {name, status} | MCP upstream server status change |
mcp-toast | {title, message, level, sound} | Toast notification from MCP layer |
triage-progress | {repo_path, summary, files, phase, done, llm_used, llm_model} | Diff-triage classification progress (browser parity for the desktop window event) |
lagged | {missed} | Client fell behind; N events were dropped |
MCP Streamable HTTP
POST /mcp
Content-Type: application/json
{ JSON-RPC message }
Single endpoint for all MCP JSON-RPC requests (initialize, tools/list, tools/call). Returns JSON-RPC responses directly in the HTTP response body. Session ID returned via Mcp-Session-Id header on initialize.
GET /mcp → 405 Method Not Allowed
DELETE /mcp → Ends MCP session (pass Mcp-Session-Id header)
Git Endpoints
Repository Info
GET /repo/info?path=/path/to/repo
Returns RepoInfo (name, branch, status, initials).
Git Diff
GET /repo/diff?path=/path/to/repo
Returns unified diff string.
Diff Stats
GET /repo/diff-stats?path=/path/to/repo
Returns { "additions": N, "deletions": N }.
Changed Files
GET /repo/files?path=/path/to/repo
Returns array of ChangedFile (path, status, additions, deletions).
Single File Diff
GET /repo/file-diff?path=/path/to/repo&file=src/main.rs
Returns diff for a single file.
Read File
GET /repo/file?path=/path/to/repo&file=src/main.rs
Returns file contents as text.
Branches
GET /repo/branches?path=/path/to/repo
Returns sorted branch list.
Repo Summary
GET /repo/summary?path=/path/to/repo
Aggregate snapshot: worktree paths, merged branches, and per-path diff stats in one round-trip. Replaces 3+ separate IPC calls.
Repo Structure (Progressive Phase 1)
GET /repo/structure?path=/path/to/repo
Returns { "worktree_paths": { "branch": "/path", ... }, "merged_branches": ["branch", ...] }. Fast path — no diff stats computation.
Repo Diff Stats (Progressive Phase 2)
GET /repo/diff-stats/batch?path=/path/to/repo
Returns { "diff_stats": { "/path": { "additions": N, "deletions": N }, ... }, "last_commit_ts": { "branch": N, ... } }. Slow path — computes per-worktree diff stats and last commit timestamps.
Local Branches
GET /repo/local-branches?path=/path/to/repo
Returns local branch list.
Checkout Remote Branch
POST /repo/checkout-remote
Content-Type: application/json
{ "repoPath": "/path/to/repo", "branchName": "feat-remote" }
Creates a local tracking branch from origin/<branchName>.
Rename Branch
POST /repo/branch/rename
Content-Type: application/json
{ "path": "/path/to/repo", "old_name": "old", "new_name": "new" }
Check Main Branch
GET /repo/is-main-branch?branch=main
Returns true if the branch is main/master/develop.
Initials
GET /repo/initials?name=my-repo
Returns 2-char repo initials.
Markdown Files
GET /repo/markdown-files?path=/path/to/repo
Returns list of .md files in a directory.
Recent Commits
GET /repo/recent-commits?path=/path/to/repo
Returns recent git commits.
GitHub Status
GET /repo/github?path=/path/to/repo
Returns PR status, CI status, ahead/behind for current branch.
PR Statuses (Batch)
GET /repo/prs?path=/path/to/repo
Returns BranchPrStatus[] for all branches with open PRs.
PR Statuses (Multi-Repo Batch)
POST /repo/prs/batch
Content-Type: application/json
{ "paths": ["/repo1", "/repo2"], "include_merged": false }
Returns aggregated PR statuses across multiple repositories.
Issues
GET /repo/issues?path=/path/to/repo
Returns GitHubIssue[] for the repo, filtered by the user's configured issue filter.
Close Issue
POST /repo/issues/close
Content-Type: application/json
{ "repo_path": "/path/to/repo", "issue_number": 42 }
Closes the specified issue via GitHub GraphQL API.
Reopen Issue
POST /repo/issues/reopen
Content-Type: application/json
{ "repo_path": "/path/to/repo", "issue_number": 42 }
Reopens a closed issue via GitHub GraphQL API.
GitHub Auth & Diagnostics
Browser/PWA parity for the GitHub settings panel. Registered on the loopback
router only (the headless tuic-remote daemon does not expose GitHub).
GET /github/viewer-login -> string (login)
GET /repo/ci-failure-logs?repoPath=&branch= -> string (logs)
POST /github/pr-hide-drafts { hide } -> null
POST /github/auth/start -> DeviceCodeResponse
POST /github/auth/poll { deviceCode } -> PollResult
POST /github/auth/logout -> null
POST /github/auth/disconnect -> null
GET /github/auth/status -> AuthStatus
GET /github/diagnostics -> GitHubDiagnostics
Auth commands share the desktop *_impl (device-code flow + OS-keyring token via
crate::credentials). get_all_issues is intentionally unmapped — it has no frontend
invoke() caller (the /repo/issues route already serves browser issue lists).
Merged Branches
GET /repo/branches/merged?path=/path/to/repo
Returns list of branch names merged into the default branch.
Orphan Worktrees
GET /repo/orphan-worktrees?repoPath=/path/to/repo
Returns list of worktree directory paths that are in detached HEAD state (their branch was deleted).
Remove Orphan Worktree
POST /repo/remove-orphan
Content-Type: application/json
{ "repoPath": "/path/to/repo", "worktreePath": "/path/to/worktree" }
Removes an orphan worktree by filesystem path. The worktree path is validated against the repo's actual worktree list.
Merge PR via GitHub
POST /repo/merge-pr
Content-Type: application/json
{ "repoPath": "/path/to/repo", "prNumber": 42, "mergeMethod": "squash" }
Merges a PR via the GitHub API. mergeMethod must be "merge", "squash", or "rebase". Returns {"sha": "..."} on success.
Approve PR
POST /repo/approve-pr
Content-Type: application/json
{ "repoPath": "/path/to/repo", "prNumber": 42 }
Submits an approving review on a PR via the GitHub API.
CI Checks
GET /repo/ci?path=/path/to/repo
Returns detailed CI check list.
PR Diff
GET /repo/pr-diff?path=/path/to/repo
Returns diff for the current branch's open PR.
AI Review / Changelog / Conflict Assist
POST /ai/review/pr { repoPath, prNumber } -> PrReviewResult
GET /repo/merged-prs?path=&sinceTag= -> MergedPr[]
GET /repo/changelog?path=&sinceTag= -> { markdown, json }
POST /repo/conflict-assist { repoPath, prNumber } -> ConflictAssistResult
/ai/review/pr runs the multi-turn review engine (Main slot) over a PR diff and
returns line-level findings. /repo/changelog summarizes merged PRs (Headless
slot) into markdown + a structured JSON breakdown; sinceTag filters to PRs
merged at/after that tag's date. /repo/conflict-assist creates a worktree on
the PR head and rebases it onto the base. status is clean only when the base
was refreshed from origin, clean_unverified when a conflict-free result used
an existing tracking ref or local fallback, and conflicts when manual
resolution is needed. The response includes base_source, an optional
base_warning, the conflicted-file list, and an agent prompt; it never pushes
or merges.
Remote URL
GET /repo/remote-url?path=/path/to/repo
Returns the remote origin URL.
Git Panel Endpoints
Working Tree Status
GET /repo/working-tree-status?path=/path/to/repo
Returns porcelain v2 working tree status.
Panel Context
GET /repo/panel-context?path=/path/to/repo
Returns aggregated context for the Git Panel (status, branch, merge state).
Stage Files
POST /repo/stage
Content-Type: application/json
{ "repoPath": "/path/to/repo", "files": ["src/main.rs"] }
Unstage Files
POST /repo/unstage
Content-Type: application/json
{ "repoPath": "/path/to/repo", "files": ["src/main.rs"] }
Discard Files
POST /repo/discard
Content-Type: application/json
{ "repoPath": "/path/to/repo", "files": ["src/main.rs"] }
Commit
POST /repo/commit
Content-Type: application/json
{ "repoPath": "/path/to/repo", "message": "feat: add feature" }
Run Git Command
POST /repo/run-git
Content-Type: application/json
{ "repoPath": "/path/to/repo", "args": ["log", "--oneline", "-5"] }
Runs an arbitrary git command in the repo directory.
Commit Log
GET /repo/commit-log?path=/path/to/repo
Returns commit log entries.
File History
GET /repo/file-history?path=/path/to/repo&file=src/main.rs
Returns git log for a specific file.
File Blame
GET /repo/file-blame?path=/path/to/repo&file=src/main.rs
Returns line-by-line blame annotations.
Git Panel (Branches / Graph / Gutter)
GET /repo/gutter-changes?path=&file=&scope= -> GutterChange[]
GET /repo/branches-detail?path= -> BranchDetail[] (cached)
GET /repo/recent-branches?path=&limit= -> string[]
GET /repo/branch-base?path=&branchName= -> string | null
GET /repo/worktree-dirty?repoPath=&branchName= -> bool
GET /repo/base-ref-options?repoPath= -> BaseRefOption[]
GET /repo/commit-graph?path=&count= -> GraphNode[]
POST /repo/clone-branch-name { sourceBranch, existingNames } -> string
POST /repo/create-branch { path, name, startPoint?, checkout } -> { ok: true }
POST /repo/delete-branch { path, name, force } -> DeleteBranchResult
POST /repo/delete-local-branch { repoPath, branchName, keepWorktree? } -> { ok: true }
POST /repo/update-from-base { path, branchName, strategy? } -> string
POST /repo/switch-branch { repoPath, branchName, force, stash } -> SwitchBranchResult
POST /repo/merge-archive-worktree { repoPath, branchName, targetBranch, afterMerge } -> MergeArchiveResult
Powers the Git panel's Branches tab, commit graph, and editor gutter in
browser/PWA/remote. Mutations call the shared *_impl + invalidate_repo_caches.
run_diff_triage (event-emitting, LLM progress) is not yet mapped — it belongs with
the agent/chat/watcher event-bridge work; see todo.md.
Stash Endpoints
List Stashes
GET /repo/stash?path=/path/to/repo
Returns stash list.
Apply Stash
POST /repo/stash/apply
Content-Type: application/json
{ "repoPath": "/path/to/repo", "index": 0 }
Pop Stash
POST /repo/stash/pop
Content-Type: application/json
{ "repoPath": "/path/to/repo", "index": 0 }
Drop Stash
POST /repo/stash/drop
Content-Type: application/json
{ "repoPath": "/path/to/repo", "index": 0 }
Show Stash
GET /repo/stash/show?path=/path/to/repo&index=0
Returns diff of a stash entry.
Log Endpoints
Get Logs
GET /logs?limit=50&level=error&source=terminal
Retrieve log entries from the ring buffer (1000 entries max). All query params optional:
limit— max entries to return (0 = all, default: 0)level— filter by level:debug,info,warn,errorsource— filter by source:app,plugin,git,network,terminal,github,dictation,store,config
Push Log
POST /logs
{ "level": "warn", "source": "git", "message": "...", "data_json": "{...}" }
Clear Logs
DELETE /logs
Execute JS in WebView (debug)
POST /debug/invoke_js
{ "script": "return window.__TUIC__.terminals().length;" }
Executes JavaScript in the main WebView. Loopback-only (rejected with 403 from
non-localhost peers) — this is an RCE surface and is exposed on the local router only,
never the remote router. Fire-and-forget: the return value (return expr) and any
captured console.log/warn/error/info output are pushed to the ring buffer with
source="eval_js". Read the result back via GET /logs?source=eval_js&limit=1.
The only injected global is window.__TUIC__ (stores, terminals, plugins, …). Mirrors
the MCP debug action=invoke_js tool — both share log_routes::eval_debug_script. The
HTTP route is what makes the tauri dev build (which has no MCP stdio transport)
scriptable for diagnostics.
Configuration Endpoints
App Config
GET /config
PUT /config
Load/save AppConfig.
PUT /config merges its body onto the live config rather than replacing it, so
a caller may send only the fields it wants changed. Objects merge key by key;
arrays and scalars replace wholesale (an empty array still clears a list, ""
still blanks a string). A wrongly-typed field is a 400, never a silent default.
When the body moves services.server.{enabled,port,ipv6_enabled} or
services.auth.{username,password_hash}, the HTTP listener is rebound just as the
IPC save_config does, so the running process cannot keep serving a configuration
the disk no longer agrees with.
GET /config redacts remote-access secrets (services.auth.password_hash,
services.auth.session_token, services.relay.token, and
services.push.vapid_private_key). Secret presence is exposed only through
session_token_exists, token_exists, and vapid_private_key_exists.
Config / themes / notes / misc parity (story 066)
Browser/PWA parity for assorted stateless commands. Loopback router only.
Mutating/action routes carry the require_local_or_auth guard; reads do not.
GET /config/ai-prompts -> AiPromptsConfig
PUT /config/ai-prompts (AiPromptsConfig) -> { ok }
POST /config/repo-local-config { repoPath } -> { ok } (GET = read)
POST /config/branch-label { repoPath, branchName, label? } -> { ok }
POST /config/note-image { noteId, dataBase64, extension } -> string (path)
POST /config/note-assets/delete { noteId } -> { ok }
POST /config/note-assets/delete-batch { noteIds } -> { ok }
GET /config/themes -> ThemeEntry[]
POST /config/project-mcp-upstreams { repoPath, upstreamNames? } -> { ok }
POST /exec/shell-script { scriptContent, timeoutMs, repoPath } -> string [guarded]
GET /audio/output-devices -> AudioOutputDevice[] (empty on remote)
POST /agent/discover-session { agentType, cwd, claimedIds, agentPid?, envOverrides } -> string|null
POST /agent/claude-project-dir { cwd, claudeConfigDir? } -> string
POST /agent/open-in-custom { executable, args, ctx } -> { ok } [guarded]
POST /generators/generate { request } -> GeneratorResult [guarded]
GET /registry/plugins -> RegistryEntry[]
Intentionally NOT mapped (no frontend invoke() caller — YAGNI): load_app_config,
save_app_config, get_note_images_dir, process_prompt_content_shell_safe,
detect_claude_binary, mdkb_code_find. Skipped as integration/stateful (separate
follow-up): set_ansi_colors (PTY ring-buffer state), the mdkb_* daemon commands,
install_agent_mcp/remove_agent_mcp (config-file writes, also no caller).
Provider keyring + slot/ollama checks (story 072)
Browser/PWA parity for provider API-key storage (the OS keyring is proxied through
the server so remote clients never touch it directly) plus slot/Ollama connectivity
checks. Loopback router only; mutating routes carry the require_local_or_auth guard.
GET /config/provider-key/exists?providerId=<id> -> bool
POST /config/provider-key { providerId, key } -> { ok } [guarded]
DELETE /config/provider-key { providerId } -> { ok } [guarded]
POST /config/slot-test { slot } -> string (connection test result)
POST /config/ollama-models { providerId } -> string[] (discovered model ids)
The OAuth upstream flow (start_mcp_upstream_oauth / cancel_mcp_upstream_oauth) is
not mapped: start binds a loopback callback server and opens the OS browser, so
the redirect can't return to a remote/PWA client. Desktop drives it over IPC; browser
clients get a clean host-only error until the redirect UX is redesigned.
Hash Password
POST /config/hash-password
Content-Type: application/json
{ "password": "..." }
Returns bcrypt hash string.
Notification Config
GET /config/notifications
PUT /config/notifications
Load/save NotificationConfig.
UI Preferences
GET /config/ui-prefs
PUT /config/ui-prefs
Load/save UIPrefsConfig.
Repository Settings
GET /config/repo-settings
PUT /config/repo-settings
Load/save per-repository settings.
Repository Defaults
GET /config/repo-defaults
PUT /config/repo-defaults
Load/save default settings applied to new repositories.
Check Custom Settings
GET /config/repo-settings/has-custom?path=/path/to/repo
Returns true if the repo has non-default settings.
Repositories
GET /config/repositories
PUT /config/repositories
Load/save the repositories list.
Prompt Library
GET /config/prompt-library
PUT /config/prompt-library
Load/save prompt entries.
Notes
GET /config/notes
PUT /config/notes
Load/save notes (opaque JSON, shape defined by frontend).
MCP Status
GET /mcp/status
Returns MCP server status (enabled, port, connected clients).
MCP Upstream Status
GET /mcp/upstream-status
Returns status and metrics for all upstream MCP servers (connecting, ready, circuit_open, disabled, failed).
MCP Instructions
GET /mcp/instructions
Returns dynamic server instructions for the MCP bridge binary as {"instructions": "..."}.
Filesystem Endpoints
GET /fs/list?repoPath=/path/to/repo&subdir=src
GET /fs/search?repoPath=/path/to/repo&query=main&limit=50
GET /fs/search-content?repoPath=/path/to/repo&query=foo&caseSensitive=false&useRegex=false&wholeWord=false&limit=200
GET /fs/read?repoPath=/path/to/repo&file=src/main.rs
GET /fs/read-external?path=/absolute/path/to/file
POST /fs/write { "repoPath": "...", "file": "...", "content": "..." }
POST /fs/mkdir { "repoPath": "...", "dir": "..." }
POST /fs/delete { "repoPath": "...", "path": "..." }
POST /fs/rename { "repoPath": "...", "from": "...", "to": "..." }
POST /fs/copy { "repoPath": "...", "from": "...", "to": "..." }
POST /fs/gitignore { "repoPath": "...", "pattern": "..." }
GET /fs/resolve-terminal-path?cwd=/repo&candidate=src/x.ts -> ResolvedFilePath | null
GET /fs/stat?path=/absolute/path -> PathStat (exists/is_dir/size/modified_at)
POST /fs/warm-index { "repoPath": "..." } -> { "ok": true } (fire-and-forget BM25 build)
POST /fs/write-external { "path": "/abs", "content": "..." } -> { "ok": true }
POST /fs/copy-abs { "from": "/abs", "to": "/abs" } -> { "ok": true }
POST /fs/move-abs { "from": "/abs", "to": "/abs" } -> { "ok": true }
POST /fs/transfer { "destDir": "/abs", "paths": [...], "mode": "move"|"copy", "allowRecursive": bool } -> TransferResult
Sandboxed filesystem operations for the file manager panel. /fs/read-external reads an arbitrary absolute path (not sandboxed to a repo).
Claude Usage Endpoints
GET /claude/usage -> UsageApiResponse (rate-limit usage, 5-min cached)
GET /claude/projects -> ProjectEntry[]
GET /claude/timeline?scope=all&days=7 -> TimelinePoint[] (hourly token aggregation)
GET /claude/session-stats?scope=current -> SessionStats
Powers the Claude Usage dashboard in browser/PWA/remote. scope is "all",
"current", or a project slug. timeline/session-stats are desktop-only Tauri
commands; the handlers call non-gated *_impl siblings so they also serve the
remote daemon.
Absolute-path write boundary. /fs/write-external, /fs/copy-abs, and /fs/move-abs are gated to registered repository roots for the HTTP boundary (a 403 otherwise), mirroring /fs/read-external. The gate rejects traversal syntax (..), NUL bytes, and relative paths before the containment check: containment is Path::starts_with, which is purely lexical, so /repo/../../etc/passwd is "inside" /repo by components while the OS resolves it far outside. Paths are deliberately not canonicalized — a symlink inside a registered repo that points outside it is an accepted design decision in this project. /fs/transfer gates only its destDir — sources are commonly external (a file dragged in from the desktop). /fs/stat and /fs/resolve-terminal-path return only metadata (no content) so they are not repo-gated; both also refuse macOS TCC-protected directories. /fs/resolve-terminal-path returns JSON null on a miss (Option<ResolvedFilePath>).
Monitoring Endpoints
Health Check
GET /health
Returns { "status": "ok" }.
Orchestrator Stats
GET /stats
Returns { "active_sessions": N, "max_sessions": 50, "available_slots": N }.
Session Metrics
GET /metrics
Returns { "total_spawned": N, "failed_spawns": N, "bytes_emitted": N, "pauses_triggered": N }.
Local IPs
GET /system/local-ips
Returns list of local network interfaces and addresses.
Local IP (Primary)
GET /system/local-ip
Returns the preferred local IP address (single value).
Watcher Endpoints
Head Watcher
POST /watchers/head?path=/path/to/repo
DELETE /watchers/head?path=/path/to/repo
Start/stop watching .git/HEAD for branch changes. Browser-only mode.
Repo Watcher
POST /watchers/repo?path=/path/to/repo
DELETE /watchers/repo?path=/path/to/repo
Start/stop watching .git/ for repository state changes. Browser-only mode.
Directory Watcher
POST /watchers/dir?path=/path/to/directory
DELETE /watchers/dir?path=/path/to/directory
Start/stop watching a directory (non-recursive) for file changes (create/delete/rename). Emits dir-changed SSE event. Used by File Browser panel for auto-refresh.
Hot Repos
PUT /watchers/hot-repos
Body: {"paths": ["/path/to/repo", ...]}
Updates the set of "hot" repository paths (repos with active terminals). Cold repos (not in this set) get throttled watcher debounce (15s vs 1.5s) and reduced GitHub polling frequency (~10min vs ~1min). Browser-only mode equivalent of the set_hot_repos Tauri command.
AI Watchers (agent rules — story 070)
GET /ai/watchers -> WatcherRule[]
POST /ai/watchers { name, sessionId?, trigger, instructions?, promptId?, repoPath?, maxFires?, cooldownSecs? } -> id
POST /ai/watchers/update { id, name?, trigger?, instructions?, promptId?, repoPath?, maxFires?, cooldownSecs? } -> { ok }
POST /ai/watchers/delete { id } -> { ok }
POST /ai/watchers/toggle { id, enabled } -> { ok }
POST /ai/watchers/attach { templateId, sessionId } -> id
POST /ai/watchers/detach { id } -> { ok }
CRUD for the agent watcher rules (WatcherManager). Watcher fires surface as the
existing session-created SSE event (a fired watcher spawns an agent session), so no
dedicated watcher-fire stream is needed. Config mutations are client-initiated → the UI
refetches GET /ai/watchers; no push event for state changes. The mutation logic is the
shared ai_agent::watcher::*_rule core; watcher_create/watcher_update reuse the
extracted *_impl.
AI Chat (config + conversation CRUD — story 069 RPC slice)
GET /ai/chat/config -> AiChatConfig
PUT /ai/chat/config (AiChatConfig) -> { ok }
GET /ai/chat/conversations -> ConversationMeta[]
GET /ai/chat/conversation?id= -> Conversation
POST /ai/chat/conversation (Conversation) -> { ok } (save)
POST /ai/chat/conversation/delete { id } -> { ok }
POST /ai/chat/new-id -> string (new conversation id)
File-backed conversation persistence + chat config.
GET (WS) /ai/chat/{chat_id}/stream
Chat registry live stream (event-bridge plan Step 4). WebSocket upgrade: the first
frame is a ChatEvent::Snapshot ({"kind":"snapshot",...}), then live ChatEvent
frames (chunk/error/cleared/snapshot) as they are fanned out. Closing the
socket unsubscribes (no explicit chat_unsubscribe call). Browser parity for the
desktop chat_subscribe Tauri Channel — frames are byte-identical so the same
applyRegistryEvent handler consumes both. Dedicated per-chat WS, NOT the global
/events bus (high-frequency token stream).
AI Agent Loop control + knowledge + scheduler (story 068 RPC slice)
POST /ai/conversation/cancel { sessionId } -> string
POST /ai/conversation/pause { sessionId } -> string
POST /ai/conversation/resume { sessionId } -> string
POST /ai/conversation/approve { sessionId, approved } -> { ok }
GET /ai/session-knowledge?sessionId= -> SessionKnowledgeSummary
POST /ai/suggestions/toggle { sessionId } -> bool (new state)
POST /ai/knowledge/sessions { filter?, limit? } -> SessionListEntry[]
GET /ai/knowledge/session?sessionId= -> SessionDetail | null
GET /ai/scheduler/config -> SchedulerConfig
PUT /ai/scheduler/config (SchedulerConfig) -> { ok }
POST /ai/triage/run { repoPath, refresh? } -> TriageResult (desktop only)
POST /ai/improvements/scan { repoPath, focus } -> ImprovementScanResult (desktop only)
POST /repo/create-issue-from-proposal { repoPath, proposal } -> CreatedIssue (desktop only)
GET (WS) /ai/conversation/{session_id}/stream
Agent-loop control (cancel/pause/resume/approve), session-knowledge reads, and the
scheduler config. State-taking commands reuse extracted *_impls
(get_session_knowledge_impl, toggle_ai_suggestions_impl,
get_knowledge_session_detail_impl).
Conversation token stream (event-bridge plan Step 3): the WebSocket
/ai/conversation/{session_id}/stream is the browser parity for the desktop
start_conversation Tauri Channel. The client sends the start params as the first
text frame — { message, autonomy?, maxSteps?, temperature?, modelOverride?, bypassedTools?, reasoningEffort? } — then receives ConversationEvent frames
({"type":"text_chunk",...} etc.) with the same 50ms batching as desktop. Dedicated
per-session WS, NOT the global /events bus (high-frequency token stream). A client
disconnect stops forwarding but leaves the conversation running — cancel explicitly
via /ai/conversation/cancel.
Diff triage (POST /ai/triage/run, event-bridge plan Step 2): triggers
run_diff_triage; progress frames stream over the global /events SSE bus as
triage-progress (low-frequency, safe on the bus). Desktop-only — the triage LLM
pipeline needs the desktop providers, so the remote daemon does not serve it.
Improvement proposals (POST /ai/improvements/scan) run a one-shot Headless-slot
LLM pass over deterministic local repo context (working-tree status + recent commits)
and emit proposals-ready on the same GitHub Ops event shape. The scan never creates
GitHub issues. A user action calls POST /repo/create-issue-from-proposal, which
wraps the existing create_issue_impl path and returns { number, url, title }.
Agent Endpoints
Detect All Agents
GET /agents
Returns detected agent binaries and installed IDEs.
Detect Specific Agent
GET /agents/detect?binary=claude
Returns detection result for a specific agent binary.
Detect Installed IDEs
GET /agents/ides
Returns list of installed IDEs.
Prompt Endpoints
Process Prompt
POST /prompt/process
Content-Type: application/json
{ "content": "...", "variables": { ... } }
Substitutes {{var}} placeholders in prompt text.
Extract Variables
POST /prompt/extract-variables
Content-Type: application/json
{ "content": "..." }
Returns list of {{var}} placeholder names found in content.
Plugin Endpoints
List Plugins
GET /plugins/list
Returns array of valid plugin manifests.
Plugin Development Guide
GET /plugins/docs
Returns the complete plugin development reference as {"content": "..."}. AI-optimized documentation covering manifest format, PluginHost API, structured event types, and example plugins.
Plugin Data
GET /api/plugins/:plugin_id/data/*path
Reads a plugin's stored data file. Returns application/json if content starts with { or [, otherwise text/plain. Returns 404 if the file doesn't exist. Goes through the same auth middleware as all other routes.
Note: write_plugin_data maps to POST /api/plugins/:plugin_id/data/*path; delete_plugin_data has no HTTP route (no frontend caller). Data is sandboxed to ~/.config/tuicommander/plugins/{plugin_id}/data/.
Plugin RPC (host capabilities, story 071)
Browser/PWA parity for the plugin host RPC surface. Every route is :plugin_id-scoped
and reuses the same per-plugin sandboxing as the Tauri commands (plugin_fs.rs path
jail, plugin_http.rs allowed-URL check, plugin_exec.rs binary whitelist).
GET /api/plugins/:plugin_id/fs/read?path=<p> -> string (plugin_read_file)
GET /api/plugins/:plugin_id/fs/read-base64?path=<p> -> string (plugin_read_file_base64)
GET /api/plugins/:plugin_id/fs/tail?path=<p>&maxBytes=<n> -> string (plugin_read_file_tail)
GET /api/plugins/:plugin_id/fs/list?path=<p>&pattern=&sortBy= -> string[] (plugin_list_directory)
POST /api/plugins/:plugin_id/fs/write { path, content } -> { ok } (plugin_write_file)
POST /api/plugins/:plugin_id/fs/rename { from, to } -> { ok } (plugin_rename_path)
POST /api/plugins/:plugin_id/build-artifacts/scan { repoPaths, forceRefresh? } -> BuildArtifact[]
POST /api/plugins/:plugin_id/build-artifacts/delete { path, repoPaths } -> { ok }
POST /api/plugins/:plugin_id/exec { binary, args, cwd? } -> string (plugin_exec_cli)
POST /api/plugins/:plugin_id/http { url, method?, headers?, body?, allowedUrls } -> HttpResponse
GET /api/plugins/:plugin_id/pty/output?sessionId=<id>&maxLines= -> string (plugin_read_session_output)
POST /api/plugins/:plugin_id/register { capabilities } -> { ok }
POST /api/plugins/:plugin_id/unregister -> { ok }
GET /api/plugins/:plugin_id/readme -> string | null
Build-artifact scans normalize the root set, share an in-flight scan across callers,
and reuse completed results for 30 seconds. Set forceRefresh: true to bypass a
completed cached result; a scan already running for the same roots remains shared.
Intentionally not mapped (native/host-only, stay Tauri-only): plugin_watch_path /
plugin_unwatch (change events need AppHandle/WS delivery), plugin_read_credential
(OS keychain), and user-plugin install/uninstall (install_plugin_from_*,
uninstall_plugin — local-FS install + AppHandle emit). delete_plugin_data is unmapped
for lack of a frontend caller (YAGNI).
Worktree Endpoints
List Worktrees
GET /worktrees
Returns list of managed worktrees.
Create Worktree
POST /worktrees
Content-Type: application/json
{ "base_repo": "/path", "branch_name": "feature-x" }
base_repo must be an absolute, normalized path. The route rejects invalid paths
before invoking git, matching MCP repo action=worktree_create validation.
Worktrees Base Directory
GET /worktrees/dir
Returns the base directory where worktrees are created.
Get Worktree Paths
GET /worktrees/paths?path=/path/to/repo
Returns { "branch-name": "/worktree/path", ... }.
Generate Worktree Name
POST /worktrees/generate-name
Content-Type: application/json
{ "existing_names": ["name1", "name2"] }
Returns a unique worktree name.
Finalize Merged Worktree
POST /worktrees/finalize
Content-Type: application/json
{ "repoPath": "/path/to/repo", "branchName": "feature-x", "action": "archive" }
Finalizes a merged worktree branch. action must be "archive" (moves to archive directory) or "delete" (removes worktree and branch).
For action: "delete", the response includes branch_delete_warning when the worktree was removed but safe branch deletion failed, for example because the branch has unmerged commits.
Remove Worktree
DELETE /worktrees/:branch?repoPath=/path&deleteBranch=true
Query parameters:
repoPath(required) -- base repository pathdeleteBranch(optional, defaulttrue) -- whentrue, also deletes the local git branchforce(optional, defaultfalse) -- whentrue, uses forced worktree removal and forced branch deletion
Returns { "ok": true, "branch_delete_warning": null } on full success. When deleteBranch=true and git branch -d refuses to delete the branch after the worktree is removed, the request still succeeds with branch_delete_warning set so clients can report the partial outcome.
Push Notification Endpoints
Get VAPID Public Key
GET /api/push/vapid-key
Returns the VAPID public key for PushManager.subscribe(). No authentication required.
Response: { "publicKey": "<base64url>" }
Returns 404 if push is not enabled.
Subscribe
POST /api/push/subscribe
Content-Type: application/json
{ "endpoint": "https://...", "keys": { "p256dh": "...", "auth": "..." } }
Register a push subscription. Idempotent (same endpoint updates keys).
Push delivery is gated by desktop window focus: notifications for question and session completion events are sent whenever the desktop window is not focused (including when the app is minimized or the user is on another workspace). This avoids duplicate alerts while the user is actively at the desktop, and still wakes the PWA service worker when the phone is locked.
Unsubscribe
DELETE /api/push/subscribe
Content-Type: application/json
{ "endpoint": "https://..." }
Remove a push subscription by endpoint.
Tauri-Only Commands (No HTTP Route)
The following commands are accessible only via the Tauri invoke() bridge in the desktop app. They have no HTTP endpoint.
| Command | Module | Description |
|---|---|---|
get_claude_usage_api | claude_usage.rs | Fetch rate-limit usage from Anthropic OAuth API |
get_claude_usage_timeline | claude_usage.rs | Get hourly token usage timeline from session transcripts |
get_claude_session_stats | claude_usage.rs | Scan session transcripts for aggregated token/session stats |
get_claude_project_list | claude_usage.rs | List Claude project slugs with session counts |
plugin_watch_path | plugin_fs.rs | Start watching path for changes (change events need AppHandle/WS) |
plugin_unwatch | plugin_fs.rs | Stop watching a path |
plugin_read_credential | plugin_credentials.rs | Read credential from system store |
fetch_plugin_registry | registry.rs | Fetch remote plugin registry index |
install_plugin_from_zip | plugins.rs | Install plugin from local ZIP file |
install_plugin_from_url | plugins.rs | Install plugin from HTTPS URL |
uninstall_plugin | plugins.rs | Remove a plugin and all its files |
get_agent_mcp_status | agent_mcp.rs | Check MCP config status for an agent |
install_agent_mcp | agent_mcp.rs | Install TUICommander MCP entry in agent config |
remove_agent_mcp | agent_mcp.rs | Remove TUICommander MCP entry from agent config |