Skip to content

HTTP + WebSocket API

At a glance

condash runs a local axum server bound to 127.0.0.1:<port>. The main condash binary wraps it in a Tauri window; condash-serve runs the same server headless for browsers and automation. 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, /fragment/{history,knowledge,code,projects} Page HTML, favicons, per-node + per-pane fragments
Change polling /check-updates, /events Per-node fingerprints (legacy reloadNode); SSE stream that drives htmx tab refreshes
Notes /note, /note-raw, /note/* Read, edit, rename, create, upload
Assets /asset/{*path} Streaming bytes for PDFs, images, arbitrary files under the conception tree
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 /configuration, /config, /recent-screenshot Config r/w, summary, screenshot-paste lookup
Vendored assets /vendor/{*path} PDF.js + xterm.js + CodeMirror + Mermaid + htmx 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 node (the legacy per-id surface; called from reloadNode after explicit mutations)
GET /fragment/history?q=<query> History pane content. Empty q → month-grouped tree; non-empty q → search-results list. Driven by htmx on #history-content
GET /fragment/knowledge Knowledge tree pane content. Refreshed on sse:knowledge
GET /fragment/code Git strip pane content. Refreshed on sse:code. Runner-viewer mounts inside carry hx-preserve="true" so xterm + WebSocket-attached terminals survive the morph swap
GET /fragment/projects Cards-grid pane content. Refreshed on sse:projects. Each card has id="<slug>" so Idiomorph keys swaps by it; client htmx:beforeSwap/afterSwap hooks re-apply expanded class + active-subtab visibility

/fragment ids (legacy per-node):

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 Per-node fingerprint map. Now only used by reloadNode callers to refresh a single subtree's baseline after an explicit mutation; the htmx-driven panes don't poll
GET /events Server-sent events stream. Each frame is a named event (event: projects / knowledge / code) carrying {tab, ts, file?}; hx-trigger="sse:<tab>" on each pane re-fetches the matching /fragment/<tab>

/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.

The History tab's full-text search route used to be GET /search-history?q=<query> returning JSON. It moved to the htmx-driven GET /fragment/history?q=<query> (HTML) when the History tab migrated; the JSON route is gone.

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 /asset/{*path} Any file under the conception tree — PDFs, images embedded in Markdown previews, deliverables, anything linked from a note. 24-hour public cache.

path is taken relative to conception_path. The handler canonicalises it, refuses to escape the tree, and 403s on a .. / null-byte / sandbox-escape attempt; 404s if the file is missing. Content-Type is inferred from the file extension via mime_guess.

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 + config

Method Path Purpose
GET /configuration Raw <conception_path>/configuration.yml as text/yaml. Empty string if the file does not exist.
POST /configuration Replace configuration.yml. Body is the plain-text YAML (not JSON) the gear modal's textarea contains. The handler parses the body, rejects invalid YAML with 400 + parse error, and atomically writes via .tmp + rename.
GET /config Small JSON summary the frontend polls on load: {conception_path, terminal}. Used for the first-run setup banner and to pick up terminal-shortcut changes without a full reload. Does not return the full runtime config.
GET /recent-screenshot {path, dir, reason?} — path of the newest image file in terminal.screenshot_dir.

Clipboard

There is no HTTP clipboard endpoint. The dashboard reads and writes the system clipboard through tauri-plugin-clipboard-manager in the Tauri build, and falls back to the browser's native navigator.clipboard API when run under condash-serve in a browser.

/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/{*path} Bundled third-party assets embedded in the binary. 24-hour public cache.

The single route covers four vendored subtrees:

Prefix Contents
/vendor/pdfjs/… Mozilla PDF.js (worker, cmaps, fonts, wasm, iccs).
/vendor/xterm/… xterm.js library + CSS + addon-fit.
/vendor/codemirror/… CodeMirror 6 editor bundle used by the note editor and the gear modal's YAML pane.
/vendor/mermaid/… Mermaid's UMD bundle for rendering Mermaid code blocks inside the note preview modal.
/vendor/htmx/… htmx core + the SSE + Idiomorph extensions. Drives the per-tab fragment refresh on sse:<tab> and identity-stable swap (cards, knowledge tree, runner mounts).

.. and null bytes are rejected; paths outside the bundled directory 403.

Why vendored: Tauri ships with the system's webview, and a CDN fetch for any of these bundles breaks offline / air-gapped installs. The binary stays self-contained by embedding all four libraries via rust-embed.

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 condash-serve with a pinned CONDASH_PORT — it prints the bound URL on startup. Without CONDASH_PORT set, the server asks the OS for any free port (bind(… 0)), so the port varies across launches; read it from the condash-serve: listening on … stderr line.