Skip to content

HTTP + WebSocket API

At a glance

condash runs a local FastAPI server bound to 127.0.0.1:<port>. The native window loads it via pywebview; in --no-native mode, you point a browser at the same URL. All routes are local-only — there is no auth layer, and condash never binds to a non-loopback address.

Groups:

Area Routes Purpose
Dashboard shell /, /favicon.*, /fragment Page HTML, favicons, partial re-renders
Change polling /check-updates, /search-history Fingerprints + global search
Notes /note, /note-raw, /note/* Read, edit, rename, create, upload
Assets /download, /asset, /file Streaming bytes for PDFs, images, arbitrary files
Mutations /toggle, /add-step, /edit-step, /remove-step, /reorder-all, /set-priority README edits
Openers /open, /open-doc, /open-folder, /open-external Launch external processes
Meta / clipboard /config, /clipboard, /recent-screenshot Config r/w, Qt clipboard, screenshot-paste lookup
Vendored assets /vendor/pdfjs/…, /vendor/xterm/… pdf.js + xterm.js bundles
Terminal WS /ws/term Interactive PTY

For mutation semantics (what each route writes), see Mutation model.

Dashboard shell

Method Path Returns
GET / Full dashboard HTML. Re-parses the conception tree on every call.
GET /favicon.svg, /favicon.ico Bundled SVG app icon
GET /fragment?id=<id> HTML subtree for one card or one knowledge directory

/fragment ids:

Shape Returns
projects/<priority>/<slug> One project card.
knowledge/<path>.md One knowledge card.
knowledge/<path> (dir) Knowledge directory subtree.
knowledge (root) 404 — client falls back to full-page reload.
Anything else 404.

Change polling

Method Path Purpose
GET /check-updates Cheap full-tree fingerprint — client polls every 5 s
GET /search-history?q=<query> Ranked search across README bodies, notes, filenames

/check-updates response shape:

{
  "fingerprint": "0123456789abcdef",
  "git_fingerprint": "fedcba9876543210",
  "nodes": {
    "projects": "…",
    "projects/now": "…",
    "projects/now/2026-04-18-helio-benchmark-harness": "…",
    "knowledge/topics/playwright-sandbox.md": "…"
  }
}

fingerprint is the 16-hex MD5 of the whole-tree repr; a change at any level flips it. nodes is a flat map that lets the client decide which subtree changed and re-fetch just that — preventing full-page flicker on a single step toggle. See internals for how the hashes are computed.

/search-history returns a list of per-item hits ranked by search.py::search_items. Empty q returns [].

Notes

All paths are relative to conception_path.

Method Path Body / Query Response
GET /note?path=<rel> HTML render of a Markdown / text / PDF / image note
GET /note-raw?path=<rel> {path, content, mtime, kind} for the edit view
POST /note {path, content, expected_mtime?} {ok, mtime} or 409 {ok: false, reason} on mtime drift
POST /note/rename {path, new_stem} {ok, path, mtime}
POST /note/create {item_readme, filename, subdir?} {ok, path, mtime}
POST /note/mkdir {item_readme, subpath} {ok, rel_dir, subdir_key} or 409 {reason: "exists"}
POST /note/upload multipart/form-data with item_readme, optional subdir, file parts {ok, stored: [...], rejected: [...]}

Upload size cap: 50 MB per file. Collisions auto-suffix (2), (3)

See mutations for the filename regexes and sandbox rules.

Asset streaming

Method Path Purpose
GET /download/{rel} PDF download with Content-Disposition: inline. Rejects non-PDF paths.
GET /asset/{rel} Image assets embedded in Markdown previews. 5-minute public cache.
GET /file/{rel} Any file under the conception tree — used by the in-modal PDF + image viewer. 60 s private cache.

All three re-validate the path against conception-tree regexes on every call. 403 on escape.

Mutations

All operate on an item's README.md by line number. See mutations for the effect on the file.

Method Path Body
POST /toggle {file, line} — cycles [ ]→[x]→[~]→[-]→[ ]
POST /add-step {file, text, section?}
POST /edit-step {file, line, text}
POST /remove-step {file, line}
POST /reorder-all {file, order: [line, line, …]}
POST /set-priority {file, priority} — one of now/soon/later/backlog/review/done

All return {ok: true, …} on success or {error: "<message>"} with 400 on validation failure.

Openers

These launch external processes. No filesystem writes — but they do mean "condash runs a shell command", so the sandbox regexes matter.

Method Path Body What runs
POST /open {path, tool} cfg.open_with[tool].commands chain. path must resolve under workspace_path or worktrees_path.
POST /open-doc {path} cfg.pdf_viewer chain for .pdf, OS default for everything else. path under conception_path.
POST /open-folder {path} OS default file manager. path must match projects/YYYY-MM/YYYY-MM-DD-slug/.
POST /open-external {url} User's default browser. URL must be http(s)://….

Meta, clipboard, config

Method Path Purpose
GET /config Full runtime config as JSON (merged TOML + YAML)
POST /config Save the config. Returns {ok, restart_required: [...], config}
GET /clipboard System clipboard text. Tries Qt QClipboard, then wl-paste / xclip / xsel.
POST /clipboard Set the system clipboard. Body is the raw text.
GET /recent-screenshot {path, dir, reason?} — path of the newest image file in terminal.screenshot_dir

GET /config returns a flat JSON matching the dashboard's gear-modal form. yaml_source / preferences_yaml_source show where the YAML fields currently come from (useful for debugging per-tree vs per-machine overrides).

/clipboard works in both native and browser mode: the Qt QClipboard path is taken when native=true; otherwise the subprocess fallbacks handle Wayland / X11.

/recent-screenshot powers the screenshot-paste shortcut. reason is one of directory does not exist, configured path is not a directory, permission denied, no image files found. The client pastes path into the active terminal tab without appending a newline.

Vendored assets

Method Path Purpose
GET /vendor/pdfjs/{rel} Mozilla PDF.js (worker, cmaps, fonts, wasm, iccs). 24-hour cache.
GET /vendor/xterm/{rel} xterm.js library + CSS + addon-fit. 24-hour cache.

Both routes reject .. and null bytes; files outside the bundled directory 403.

Why vendored: QtWebEngine ships with PdfViewerEnabled=false, so the in-modal viewer can't rely on the webview's built-in PDF renderer. And a CDN fetch for xterm.js breaks offline / air-gapped installs. See internals.

Terminal WebSocket

Method Path Purpose
WS /ws/term Interactive PTY session (Linux + macOS only)

Query parameters:

Param Meaning
session_id=<id> Reattach to an existing PTY session. If the id is unknown, the server sends {type: "session-expired"} and closes.
cwd=<path> Start the new shell in this directory. Must resolve under workspace_path / worktrees_path. Silently ignored otherwise.
launcher=1 Exec terminal.launcher_command instead of a login shell.

Frames, server → client:

Type Shape
Binary Raw bytes from the PTY — append to the xterm buffer verbatim.
Text JSON {type: "info", session_id, shell, cwd} First frame after attach.
Text JSON {type: "exit"} Shell exited. The server closes the socket immediately after.
Text JSON {type: "session-expired", session_id} Requested session is gone. Drop it from localStorage.
Text JSON {type: "error", message} Unsupported platform (Windows) or other fatal refusal.

Frames, client → server:

Shape Meaning
Binary Raw input to the PTY.
Text JSON {type: "resize", cols, rows} TIOCSWINSZ relay.

The PTY survives the WebSocket: a page refresh detaches cleanly and the buffer (256 KiB ring) replays on the next attach. See guide: using the embedded terminal for the end-user surface.

Auth, CORS, bind address

  • Server binds to 127.0.0.1 only. Non-loopback addresses are never used.
  • No auth layer. The sandbox is "only localhost traffic can reach the server".
  • No CORS headers — the dashboard lives on the same origin.
  • No multi-user mode; condash is single-user by design.

If you want to drive condash from a second tool, run both on the same host and talk to the loopback port. The port is printed by condash config show (when set) or picked at launch from 11111–12111 when port = 0.