Skip to content

Config files

Audience. Daily user.

condash reads two JSON files with disjoint schemas — every setting key lives in exactly one of them, so there are no overrides, no inheritance, and nothing to merge. The per-machine settings.json holds everything personal to you and this machine (appearance, terminal, launchers, open-with, the dashboard, …). The per-conception .condash/settings.json holds only what describes this tree — its workspace / worktree paths, its repo list, its retired handles, and its task config. Either file is optional — the dashboard runs with sensible defaults.

At a glance

File Path Lifecycle Owns exclusively
settings.json ${XDG_CONFIG_HOME:-~/.config}/condash/settings.json (Linux) · ~/Library/Application Support/condash/settings.json (macOS) · %APPDATA%\condash\settings.json (Windows) Per-user, per-machine Everything personal/per-machine — appearance, terminal, agents, open_with, pdf_viewer, dashboard, layout, … plus lastConceptionPath, recentConceptionPaths (cap 5)
.condash/settings.json <conception_path>/.condash/settings.json (legacy fallbacks: condash.json, configuration.json) Per-conception, per-host (gitignored) workspace_path, worktrees_path, long_lived_branches, repositories, retired_apps, taskConfig

The two files have disjoint schemas: each top-level key is valid in exactly one of them — see the full table below for which. There is no override, no inheritance, and no merge: a key only ever has one value, read from its owning file. Putting a key in the wrong file is a validation error on save; for files written under the old shared-schema model, the boot-time scope-partition migrator moves each mis-homed key into its owning file automatically on first open. The only field accepted in both files is the $schema_doc documentation pointer (which is not a setting).

The .condash/ workspace directory

.condash/ is condash's per-conception state directory — the home of settings.json plus terminal logs at .condash/logs/YYYY/MM/DD/HHMMSS-<sid>.txt. The whole directory is gitignored by default (the auto-migrator appends a .condash/ line to your .gitignore on first run when the conception is a git repo), so settings + logs are per-host state with no commit-leak risk. Teams that want to share a baseline config either commit condash.json alongside (legacy path still reads) or manually un-ignore settings.json in their .gitignore.

Reading and writing

  • Read precedence: <conception>/.condash/settings.json (canonical) → <conception>/condash.json (legacy) → <conception>/configuration.json (legacy²). Both legacy filenames are read indefinitely with no deprecation date.
  • Write target: every save through the GUI or condash config set writes to .condash/settings.json. The auto-migrator copies the legacy content into the new path on first open, tombstones the source file (so accidental edits don't drift), and appends .condash/ to .gitignore. Run condash config migrate to invoke it explicitly.
  • Key ownership: every setting has exactly one home — settings.json (personal/per-machine) or .condash/settings.json (this tree's paths, repos, and tasks). A key written to the wrong file is rejected by the strict schema; the scope-partition migrator relocates mis-homed keys on conception open. A conception file cannot set lastConceptionPath / recentConceptionPaths — a tree cannot describe its own location.
  • Environment override: CONDASH_CONCEPTION_PATH still wins for the session, matching the legacy behaviour.

Scope-partition migration

On every conception open — right after the legacy-filename migrator lifts any condash.json / configuration.json into .condash/settings.json — condash partitions both settings files so each holds only the keys it owns (per SCOPE_OF in src/main/config-schema.ts). A key found in the file that does not own it is moved to its owning file; if the owning file already sets that key, the two are reconciled by value type: an object value is deep-merged into the owning file's object (the owned-file value wins on any leaf conflict, so disjoint sub-keys that were split across the two files under the old override model — e.g. terminal.screenshot_dir in settings.json alongside terminal.logging in .condash/settings.json — are preserved), while a scalar or array value is dropped (the owned-file value wins wholesale). Every move, merge, and drop is logged. The pass is idempotent — once both files are partitioned, a re-run moves nothing — so a machine upgrading from the old shared-schema / override model has its settings split into the right files automatically on first open. (Implemented in src/main/scope-partition-migrate.ts.)

All config keys

Every top-level key, in one place. Scope is the one file the key lives in: global keys belong to the per-machine settings.json; conception keys belong to the per-tree .condash/settings.json. A key placed in the wrong file is rejected by the strict schema (and relocated by the scope-partition migrator on conception open).

Key Scope Type Default What it does
workspace_path conception string Root condash resolves repo paths against; populates the Code pane. Unset hides the pane.
worktrees_path conception string Extra sandbox root accepted by the "open in IDE" launchers.
repositories conception array [] Ordered Code-pane repo list; also the app registry (#handle, label, aliases, run, submodules, section).
retired_apps conception array [] Defunct #handles still referenced by closed projects — resolve, never rendered.
long_lived_branches conception array ["main", "master"] Branch glob patterns (* / ?) protected from condash worktrees remove. Unset → main + master.
agents global array [] Flat {id,label,command} terminal-launcher list shown in the tab-strip spawn dropdown.
taskConfig conception object Per-task {schedule?, timeout?, runMode?, excludeFromLogs?, gateOnUpdatedTabs?} keyed by slug — opt-in headless scheduling + run timeout + run mode (--prompt/--run) + run-log routing + activity gate.
open_with global object The three IDE/terminal launch slots (main_ide, secondary_ide, terminal).
pdf_viewer global array Ordered fallback chain of external PDF viewers.
terminal global object Shell, shortcuts, screenshot dir, xterm theming, logging, memory containment, project-action templates — one whole personal/per-machine key.
dashboard global object Live terminal-tab summarization (direct OpenAI-compatible endpoint, DeepSeek by default): {enabled, provider, apiKey, baseUrl, model, writerModel, cardReasoning, writerReasoning, cardInputChars, intervalSec, gateOnActivity, historyLimit}. Off by default; set it in Settings → Dashboard, which writes to the global file (the apiKey is a secret).
autoSync global object GUI-driven periodic committer: {enabled, intervalMinutes, quietPeriodSeconds, push}. While a conception is open, runs condash sync run on a timer. Off by default; set it in Settings → Auto-commit.
theme global enum system Colour theme: light (Paper) | mist (Mist) | dark (Warm Gallery) | nocturne (Nocturne) | console (Console) | system (follow the OS between Paper and Warm Gallery).
uiFonts global object Per-category UI typography {cardTitle, heading, body, code, terminal}, each a {family, weight, size} object. Any field left default keeps the theme's value for that surface.
layout global object Persisted pane layout, including leftView (projects | tasks | deliverables).
welcome global object { dismissed } — first-launch welcome-screen state.
cardMinWidth global object Per-surface minimum card width.
treeExpansion global object Remembered expand/collapse state of the tree panes.
selectedBranches global array [] Code-pane branch-filter selection.
branchFilterStickyAll global boolean true Branch filter "All (sticky)" mode — show every branch and auto-pin new ones.
lastConceptionPath global string|null null Currently-open conception path.
recentConceptionPaths global array [] Most-recently-opened paths, newest first (cap 5).

Task definitions are not a config key — they live on disk at <conception>/tasks/<slug>/ (see Tasks). Their per-task scheduling / log-routing lives in the taskConfig key above, keyed by slug. condash also persists a few small UI-state fields it manages itself (e.g. skillsActiveScope) in settings.json; you don't edit those by hand. Strict-mode validation rejects any unknown top-level key on save.

Config keys — shapes and ownership

The subsections below document the shape of each config key. Which file a key lives in is the Scope column in All config keys: conception keys (workspace_path, worktrees_path, long_lived_branches, repositories, retired_apps, taskConfig) live in .condash/settings.json; everything else is global and lives in settings.json. A few blocks documented here for convenience — terminal (and its logging / projectActions / newProjectActions / xterm), dashboard, open_with, agents — are global (personal/per-machine), not per-conception. No key is valid in both files.

.condash/settings.json (per-conception, per-host)

Lives at <conception_path>/.condash/settings.json. Don't commit it — the auto-migrator gitignores .condash/ for you. Every key is optional — a minimal valid file is {}, in which case condash uses globals (or built-in defaults) everywhere. Strict-mode validation: extra top-level keys are rejected on save.

Legacy filenames. Older trees ship condash.json (canonical before this migration) or configuration.json (legacy²) at the conception root. Both are still read; the migrator lifts their content into .condash/settings.json on first open. Don't hand-rename — let the migrator run.

{
  "workspace_path": "/home/you/src",
  "worktrees_path": "/home/you/src/worktrees",
  "repositories": [
    "condash",
    {
      "name": "helio",
      "submodules": [
        { "name": "apps/web", "run": "make dev" },
        { "name": "apps/api", "run": "make dev" }
      ]
    },
    {
      "name": "notes.vcoeur.com",
      "run": "make dev",
      "force_stop": "fuser -k 8200/tcp 5200/tcp"
    },
    "conception"
  ]
}

Only the six tree-shape keys (workspace_path, worktrees_path, long_lived_branches, repositories, retired_apps, taskConfig) are valid here; personal keys such as open_with, pdf_viewer, and terminal belong to the global settings.json and are rejected in a conception file (and lifted out by the scope-partition migrator if found). Use absolute paths for workspace_path / worktrees_path — there is no ~ expansion for path keys. (The one place ~ works is a leading ~/ in an open_with command token — see Per-OS recipes.) JSON does not carry comments — keep prose documentation in the project README or the per-tree CLAUDE.md.

terminal is not a conception key — the whole block (including its logging sub-block) lives in the per-machine settings.json and is edited once in Settings → Terminal, which live-rewrites settings.json. A boot-time migration in older condash builds lifted any pre-existing terminal block out of configuration.json; the scope-partition migrator now also lifts a terminal block out of any .condash/settings.json that still carries one (e.g. a tree configured under the old shared-schema model). Both passes are idempotent.

The legacy terminal.launchers array and the scalar terminal.launcher_command (condash ≤ 3.25) are dropped on the next write — the tab-strip dropdown is now populated from the top-level agents list. A legacy action-template launcher binding is renamed to agent in place.

Terminal logging

Inside the terminal block, terminal.logging configures per-session capture. Each pty spawn produces a single plain-text file at <conception>/.condash/logs/YYYY/MM/DD/HHMMSS-<sid>.txt. Metadata travels inside the file as two # condash: {...} JSON lines:

  • Header (line 1, always present) — { sid, side, repo?, cwd, cmd, argv, started }.
  • Footer (last line, only after the session exits) — { finished, exitCode }.

The body also carries periodic <!-- YYYY-MM-DD:HH:MM --> timestamp markers (local time), written at the markerIntervalSec cadence but only when new output has arrived since the previous marker — an idle session is never stamped. For an in-band transcript the marker sits inline at a message boundary; for a grid snapshot it lands in a trailing <!-- timeline --> block (a repaint can't host inline markers). The HTML-comment form is invisible in rendered markdown and skippable by a parser.

The writer pipes pty bytes through a headless xterm (@xterm/headless) and every 5 s reads the buffer via IBufferLine.translateToString(true). Rows that have scrolled above the viewport can never change again, so the body is appended: those rows are written past the end of the file once, and only the ≤ 50-row live tail is truncated and rewritten. A flush therefore costs what the session just printed, not what the buffer still holds. Output is plain UTF-8 — no SGR, no CSI, no cursor-forward — so the file is grep-friendly and the viewer needs no ANSI parser. Colour / bold / underline fidelity belongs to the live terminal's Save buffer button.

Typed keystrokes are not captured separately — the pty echoes them back through stdout, so the rendered buffer already shows what was typed. The Logs working surface (View → Show Logs, Cmd+Shift+L) lists sessions grouped by day; clicking a card opens a full-overlay viewer modal with virtualised text + case-insensitive search.

Key Type Default Meaning
enabled boolean false Toggle capture entirely. Default flipped to opt-in for privacy (2.25.0). Disabling stops new writes on the next session spawn; existing files stay on disk for the janitor. The Logs pane stays usable for browsing past transcripts even when disabled.
retentionDays integer ≥ 0 14 Day-directories older than this are removed on next janitor run. 0 disables age-based eviction (the size cap still applies).
maxDirMb integer ≥ 0 500 Total cap on <conception>/.condash/logs/. The janitor evicts oldest day-directories first while over cap, regardless of age.
scrollback integer ≥ 100 5000 Scrollback lines retained by the per-session headless xterm. It bounds how much output one flush can carry, not the file: an appended body keeps rows the buffer has since evicted, up to a 2 MB per-file cap. Output that scrolls past the whole buffer between two flushes is still lost.
markerIntervalSec integer ≥ 0 60 Wall-clock seconds between in-body <!-- YYYY-MM-DD:HH:MM --> timestamp markers. A marker is emitted only when new output arrived since the previous one, so an idle session is never stamped. Applies to both transcript and grid logs. 0 disables periodic markers.

A grid body is capped at 2 MB per file. The number is derived from these very defaults rather than borrowed: age stays the binding constraint only while a day's logs stay under ~35 MB (maxDirMb ÷ retentionDays), and at ~25 logged sessions a day that is ~1.4 MB per file — about what a saturated log already was before the body became append-only. 2 MB therefore roughly doubles the history a grid log keeps while staying in the size class these defaults were chosen against. The trade: a day in which 25 sessions all saturate reaches maxDirMb at ~10 days rather than 14; raise maxDirMb to keep the full 14. Typical sessions are far below the cap, so age normally still binds first. Past the cap the oldest half of the appended history is dropped at a row boundary on the next flush. The janitor runs at app startup and every 24 hours: it (1) deletes day-dirs older than retentionDays, then (2) evicts the oldest surviving day-dir while total size is over maxDirMb. No compression pass since v2.27.0 — plain .txt files are small enough at those caps that gzip is not worth the round-trip cost. Errors are logged to stderr and never propagate into the IPC layer.

Migration from maxFileMb / ansiPolicy: both fields were dropped from the schema in v2.23.0 when per-file rotation and ANSI stripping were retired. Settings files that still carry them (typically conceptions upgraded straight from ≤ 2.22) are scrubbed in-flight on every read and the legacy keys vanish from disk on the next settings write — no manual action.

Legacy formats — JSONL event streams from condash ≤ 2.22, compressed .txt.gz files from 2.23–2.26 — are ignored by the new viewer and global search. They sit on disk until the janitor's age-based eviction sweeps them. To clear them immediately, delete <conception>/.condash/logs/ and start fresh.

Dashboard

The dashboard block configures live terminal-tab summarization: a periodic loop in the main process reads the recent output of the open terminal tabs and summarizes it by POSTing directly to an OpenAI-compatible LLM endpoint (DeepSeek by default), surfacing the result as (1) LLM-derived tab titles, (2) a hover popover, and (3) the Dashboard body in the bottom band — a handle next to Terminal (Ctrl+Shift+D) swaps the band between the terminals and the dashboard. For a full-screen agent tab it reads the program's clean in-band transcript when one is emitted (claude / opencode), falling back to the cleaned scrollback for plain shells — so a repainting TUI is summarized from its real conversation, not frame noise. The Dashboard shows a card for every open terminal tab (a fallback from the tab's command/cwd until a summary exists), so an idle or not-yet-summarized tab is never invisible. The roster is the Terminal pane's tabs only — the Code-pane Run sessions (long-running dev servers) are panes, not agent tabs, so they are not counted in the top-line tab tallies nor rendered as cards. A card — and its entry in the working/idle tally — is dropped the moment its tab closes, every tick and without an API call, so a status never outlives the tab it describes. Each card also carries an Update button that forces an immediate re-summarization of that one tab, bypassing the interval and the activity gate. It is off by default — nothing runs and no data leaves the machine until you enable it. State (per-tab summaries + a rolling event history) persists at <conception>/.condash/dashboard/state.json.

Edit it once in Settings → Dashboard (under Personal · this machine). The whole dashboard block is editable there — the secret apiKey, the endpoint (baseUrl), both model tiers (model / writerModel), their reasoning toggles (cardReasoning / writerReasoning), the card input window (cardInputChars), the interval, and the activity gate. It is a personal/per-machine setting written to settings.json; it is not a conception key, so nothing about the dashboard is ever committed to a tree's .condash/settings.json. The section also carries a Test connection button that runs a one-shot completion against the entered key/URL/card model.

Key Type Default Meaning
enabled boolean false Master switch. Off → the engine is inert; on → summaries run on the interval.
provider enum deepseek Auth/registry namespace. Only deepseek is accepted today (modelled as an enum so others can be added later); a custom baseUrl lets it reach any OpenAI-compatible endpoint regardless.
apiKey string API key for the endpoint. Global file only. When unset, falls back to the DEEPSEEK_API_KEY environment variable.
baseUrl string OpenAI-compatible API base URL. Blank → the provider's built-in endpoint (https://api.deepseek.com). Set it to a self-hosted / proxy endpoint (e.g. an opencode-go server) to use any model id it serves. Falls back to DEEPSEEK_BASE_URL.
model string deepseek-v4-flash Card model — the cheap, high-volume tier that pre-processes each tab's wide window into state + facts + a draft title. Without a baseUrl it must be a built-in DeepSeek model; with a baseUrl, any id the endpoint serves. A config that sets only model (no writerModel) drives both tiers with it.
writerModel string deepseek-v4-pro Writer model — the richer tier that composes each card's published title (3–7 words) + one-sentence subtitle (the work's context and purpose) from the card facts plus the tab's derived provenance (app / worktree / project), falling back to the card model's draft title when its reply omits one. Defaults to model when a single-tier config is in use (so a single-model endpoint keeps working).
cardReasoning boolean false Whether the card model reasons. Off by default: card work is mechanical state+fact extraction, where hidden reasoning only adds latency (~3–5× slower) with no quality gain. Sent as DeepSeek's thinking:{type:disabled} when off.
writerReasoning boolean false Whether the writer model reasons. Off by default: a model bake-off found reasoning-on returns an empty reply on a non-trivial fraction of writer calls — unacceptable now that this tier owns the published title.
cardInputChars integer 16000 Chars of recent tab output fed to the card model (floored at 2000). Wider than the legacy 6000 because the cheap tier can afford a larger window.
intervalSec integer 120 Base seconds between a tab's refreshes — each tab runs on its own clock seeded from this plus a small per-tab jitter, clamped to 30–300.
gateOnActivity boolean true Skip a due tab's refresh when that tab produced no new output since its last summary (reuses the scheduler's per-tab growth gate). Off → refresh every due tab regardless.
historyLimit integer 20 Maximum retained events per tab and in the global history; older events roll off.

Privacy: enabling the dashboard transmits recent on-screen terminal output to the configured API endpoint — the DeepSeek API by default, or whatever baseUrl points at. Before any text leaves the machine it is run through the same secret redactor as condash logs --redact (provider key prefixes, bearer tokens, JWTs, secret-named assignments, PEM private-key blocks → «redacted:…»). That redactor is conservative by design and recognises only high-precision secret shapes, so it is a backstop, not a guarantee: leave the dashboard off for tabs that display credentials you don't want sent off-machine.

Auto-commit

The autoSync block turns condash into the single writer for a conception shared by parallel agent sessions: while a conception is open, a main-process engine runs condash sync run on a timer, committing every settled, non-gitignored change and pushing. It is the same sweep as the CLI verb, with the same safety — the non-blocking lock, the quiet-period mid-edit guard, the mid-merge/conflict refusal, and push-as-a-warning — so nothing here can commit a half-written file or rewrite the tree under a live session.

Field Default Meaning
enabled false Master switch. Off by default — the engine is armed but never commits.
intervalMinutes 10 How often to sweep and commit. Clamped to 1–120.
quietPeriodSeconds 90 A file edited more recently than this is left for the next sweep — the guard against committing mid-edit. Clamped to 0–3600; 0 commits even just-touched files.
push true Push after committing (a rejected push is a warning; the next sweep retries).

Edit it in Settings → Auto-commit (under Personal · this machine), which also carries a Commit & push now button (one sweep, regardless of the cadence) and a live status line (next-run ETA · last result · any error). It is a personal/per-machine setting written to settings.json — nothing about it is committed to a tree's .condash/settings.json.

The status bar surfaces the same engine at a glance: an auto-sync pill (synced / N to sync / syncing / failed / off) with its own Sync now button and a click-to-open list of the conception's most recent commits (each marked pushed or unpushed) — so you can see sync state and trigger a sweep without opening Settings. Alongside it, a shipped-skills pill shows whether the condash-shipped skills under .agents/skills/ are in sync, with an Install action (runs condash skills install) when any are missing or outdated.

The first commit lands one interval after you enable it, never the instant the app opens. To keep a file out of auto-commit, gitignore it. Two caveats: the engine only runs while the condash app is open (for headless commits, use the systemd timer), and the quiet period is the only thing standing between an in-progress edit and a commit — set it comfortably longer than your longest pause between keystrokes.

Workspace keys

Key Meaning
workspace_path Directory condash resolves relative repo paths against. Every direct subdirectory containing a .git/ shows up in the Code pane. If unset, the pane is hidden. Repos with an explicit path may live outside this root.
worktrees_path Additional sandbox for the "open in IDE" buttons. Paths outside workspace_path and worktrees_path are rejected before the shell sees them.
long_lived_branches Branch name patterns that condash worktrees remove refuses to delete. Supports glob wildcards * (any run of characters) and ? (one character) — e.g. main, release/*, hotfix-?. Defaults to ["main", "master"] when unset, so leaving it empty does not disable protection. A non-empty list replaces the default — it does not extend it — so keep main / master in the list if you still want them protected (e.g. ["main", "master", "release/*"]). Edited under Settings → Workspace & paths.

Since the reframe, two panes read hard-coded directories — no config key controls either:

  • The Skills pane reads the agedum sources at <conception_path>/.agents/skills/ (and ~/.config/agents/skills/ for the User scope). The former skills_path key was dropped; condash never reads compiled per-harness outputs (.claude/, .kimi/, …). See the Skills pane guide.
  • The Resources pane reads <conception_path>/resources/.

repositories

A single ordered array of repo entries — the Code pane renders cards in the order declared here. Entries take one of the following shapes:

{
  "repositories": [
    { "section": "Sites" },
    { "name": "alicepeintures.com", "run": "make dev" },
    { "name": "notes.vcoeur.com", "run": "make dev", "force_stop": "fuser -k 8200/tcp" },
    { "section": "Tools" },
    "condash",
    { "name": "helio", "submodules": ["apps/web", "apps/api"] }
  ]
}

| Shape | Effect | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bare string "condash" | Directory name (not a path) matched against the scan of workspace_path. | | {"name": "repo"} | Same as bare — the inline-object form coexists because a repo may want sibling keys. | | {"path": "/abs/or/rel/path"} | Override the filesystem location. The directory name is basename(path); name is optional when path is given. Relative path resolves against workspace_path; absolute path lets the repo live anywhere on disk. When path is absent, name continues to serve as the path (backwards compatible). Non-git directories are auto-detected and render as plain directory shortcuts instead of broken-repo cards. | | {"handle": "kasten", "path": "notes.vcoeur.com"} | The canonical #handle — the app's one public identity, used by both card pills (always rendered as #handle, coloured by handle), project README apps: lists, the generated AGENTS.md table, and search. Defaults to appHandle(name) (the directory name, sigil-stripped + lowercased) when omitted, so simple repos need not set it; domain-style or camelCase repos should (kasten, painting-manager). | | {"handle": "h", "aliases": ["OldName", "old-name"]} | Legacy spellings that resolve to this handle. condash applications validate flags a README apps: value matching an alias and suggests the #handle rewrite; rename records the old handle here automatically. | | {"name": "repo", "submodules": ["sub/a", "sub/b"]} | Renders the repo as an expandable row. Each submodule gets its own dirty count and "open with" buttons. Useful for monorepos where subtrees are edited independently. Submodule entries follow the same shape as parent entries (string or object). | | {"name": "repo", "run": "<cmd>"} | Wires an inline dev-server runner into that row. run is independent of submodules — a parent's run is not inherited by its submodules; add run per submodule if they each have their own dev server. | | {"name": "repo", "run": "<cmd>", "force_stop": "<cmd>"} | Same as above plus a repo-level force-stop button. The button runs force_stop as a shell command (/bin/sh -c on POSIX, cmd.exe /d /s /c on Windows), without going through condash's own process tracking — use it to free a port held by a server condash didn't start. Per-OS recipes for "kill whatever is holding port 8300": Linux fuser -k 8300/tcp or pkill -f 'manage.py runserver'; macOS lsof -ti tcp:8300 \| xargs kill -9; Windows for /f "tokens=5" %a in ('netstat -ano ^ | findstr :8300') do taskkill /F /PID %a. Same shell trust level as run — you're running these commands on your own machine, so a malicious tree is a malicious shell. | | {"name": "repo", "label": "<text>"} | Optional human-friendly label. The Code-pane card's primary pill is always the #handle; the label renders as a secondary subtitle beside it (falling back to the directory name when it differs from the handle). Useful when the handle is terse and a friendlier descriptor (Kasten, Alice PEINTURES) gives quicker context. Works on both top-level entries and submodules; combinable with run / force_stop / submodules. | | {"name": "repo", "install": "<cmd>"} | Install command run after condash worktrees setup creates a worktree — applied unconditionally when present (no flag needed); pass --no-install to skip. Typical: npm install for a Node repo, pip install -e . for a Python one. Same shell trust level as run. | | {"name": "repo", "env": [".env", ".env.local"]} | Files copied from the primary checkout into the worktree on condash worktrees setup — applied unconditionally when present (no flag needed); --no-env skips it. A new worktree gets every declared file (sources absent from the primary checkout are skipped); re-running setup on an already-present worktree backfills only the declared files it is missing, so a deliberately divergent copy (different ports to run two branches side by side) survives. Closes the silent-undefined-VITE_* footgun where forgetting --copy-env leaves a Vite SPA reading import.meta.env.VITE_* as undefined. Default empty → no copy. Path traversal is rejected (no .., no absolute paths). | | {"name": "repo", "pinned_branch": "<branch>"} | Pin the repo to a fixed branch. condash worktrees setup skips it instead of creating a worktree on the requested branch. Use for shared / vendored repos that should never track the project branch axis. | | {"section": "<heading>"} | Section marker — not a repo. Every repo that follows in repositories[] belongs to this section until the next {"section"} entry. The Settings modal renders a header row; the Code pane groups cards under the heading with an in-memory collapse toggle. Repos placed before the first marker live in an implicit default bucket that renders as today's flat list (no header). Top-level onlysubmodules cannot contain section markers. Carries no other field. |

Anything under workspace_path not named in repositories is ignored — only listed entries appear on the Code pane.

retired_apps

Defunct app handles that closed-project READMEs still reference but whose repos no longer exist. Each entry is { "handle": "<h>", "label"?: "<l>", "aliases"?: ["<old>"] }. They are validated against (so a historical #handle resolves) but never rendered as Code-pane cards and never appear in the generated Apps table. A handle is either live (in repositories) or retired (here) — never both.

{
  "retired_apps": [
    { "handle": "kasten-manager", "label": "KastenManager", "aliases": ["KastenManager"] }
  ]
}

open_with

Three vendor-neutral launcher slots used by the "Open with …" buttons on every repo row and note file:

Slot Typical use
main_ide Full IDE — IntelliJ IDEA, PyCharm, RustRover, WebStorm.
secondary_ide Lighter editor — VS Code, VSCodium, Zed.
terminal Spawn a terminal already cd-ed into the target.

Each slot takes a command string (required) and an optional label (tooltip text).

{
  "open_with": {
    "main_ide": {
      "label": "Open in main IDE",
      "command": "idea {path}"
    }
  }
}

{path} is substituted with the absolute path of the repo, worktree, or directory being opened. If the command isn't on $PATH, the button reports failure via a toast.

Schema note. condash (Electron) takes a single command string per slot — there is no commands list / fallback chain. If you need a fallback (e.g. idea then idea.sh), wrap it in a small launcher script that does the trial-and-fall-through itself.

There are no built-in defaults — a slot is functional only once it carries a command. Unconfigured slots are omitted from the "Open with" buttons, and launching one anyway reports open_with.<slot> is not configured. Configure each slot you want to use.

Per-OS recipes

The command is invoked directly (not through a shell) — ~/ and $VARS are not expanded except for a leading ~/ which condash rewrites to the user's home. Pick a recipe matching your OS:

Slot Linux macOS Windows
main_ide idea {path} · code {path} open -na "IntelliJ IDEA" --args {path} · open -na "Visual Studio Code" --args {path} idea64.exe {path} · code {path} (after VS Code's "Add to PATH" installer step)
secondary_ide codium {path} · zed {path} open -na "VSCodium" --args {path} · zed {path} code {path} · zed {path}
terminal gnome-terminal --working-directory {path} · konsole --workdir {path} · x-terminal-emulator --working-directory {path} open -a Terminal {path} · open -a iTerm {path} · open -a Ghostty {path} wt.exe -d "{path}" (Windows Terminal) · cmd.exe /K "cd /d {path}"

terminal

Embedded-terminal preferences. All keys are optional; an empty string means "fall back to the built-in default".

Key Default Meaning
shell $SHELL/bin/bash Absolute path to an interactive shell.
shortcut Ctrl+` Toggle the terminal pane. Modifiers: Ctrl, Shift, Alt, Meta. Key names follow the HTML KeyboardEvent.key convention.
screenshot_dir ~/Pictures/Screenshots on Linux, ~/Desktop on macOS Directory scanned for "most recent screenshot" by the paste shortcut.
screenshot_paste_shortcut Ctrl+Shift+V Inserts the absolute path of the newest image in screenshot_dir into the active terminal. No Enter — you confirm.
(agents) The tab-strip spawn dropdown lists agents from the top-level agents list — not a terminal.* key. See Agents below.
projectActions[] [] Configurable per-project actions — see terminal.projectActions below. Each entry adds an option to the dropdown next to a project's Work on button.
newProjectActions[] [] Configurable starter prompts — see terminal.newProjectActions below. Each entry adds an option to the dropdown next to the + New project button.
move_tab_left_shortcut Ctrl+Left Move the active tab to the left pane.
move_tab_right_shortcut Ctrl+Right Move the active tab to the right pane.
xterm {} xterm.js renderer settings — see terminal.xterm below. Editable through the Settings modal's Terminal section.
memory {} (enabled) Per-tab memory containment via a systemd user scope (Linux only) — see terminal.memory below.
perf {} (disabled) Main-process performance recording — see terminal.perf below. Off by default; toggled from the Settings modal's Terminal → Performance recording subgroup, or from the Performance pane.
autoRefreshOnTabSwitch true When true (default), any tab coming into view automatically runs Refresh — full-screen TUIs, plain shells, and agent sessions are all repainted, so a hidden tab never shows a stale snapshot. That covers a tab switch, the first tab shown after a restore or spawn (once per column, not once per restored tab), the tab that takes over when the active one closes (or exits cleanly), and the Dashboard→terminal flip / pane reopen (both columns of a split). Set explicitly to false to restrict auto-refresh to alternate-buffer tabs only (the previous default). See Hidden terminal tabs parse off the main thread.

Terminal perf

terminal.perf controls main-process performance recording. Off by default — while disabled the instrumentation is inert, so an ordinary run pays nothing.

Key Default Meaning
enabled false Record counters to <conception>/.condash/perf/YYYY-MM-DD.jsonl.

When enabled, condash appends one JSON record per sampling window (2.5 s — the same tick that drives the per-tab memory meter; recording adds no timer of its own). Each record carries event-loop delay percentiles for the main process and, per session, bytes and chunks read off the pty, time spent in the OSC transcript scan, time spent in the disk logger's ANSI parse, grid-render time, the disk log's flush cost with its compose / encode breakdown, coalesced IPC batches, backpressure pauses, and the un-acked in-flight high-water mark.

Cost and elapsed are different fields, and only one of them is a cost. syncFlushMs is the time a flush actually held the main thread — its synchronous stretches, summed — and is the field to weigh against loop.max. It contains gridRenderMs (which still covers GridBodyRenderer.render() and nothing else), composeMs (the file-text join) and encodeMs (the bookkeeping's second UTF-8 encode of the same text). flushMs and writeMs are elapsed across the xterm drain and five libuv round trips, so they absorb whatever else the event loop is doing: the same 1 MB write measures 2 ms on an idle loop and 209 ms behind an unrelated 26 ms-per-turn block, which is roughly the production median grid render. Use them for "how far behind the buffer did the log fall", never as main-thread cost, and never subtract them from loop.max — a session's flush elapsed can be dominated by a different session's work.

Two blocks beyond the terminal byte path ride the same record:

  • main — spans for work outside the byte path (dashRecentText, dashProvenance, transcriptRead, repoRecompute, gitStatus, gitUpstream, gitDetails), ipcMain.handle dispatch time bucketed by channel, and GC pauses. Only transcriptRead and dashRecentText are synchronous end to end; every other span is elapsed wall time, mostly subprocess or network wait during which main is free, so read them as "this was in flight", not as delay to subtract. The spans also nestrepoRecompute contains gitStatus and gitUpstream, dashRecentText contains transcriptRead — and concurrent same-name spans overlap, so summing them double-counts. ipc.*.ms is elapsed too, attributed to the window the handler finished in, and may exceed windowMs (an LLM-backed handler is seconds long); the instrument's own perfRendererReport channel is deliberately not timed. GC is reported as {n, ms, maxMs} whenever the runtime can observe it — an absent gc block means unobservable, n: 0 means no collection.
  • renderer — the renderer's own event-loop delay (computed by the same shared function main uses, so the two are comparable), animation-frame counts including long frames, spans for the visible tab's term.write, the tab-switch replay burst, the demote serialize(), the worker RPC round trip and the mount, counters for demotes / promotes / RPC failures / writes into a collapsed Code-pane row, and peaks (maxima) such as the transition buffer's depth. The write spans close on xterm's completion callback, since term.write only queues the parse — they are elapsed until processed, an upper bound on block time. The renderer drains on its own 2.5 s timer and sends one message per drain, and nothing at all for an empty window — never per frame. reports says how many drains merged into the window; counters sum, peaks take the max, and the loop percentiles are the worst of them, never an average.

Renderer loop delay excludes hidden time. Chromium throttles renderer timers to about 1 Hz while the window is occluded or minimised (backgroundThrottling is on), which would otherwise fill the file with ~990 ms "stalls" that never happened for as long as you look at another window. Samples taken while the page is hidden are discarded; renderer.hiddenMs says how much of the window was unmeasured for that reason, and renderer.samples says how many probe samples the percentiles rest on (~250 for a fully visible window). A window with a large hiddenMs is partly unmeasured, not quiet.

Which windows are recorded. Every window with anything to report: a session that moved bytes, a timed span, an IPC dispatch, an observed GC, a renderer report carrying real activity, or an event-loop delay of 5 ms or more. Only a window idle on all of those is dropped, which is what keeps an idle app from writing a record every 2.5 s forever — the same 5 ms threshold governs the renderer's decision to send at all, because if it reported unconditionally the main-side gate could never fire. Until schema 3 the rule was "some session moved bytes", which discarded every stall that had no terminal work in it — 20 % of the ≥ 100 ms stalls in the 2026-07 baseline. Records from before that change are not comparable: a percentile taken over a schema-2 file is conditioned on a tab having been busy. The schema field discriminates, and scripts/perf-load.mjs refuses to aggregate across it.

The event-loop delay is the most directly useful figure: main is a single thread shared by every terminal tab as well as git status, file watching, and all IPC, so its delay under load is the clearest measure of UI stalls. It is reported as delay in excess of the sampler's own 10 ms interval: monitorEventLoopDelay records the gap between its own timer firings rather than the excess over the expected gap, so a raw reading has a floor equal to its resolution — an idle process measures p99 ≈ 10.3 ms. Reporting that raw put a fixed ~10 ms on the headline figure for an idle app, which is both the symptom under investigation and a plausible magnitude for it. Subtracting the resolution is what makes an idle app read ~0. Delays genuinely below 10 ms are not resolvable and read as 0.

Two toggles flip recording, both writing terminal.perf.enabled and applying immediately: the Settings modal's Terminal → Performance recording checkbox, and the Performance pane's Record button. The pane also shows the live values; per-tab memory, growth rate, and throttle state are shown there whether or not recording is on, since they come from the always-on sampler.

Records are plain JSONL and are safe to delete. Recording accumulates roughly 10 MB/day with two active tabs and ~80 MB/day with twenty — plus, since schema 3, a ~300-byte record for each window that had non-terminal activity but no pty traffic (a git status, an IPC call, a stall). An app left open and genuinely untouched still writes nothing; one being used with no busy tab writes what its own watchers, IPC and stalls justify. So condash prunes the directory for you: a janitor runs at startup and every 24 h and deletes records older than 14 days, then — while the directory is still over 200 MB — the oldest remaining files until it is under. The current day's file is never deleted, since a live recorder is appending to it.

That means perf records are not long-term storage. If a run matters beyond a fortnight, or if a burst has pushed the directory over the cap, copy the JSONL somewhere else. .condash/ is gitignored in full in conceptions whose .gitignore carries that rule; a conception created from the bundled template does not yet ship one.

To reproduce load deliberately rather than waiting for it, scripts/perf-load.mjs drives N tabs at a controlled byte rate and reports the counters back — including an A/B of disk logging on versus off, which isolates the cost of the logger's duplicate ANSI parse on the main thread.

--profile selects the shape of that load, which matters more than the rate. The disk logger renders its grid body out of a headless xterm holding at most 5050 rows and flushes every 5 s, reusing the previous flush's frozen prefix — so the figure that decides what a run can measure is new rows per flush against 5050:

--profile Default rate Rows per 5 s flush Regime
flood (default) 512k ~17 600 3.5× full buffer turnover — worst-case saturation
realistic 16k ~806 16 % of the buffer, 84 % retained and reusable

flood is unchanged and stays the right tool for stressing the byte path, but it emits one 10 924-character line per chunk, which wraps at 200 columns to 55 rows. At 64 chunks/s that replaces the whole buffer three and a half times per flush, so nothing is ever retained — which makes any optimisation that depends on retained rows invisible to it by construction. Measured 2026-07-23, the v4.97.1 incremental grid render scores exactly zero improvement under flood and 1.4–1.6× under realistic.

realistic emits 80–119-character lines (one grid row each) in bursts separated by idle gaps, from a fixed seed so both arms of an --ab run see byte-identical input. Its default rate is deliberately not the flood's: short lines at 512k would land ~25 795 rows per flush — 5.1× turnover, deeper into saturation than the flood itself. It also forks about 100× less — the lines are literals and printf is a shell builtin, so only the per-burst sleep forks, against the flood's ~2000 process creations/second — which is why absolute constants read off flood are upper bounds rather than measurements of the byte path alone.

Every rows-per-flush figure above is keyed to the rate you request, which is what the harness computes and prints. The flood's post-base64 output is about a third higher (699 200 B/s at 512k), and the same arithmetic against that gives 34 401 rows and 6.8× — a different quantity, and one this page quoted in place of the other until 2026-07-23.

Reading the ms-per-render figure

A grid render costs O(retained buffer size), so a render taken while the buffer is still filling is not the same measurement as one taken after it is full. The profiles fill at very different speeds — about 1.4 s for flood, about 31 s for realistic at 16k — so averaging across that boundary is a systematic bias, and it runs in favour of whichever profile saturates first.

The harness therefore drops every pre-saturation window and reports gridRenderMsPerRenderSteady as the headline, alongside the sample count it rests on. Below three post-saturation windows or four renders it refuses to report one at all and says so loudly, rather than publishing a mean of two samples. The unfiltered whole-run mean is kept beside it as gridRenderMsPerRenderAllWindows, named for what it is. A realistic run therefore needs to outlast ~31 s by several flushes: 60 s is the practical minimum, 120 s gives a comfortable series.

Measured 2026-07-23 on one tab, matched pairs at 60 s and 120 s:

Profile Steady ms/render Whole-run ms/render Delivered vs nominal
flood @ 512k 53.5 / 54.0 49.1 / 51.7 84 %
realistic @ 16k 11.7 / 10.8 8.7 / 9.6 99 % / 100 %

That is 4.6–5.0× at steady state, where the unfiltered means would have read 5.7× and 5.4×.

The ratio is not decomposed here, and should not be. The two profiles differ in at least four ways at once — rate (32×), rows per line (55×), fork rate (114×), and steady versus bursty output — and a control run cannot vary them independently, because floodCommand derives its chunk size from the rate, tying line width to turnover. The observed ratio is the combined effect of all of them.

What realistic does and does not represent

It is a floor on the grid renderer's cost for a non-cooperating tab, not a portrait of a typical one:

  • Neither profile emits anything but printable ASCII and newlines — no alternate screen, no RIS, no \r progress bars, no CSI L. So GridBodyRenderer.invalidate() and the marker-anomaly path are never exercised and the frozen prefix is never dropped mid-run. Real tabs run TUIs and spinners that do exactly that, and pay more than this.
  • SessionLogger.flushNow writes the transcript body whenever the session has one, and falls back to the grid only otherwise. Cooperating agent tabs emit their transcript in-band over OSC and so never reach GridBodyRenderer at all. The grid path serves non-cooperating tabs, and that is the population these figures describe.

The per-tab byte rate the harness prints is a nominal ceiling — after base64 expansion for the flood, before delivery — not a measurement of what reached the app. Emitting output takes wall-clock the fixed sleep never subtracts, so the loop always runs slower than its own arithmetic: a flood run printing 682.8 KB/s delivered 566 KB/s, which is the documented fork overhead. The summary reports what actually arrived as a percentage of that ceiling and warns outside 70–105 %, so a one-liner that silently failed to run cannot exit 0 behind a plausible-looking summary.

The harness runs against a throwaway user-data dir and conception under /tmp, and asserts that isolation in the main process before applying any load — so it never touches your real settings.json, never floods your conception's log store, and never shares a perf JSONL with a running instance. It also refuses to start a run larger than 12 tabs, or one whose estimated working set exceeds available memory, unless --force is given — per-tab caps are per tab, and the documented field failure is whole-machine pressure, which they do not prevent.

Every other precondition is asserted at runtime too, and none of the assertions is ceremony. A harness that measures other software turns an unverified assumption into plausible false data rather than a crash, so each one has already caught a real defect: the isolation check caught --user-data-dir being silently overridden by the dev-mode userData redirect; the renderer check caught the app booting against a Vite dev server nobody had started, which left a dead renderer and a run that exited 0 with numbers off by up to 9×; and the GC-record count caught --trace-gc output going to stdout while only stderr was captured, so gc.log had never held a single GC record. Requires a current npm run build — both the main bundle and the renderer bundle.

Renderer CPU profile

Every counter above lives in the main process. --renderer-profile adds the one measurement the line never had — a CPU trace of the renderer main thread under the flood — by attaching a CDP Profiler to the renderer page (page.context().newCDPSession(page), then Profiler.enable / setSamplingInterval / startstop) around the flood window and writing a .cpuprofile beside perf.jsonl. It is a flag, not a sibling script, on purpose: the one property the harness exists to guarantee is isolation, so the renderer trace reuses the same sandbox, launch env, runtime assertions, memory guard and tab ceiling literally in place rather than re-deriving them. Opt-in and incompatible with --ab (a profile is a single trace); --profiler-interval sets the sampling interval in microseconds (default 250, finer than V8's 1000 so the OSC/clone/parse split resolves).

The trace is asserted real before it is trusted — a profile with no samples, or one whose samples are all synthetic (idle)/(program), means the profiler never attached to a busy renderer, and the run fails loudly rather than reporting a clean-looking empty trace (the exact hole --trace-gc hid). It also asserts the hidden-tab path is actually engaged: in the sandbox the terminal pane opens on the terminal view, so spawning N tabs leaves one visible DOM Terminal and demotes the other N-1 into the shared worker — and a demote removes the tab's DOM element, so .xterm collapsing to 1 is the runtime proof that the F7/F8 double-copy path is live. Rank a written profile with node scripts/analyze-cpuprofile.mjs <file.cpuprofile>, which aggregates self-time by function.

What it reaches: F7/F8 (the hidden-tab worker feed and the F8 IPC-deserialize-then-postMessage double copy, both on the renderer main thread). What it does not: F6, whose Code-pane run rows need a code-side session from a repo's Run button that the all-my-side flood cannot stage — scoped out rather than faked. The Profiler sees the page main thread only; the worker thread's own parse of the hidden tabs is a separate context, which is fine, because F8's claim is about the main-thread copy cost specifically. Measured 2026-07-23 under an 8-tab flood at machine load ~7.5 (a sampling profile is proportional, so self-time shares stay interpretable even though absolute ms are inflated by OS descheduling): among named renderer JS work, xterm ANSI parse/render of the single visible tab is ~5.7 s and the F8 structured-clone/postMessage path is ~0.2 s — a ~28× gap. F8 is real in the code but negligible in cost (a data-transfer bound puts the two copies of 160 MB at tens of ms of memcpy), so it is refuted as a bottleneck; the worker offload the architecture pays for is precisely what keeps the main thread parsing one tab instead of eight.

Terminal memory

On Linux with a systemd user manager and cgroup v2, condash spawns each terminal tab's pty inside its own transient systemd-run --user --scope carrying a memory ceiling. A tab that runs away — a leaking or over-eager agent — then trips its own cgroup's OOM killer and is killed alone, instead of the leak exhausting system RAM+swap and triggering a global OOM whose kill can land on condash's own renderer and take every tab down with it. On any other host the block is a no-op and tabs spawn directly. The tab strip shows each scoped tab's live usage, turning into a warning badge as it approaches the cap. Capability is probed with a throwaway scope; a success is cached, but a transient failure is re-checked on the next spawn — a momentary glitch (systemd busy under load, user manager restarting) never silently disables containment for the rest of the session. When a tab is nonetheless spawned uncapped on a capable host, condash logs a one-time warning.

The per-tab scope only binds processes spawned through the tab path. A child that skips it — a non-tab helper, or a tab left uncapped because the probe failed — stays in condash's own app-gnome-condash-*.scope, which carries no limit; a runaway there again escalates to a global OOM (this recurred and took down a whole GNOME session on 2026-07-05). The appScope backstop closes that: at startup condash caps its own app scope via systemctl --user set-property, so any child that escapes the per-tab cap trips condash's cgroup OOM instead of the machine's global one. Same Linux + systemd gate; a no-op elsewhere.

Each tab's scope is named condash-term-<session-id>.scope (scheduled task runs use condash-task-<id>.scope), so live tabs are listable with systemctl --user list-units --type=scope 'condash-*'. The name is not cosmetic: it is how condash identifies a tab's own cgroup. systemd-run execs the child before the user manager has created the unit and migrated it in, so reading /proc/<pid>/cgroup right after spawn returns condash's own app scope — which previously made every tab report the whole app's memory as its own and derive its death verdict from the app's counters. condash now waits for the named unit to appear before attributing any figure to a tab, and reports nothing rather than something wrong if it never does.

All keys optional; sizes are systemd size strings ("6G", "512M", "infinity").

Key Default Meaning
enabled true Master switch for the per-tab caps. Set false to force plain spawns everywhere. No effect on hosts without a systemd user manager + cgroup v2.
high "6G" Soft limit (MemoryHigh): the kernel throttles + reclaims the tab's cgroup past this, buying time before the hard wall.
max "8G" Hard limit (MemoryMax): the tab's cgroup is OOM-killed at this ceiling — what guarantees a leak kills only the one tab.
swapMax "2G" Swap ceiling (MemorySwapMax) so a capped tab can't instead exhaust system swap.
appScope {} (on) Backstop cap on condash's own app scope — see below.

Raise max for legitimately memory-hungry runs (e.g. a multi-agent session); the trade-off is a higher ceiling before a runaway is contained.

terminal.memory.appScope — the whole-session backstop. All keys optional.

Key Default Meaning
enabled true Set false to leave condash's app scope uncapped (per-tab caps still apply).
max physical RAM − 3 GB Hard limit (MemoryMax) on condash + any child not in its own tab scope. Floored at half RAM. Kept below total RAM so the cgroup OOM fires before the system's global one.
swapMax "2G" Swap ceiling (MemorySwapMax) on the app scope — the lever that stops a runaway from thrashing all of system swap into a global OOM.

Agents

Agents are a flat list of terminal launchers under the top-level agents key. The tab-strip spawn dropdown lists them (it always offers New shell first, then each agent by label). Picking one opens a new terminal tab running its command — that's the whole model. agents is a global key: these are personal launcher tools, identical no matter which conception is open, so the list lives once in the per-machine settings.json.

Favourites. Mark agents with "favorite": true to keep the dropdown short: it then shows New shell + the favourites (each prefixed with a ★) directly, and tucks every non-favourite under a More ▸ fly-out submenu. With no agent marked favourite, the dropdown lists every agent inline — so the split only takes effect once at least one agent opts in. Order within each group follows config order. A long More ▸ list never runs off-screen: the fly-out is capped to the viewport and wraps into multiple columns (scrolling only as a last resort), and flips left or up so the whole popup stays visible.

{
  "agents": [
    { "id": "claude", "label": "Claude", "command": "claude", "favorite": true },
    { "id": "claude-kimi", "label": "Claude · Kimi", "command": "claude-kimi", "favorite": true },
    { "id": "opencode-kimi", "label": "OpenCode · Kimi", "command": "opencode-kimi" }
  ]
}
Key Type Required Meaning
id string yes Stable identity referenced by tasks and the agent field of project / new-project actions. Non-empty.
label string yes Display name shown in the spawn dropdown and as the pinned tab title.
command string yes Shell command run on launch, in a fresh tab with the terminal's ambient environment. Point it at a wrapper on PATH or inline the invocation. Blank → skipped.
promptFlags bool no Default false. Set true when command understands agedum's --prompt / --run flags. Tasks and agent-bound actions then pass the prompt in argv — <command> --run "<prompt>" when submitting (non-interactive, exits) or <command> --prompt "<prompt>" otherwise (interactive, seeded) — instead of spawning the bare command and typing the prompt into the live TUI. Leave off for an opaque command (e.g. a raw claude).
favorite bool no Default false. Surface this agent directly in the spawn dropdown (prefixed with a ★); non-favourites move under a More ▸ fly-out. When no agent is marked, every agent is listed inline.

condash builds no provider environment and stores no secrets — model/provider wiring and any API token live entirely in command (usually a ~/bin wrapper script). See the Agent CLIs and model providers guide for wrapper recipes. Edit the list in the Settings modal's Launchers section (under Personal · this machine) or in settings.json directly. Migration: condash ≤ 3.25 had terminal.launchers + the scalar terminal.launcher_command; both are dropped on read. (A later per-file <conception>/agents/<slug>.json harness store was also replaced by this agents list.)

Tasks

Tasks are reusable, parameterized agent prompts — like agents, they live under the conception (not a condash.json key), managed by the Tasks pane (left activity rail → Tasks). A task is a referenced agent plus a markdown prompt with fillable {markers}.

  • Definition<conception>/tasks/<slug>/, one directory per task. task.json carries name, agent (the id of an agent from the agents list above), and submit (optional bool, default true); prompt.md is the raw markdown prompt with markers. Config in JSON, prose in markdown — both are safe to commit. The slug is the directory name (^[a-z0-9-]+$); the tasks/ tree is created on first save.
  • Markers{KEY} (required field) or {KEY:default} (prefilled). Reserved {APP} / {PROJECT} (and their {APP_PATH} / {PROJECT_BRANCH} / … sub-tokens) render as searchable pickers; one selection fills the whole family. {TABS} and {UPDATED_TABS} are condash-provided (never fields) — both expand to the open-tab list [{sid,cwd,repo,cmd}], {UPDATED_TABS} narrowed to the tabs that produced new output since the task's last scheduled run. A {KEY:default} marker must not have whitespace right after the : — code-like fragments such as {key: .sid} (e.g. inside an inline jq snippet) are not treated as markers.
  • Run — spawns the task's agent in a fresh terminal tab (cwd = conception root). For an opaque agent it types the substituted prompt and presses Enter when submit is true; for an agent with promptFlags it instead passes the prompt in argv per the task's run mode (--prompt interactive, or --run one-shot) and types nothing.
  • taskConfig — per-task scheduling + run mode + log routing, keyed by slug, in .condash/settings.jsonnot in task.json. It is a conception key (it describes this tree's tasks) and lives only in the conception file. Each entry is { schedule?, timeout?, runMode?, excludeFromLogs?, gateOnUpdatedTabs? }:
  • schedule — opt-in cadence (s / m / h / d, e.g. 5m / 1h / 7d). The editor takes a free-text cadence and shows the parsed interval beside the field. A scheduled task runs headless (no tab) on that interval, single-flighted (never overlaps its own still-running run); the tabs that changed since the last run are handed to it as {UPDATED_TABS} (see gateOnUpdatedTabs to also skip idle ticks). Its console output is teed to .condash/scheduled/<slug>/ (last ~5), never the normal logs. No default schedule; the task must carry a prompt-seedable agent.
  • timeout — per-run hard cap (same cadence syntax; absent = 10m). With runMode: oneshot the agent exits on its own and this is a pure backstop; with the default interactive it is also the discard mechanism for an agent that finishes its work but never exits. Keep it ≤ schedule or single-flight stretches the effective cadence to the timeout. The editor offers 1m / 5m / 10m / 30m / 1h.
  • runMode — per-task default for how a promptFlags agent is driven: interactive (agedum --prompt, the default — the session stays open) or oneshot (--run — runs the prompt once and exits). Overridable per run in the run popup. Prefer oneshot for a scheduled task so its headless run exits cleanly instead of being killed at timeout. Ignored for an opaque agent (keystroke path, interactive only).
  • excludeFromLogs — per-task default for routing a manual run's .txt to .condash/manual/<slug>/ instead of .condash/logs/ (overridable per run in the run popup). The tab stays visible.
  • gateOnUpdatedTabs — opt-in growth gate (default off). When true, a due tick is skipped unless some open tab produced new output since the task's last run, so a quiet workspace spends nothing. Leave it off (the default) and the task runs on every interval regardless of tab activity. Enable it only for a task that acts on {UPDATED_TABS}; a task that doesn't read updated tabs would be starved by it.

Both segregated stores are browsable from the Logs pane's Task runs view and stay invisible to the normal Logs list, search, and reports.

See the Tasks pane guide. The same {KEY:default} fallback applies to the project / new-project action templates below.

terminal.projectActions

Per-entry actions rendered in the per-card Work on dropdown on the Projects pane. The control is a single dropdown button: clicking it opens a menu whose first row is the built-in Work on action and whose remaining rows are the entries below. When projectActions is empty or missing, the menu still opens but contains only the default row.

{
  "terminal": {
    "projectActions": [
      {
        "label": "Claude review",
        "template": "claude \"review project {shortSlug}\"",
        "submit": true
      },
      {
        "label": "Kimi summary",
        "template": "summarise {shortSlug}",
        "submit": true,
        "agent": "kimi"
      }
    ]
  }
}
Key Type Required Meaning
label string yes User-defined name shown in the dropdown. Empty or whitespace is treated as the entry being unset (no dropdown option rendered).
template string yes Text pasted into the focused terminal when the entry is selected. May contain {slug}, {shortSlug}, {title}, {branch}, {base}, {kind}, {status}, {date}, {apps}, {firstApp}, {path}, {relPath}, and global placeholders ({today}, {conception}, {conceptionPath}). A {placeholder:default} form falls back to default when the placeholder is unknown; a default-less unknown placeholder is left verbatim so typos remain visible. Empty or whitespace is treated as the entry being unset.
submit bool no When true, condash presses Enter after pasting the template. Default false — matches the current Work on behaviour and lets templates that end with a colon wait for the user to type the variable bit.
agent string no When set, the id of an agent from the agents list. The action spawns a fresh tab running that agent's command before typing the template — useful for binding an action to a specific agent. Empty / missing → type into the focused tab (a plain shell when no tab exists). An id that no longer matches an agent falls through to the focused-tab flow.

terminal.newProjectActions

Per-entry starter prompts rendered in the + New project dropdown. The control is a single dropdown button: clicking it opens a menu whose first row opens the New project modal (the built-in default) and whose remaining rows are the configured starter prompts. When newProjectActions is empty or missing, the menu still opens but contains only the default row.

{
  "terminal": {
    "newProjectActions": [
      {
        "label": "Spec + design starter",
        "template": "start project for new feature, make spec.md note with functional specification, and design.md note with design plan:",
        "submit": false
      },
      {
        "label": "Start new project (Claude)",
        "template": "Start new project ",
        "agent": "claude-kimi"
      }
    ]
  }
}
Key Type Required Meaning
label string yes User-defined name shown in the dropdown. Empty or whitespace is treated as the entry being unset.
template string yes Text pasted into the focused terminal. May contain global placeholders only: {today}, {conception}, {conceptionPath}. A {placeholder:default} form falls back to default; a default-less unknown placeholder is left verbatim. Empty or whitespace is treated as the entry being unset.
submit bool no When true, condash presses Enter after pasting. Default false.
agent string no When set, the id of an agent from the agents list. The action spawns a fresh tab running that agent's command and types the template into the new tab — gives each entry a predictable starting environment (e.g. Start new project → claude-kimi always opens a fresh agent shell). Empty / missing keeps the "type into focused tab" behaviour.

Note. Selecting a new-project action does not create a project automatically — it only types a starter prompt into the terminal. The user then prompts their agent to create the project via condash projects create.

terminal.xterm

Visual + behavioural knobs for the xterm.js renderer. All keys are optional; missing keys fall through to xterm's defaults. Edit through the Settings → Terminal section — the editor live-rewrites .condash/settings.json and reloads existing tabs without a relaunch.

{
  "terminal": {
    "xterm": {
      "font_family": "JetBrainsMono Nerd Font, ui-monospace, monospace",
      "font_size": 13,
      "line_height": 1.2,
      "letter_spacing": 0,
      "font_weight": "400",
      "font_weight_bold": "600",
      "cursor_style": "block",
      "cursor_blink": true,
      "scrollback": 5000,
      "ligatures": false,
      "colors": {
        "background": "#1e1e2e",
        "foreground": "#cdd6f4",
        "cursor": "#f5e0dc",
        "black": "#45475a",
        "bright_black": "#585b70",
        "red": "#f38ba8",
        "bright_red": "#f38ba8",
        "green": "#a6e3a1",
        "bright_green": "#a6e3a1",
        "yellow": "#f9e2af",
        "bright_yellow": "#f9e2af",
        "blue": "#89b4fa",
        "bright_blue": "#89b4fa",
        "magenta": "#f5c2e7",
        "bright_magenta": "#f5c2e7",
        "cyan": "#94e2d5",
        "bright_cyan": "#94e2d5",
        "white": "#bac2de",
        "bright_white": "#a6adc8"
      }
    }
  }
}
Key Type / accepted values Meaning
font_family string CSS font stack used by xterm. Include a fallback chain since xterm doesn't load web fonts.
font_size positive int Pixel font size.
line_height positive number Multiplier; 1.0 is tight, 1.2–1.4 is comfortable.
letter_spacing number Pixels of inter-character spacing.
font_weight "100""900" or keyword Default text weight.
font_weight_bold "100""900" or keyword Bold-text weight.
cursor_style "block" / "underline" / "bar" Cursor shape.
cursor_blink bool Whether the cursor blinks.
scrollback non-negative int Lines retained per tab. Default 10 000.
ligatures bool Toggle xterm's ligatures addon. Off by default — non-monospace ligatures cause grid-misalignment in some fonts.
colors.<slot> hex string One entry per ANSI palette slot plus foreground / background / cursor / cursor_accent / selection_background. Missing slots fall through to xterm's defaults.

settings.json (per-user, per-machine)

Lives at ${XDG_CONFIG_HOME:-~/.config}/condash/settings.json on Linux (the matching paths on macOS and Windows are listed in At a glance). Not versioned. Every key is optional — a fresh install starts with the file empty (or absent) and fills it on first launch.

{
  "lastConceptionPath": "/home/you/src/vcoeur/conception",
  "recentConceptionPaths": ["/home/you/src/vcoeur/conception", "/home/you/src/work/conception"],
  "theme": "system",
  "terminal": {
    "shell": "/bin/zsh",
    "shortcut": "Ctrl+T",
    "screenshot_dir": "/home/you/Pictures/Screenshots"
  },
  "layout": {
    "projects": true,
    "leftView": "projects",
    "working": "code",
    "terminal": false,
    "projectsSplit": 0.42
  },
  "welcome": { "dismissed": true },
  "cardMinWidth": {
    "projects": 600,
    "code": 600,
    "knowledge": 480
  },
  "treeExpansion": {
    "knowledge": ["topics", "topics/security"],
    "resources": [],
    "skills": ["pr"],
    "skillsUser": ["git"]
  },
  "selectedBranches": ["feature-foo", "release-2026-05"],
  "branchFilterStickyAll": false,
  "skillsActiveScope": "conception"
}
Key Meaning
lastConceptionPath Absolute path to the conception tree condash should render. Replaces the older conceptionPath field — a one-shot migration on first read rewrites old files.
recentConceptionPaths Newest-first list of paths the user has opened (cap 5). Drives the File → Open Recent submenu and the Settings modal's recents section.
theme A theme preset id (light, mist, dark, nocturne, console) or system. Persisted by setTheme. See Theme.
uiFonts Per-category UI typography (family, weight, size). See UiFonts below. Set in Settings → Appearance; applied live via the --ui-font-* / --ui-weight-* / --ui-size-* CSS variables. Any field unset ⇒ default.
terminal.* Embedded-terminal preferences. See Terminal preferences above for every sub-key.
layout Composite-layout state. See LayoutState below.
welcome First-launch state. welcome.dismissed: true hides the Welcome screen even when both Projects and Knowledge are empty.
cardMinWidth Per-pane card grid min-width. See CardMinWidth below.
treeExpansion Per-pane set of expanded directory relPaths for the Knowledge / Resources / Skills tree panes (knowledge, resources, skills for the conception scope, skillsUser for the user scope of the Skills pane). Empty (or missing) means everything is collapsed — the on-purpose first-load state per #89.
selectedBranches Branches pinned by the Code-pane top-of-pane filter. The primary worktree row is always rendered; this set is additive on top of it. Honoured only when branchFilterStickyAll is false.
branchFilterStickyAll True ⇒ Code-pane filter is in All (sticky) mode: every branch is shown and new ones auto-pin. False ⇒ honour selectedBranches exactly (empty = main only). Defaults to true on first read when no explicit selection was ever made, false otherwise.
skillsActiveScope Active scope in the Skills pane — conception or user. Defaults to conception. Persisted on every scope switch.

Personal/per-machine keys — terminal, agents, open_with, pdf_viewer, dashboard, theme, uiFonts, layout, cardMinWidth, treeExpansion, selectedBranches, branchFilterStickyAll, welcome, skillsActiveScope — are valid only in settings.json; a conception file that carries one is rejected (and the scope-partition migrator lifts it here on open). Conversely, the tree-shape keys workspace_path, worktrees_path, long_lived_branches, repositories, retired_apps, and taskConfig are conception-only and are not accepted in settings.json. lastConceptionPath / recentConceptionPaths are global-only too — a conception's file cannot set them, since those describe the tree's own location and the machine-local recents list.

LayoutState

settings.json carries the composite-layout snapshot so a fresh launch reopens with the last layout.

Field Type Meaning
projects bool Show or hide the left band.
leftView 'projects' \| 'tasks' \| 'deliverables' Which pane fills the left band — the Projects list, the Tasks list, or the Deliverables aggregation of every project's ## Deliverables. Selected by the left activity rail. Defaults to 'projects'. A persisted 'outputs' (v3.20.0) is migrated to 'deliverables'.
working 'code' \| 'knowledge' \| 'resources' \| 'skills' \| 'logs' \| null Six-state. 'code', 'knowledge', 'resources', 'skills', or 'logs' shows that pane in the working slot; null hides them all.
terminal bool Show or hide the Terminal pane at the bottom.
projectsSplit number 0.02 – 0.98 Splitter position as a fraction of the band width, set by dragging. A fraction (not a pixel width) so the split stays proportional when the window is resized. The bounds are loose on purpose — the renderer's px clamp (a 200px floor per pane) is the real constraint, and a tighter fraction bound would disagree with it on a wide monitor and snap the handle away from where it was released. Upgrading from the older projectsWidth drops that key; an existing projectsSplit is kept, and only an absent or non-numeric one falls back to the default. The pixel value is not converted — the band width it was measured against is unknowable at parse time.

The IPC verbs getLayout / setLayout read and write this block atomically — toggling a pane via the View menu (or its keyboard shortcut) round-trips through setLayout so the change survives a restart.

CardMinWidth

cardMinWidth controls the n→n+1 reflow threshold for the eight card grids (Projects, Code, Knowledge, Resources, Skills, Logs, Tasks, Deliverables). Each grid uses minmax(min(<min>, 100%), 1fr), so a row of n cards reflows to n+1 once the pane is wide enough to fit n+1 cards each at this width.

Field Type Default Meaning
projects int 120 – 2400 (px) 650 Min width of a project card on the Projects pane.
code int 120 – 2400 (px) 650 Min width of a repo card on the Code pane.
knowledge int 120 – 2400 (px) 520 Min width of a knowledge-section card on the Knowledge pane.
resources int 120 – 2400 (px) 280 Min width of a resource card on the Resources pane.
skills int 120 – 2400 (px) 280 Min width of a skill card on the Skills pane.
logs int 120 – 2400 (px) 400 Min width of a session card on the Logs pane.
tasks int 120 – 2400 (px) 340 Min width of a task card on the Tasks pane.
deliverables int 120 – 2400 (px) 340 Min width of a deliverable card on the Deliverables pane.

Lower numbers pack more cards per row at the same window size; higher numbers keep cards roomy. Values outside the 120–2400 range are silently dropped back to the default. Keys equal to the default are removed from disk so the bundled defaults can change in a future release without leaving stale literals on every machine.

getCardMinWidth / setCardMinWidth round-trip the block; the renderer also applies the values as CSS variables on :root (--card-min-projects, --card-min-code, --card-min-knowledge, --card-min-resources, --card-min-skills, --card-min-logs, --card-min-tasks, --card-min-deliverables) so live edits in the Settings modal reflow the grids without a reload.

Theme

theme names one of the presets in the registry (src/shared/themes.ts), or system.

Value Name Kind Character
light Paper light Warm paper light — the vcoeur editorial palette.
mist Mist light Cool morning light — fog gray-blue, ink text, steel-blue accent.
dark Warm Gallery dark Gold on warm black — the gallery-dark lead theme.
nocturne Nocturne dark Moonlit indigo night — periwinkle on deep blue-black.
console Console dark Terminal-native: deep ink, phosphor green, monospace throughout.
system System Follows the OS preference between Paper and Warm Gallery.

Each preset is self-contained: it carries its own palette, and console also tightens the radius scale and re-points the brand font stacks at JetBrains Mono. There is no separate dark/light switch — a preset's kind is the only place the distinction lives, and it is what every binary subsystem (xterm, CodeMirror, highlight.js, mermaid) reads.

console goes one step further than a palette: it also restyles four shapes the token set cannot reach, in renderer/theme-console.css. The active rail item and the active terminal tab are reverse-video (filled with their own accent, ink printed on top) instead of tinted; pane titles become a tracked-out uppercase label behind an accent with a hairline running out to the actions, in place of the editorial serif title and its slide-rule; status dots are squares and the chips lose their capsule ends; and step-progress bars are segmented rather than solid. App pills stay filled — that fill is the per-app hash-hue channel, not decoration. No other preset is affected: every rule in that sheet is scoped to [data-theme='console'].

Pick a theme in Settings → Appearance, where each preset renders as a card with a swatch of its own colours. Selecting a card previews it immediately across the whole app — that is how you see a theme before committing to it — but nothing is written until you press Save, so closing the modal without saving puts the current theme back. The status-bar moon/sun button cycles through the list and persists straight away. Note that the ids light and dark predate the registry and are kept so existing settings.json files keep working — they are the ids of Paper and Warm Gallery, not a mode.

The renderer resolves the choice in JS and stamps two attributes on <html>: data-theme (the preset id, selecting the palette block in styles.css) and data-theme-kind (dark or light, which every dark-only CSS rule keys on). Adding a preset is one registry entry plus one [data-theme='<id>'] block.

UiFonts

uiFonts groups the UI into five typographic categories, each a { family, weight, size } object that restyles every element in the group at once. Each field is independent and any left default keeps the theme's value for that surface, so an all-default category (or an unset key) renders exactly as before the picker existed.

Category Elements default family
cardTitle Project, knowledge, and task card/list titles. editorial serif
heading Pane headers, section titles, modal titles, project-preview title. editorial serif
body Sidebar, controls, and general UI/body text. UI sans
code Task ids, code-pane names, deliverables, and code blocks. monospace
terminal Terminal chrome and log viewers. monospace

Each category field:

  • familydefault or one of the cross-platform faces sans, serif, mono, system, georgia, times, helvetica, verdana, trebuchet, palatino, courier (no fonts are bundled — the picker renders each option in its own face).
  • weightdefault, light, regular, medium, semibold, or bold (300–700).
  • sizedefault, xs (85%), sm (92%), lg (112%), or xl (128%), a relative scale multiplied onto the element's base size.

The renderer applies each non-default field as a :root CSS variable — --ui-font-* (family), --ui-weight-* (numeric weight), --ui-size-* (scale factor) — plus a matching data-ui-* attribute that scopes the rule in ui-fonts.css, so live edits in the Settings modal restyle the app without a reload. default sets no variable, so the element keeps its own family/weight/size. Family covers every element in a category (via the role tokens and carved --ui-font-* vars); weight and size apply to each category's primary text surfaces.

The embedded terminal's own canvas font is set separately in Settings → Terminal (terminal.xterm); the terminal category here governs the surrounding terminal/log chrome text.

uiFonts supersedes the earlier single projectCardTitleFont scalar (v4.86.0). A saved projectCardTitleFont value is folded into uiFonts.cardTitle.family and the legacy key dropped on the next read (see config migration).

Resolution order for the conception path, checked in sequence:

  1. CONDASH_CONCEPTION_PATH env var (session-scoped override; doesn't touch settings.json).
  2. lastConceptionPath in settings.json.
  3. The first-launch folder picker. On selection, the picker writes the chosen path to lastConceptionPath and prepends it to recentConceptionPaths (cap 5) so the next launch picks it up automatically.
  4. File → Open Recent lets the user switch between recent paths without a folder dialog. Picking a recent promotes it to the head of the list and swaps the active conception immediately.

The file is created on demand: the first-launch folder picker writes it; you can also create it by hand.

Editing from the dashboard

File → Settings… (Ctrl+,) opens a full-viewport modal — one scrolling surface, no tabs and no in-modal JSON editor; each persisted preference has its own form control. The left rail groups the sections under two scope headers, one per file:

Personal · this machine — writes settings.json:

  • Recent conceptions — manage the recents list backing File → Open Recent.
  • Appearance — theme (preset cards with swatches; selecting one previews it live); per-category UI fonts (with a live preview); per-pane card-grid min-widths.
  • Terminal — embedded terminal preferences (terminal, including xterm, logging, and the project-action templates).
  • Launchers — the agents list.
  • Open with — the three IDE/terminal launch slots.
  • Dashboard — live tab-summarization config (incl. the secret apiKey).

This conception — writes .condash/settings.json (the legacy condash.json / configuration.json are read but never written to):

  • Workspace & pathsworkspace_path, worktrees_path, long_lived_branches.
  • Repositories — the ordered repo list, per-repo run / force_stop.

Each section carries a scope chip naming the file it writes (settings.json or .condash/settings.json). Because every setting has exactly one home, there are no inheritance badges, no override state, and no Reset-to-global controls — the old two-tab + badge machinery was removed with the scope-partition revamp. Edits stage as drafts (a per-section dirty pip flags unsaved changes); Save flushes them and Discard drops them. Each draft round-trips through atomic CAS — settings.json via patchSettings / writeGlobalSettings, .condash/settings.json via patchConfig / writeNote — schema-validated by the strict zod schemas (globalSettingsSchema and conceptionConfigSchema, now disjoint) before the bytes hit disk.

The rail also carries Open settings.json and Open .condash/settings.json buttons (open the file in the OS default editor).

Keys not surfaced in the modal — pdf_viewer, the welcome.dismissed flag — still need a hand-edit. See settings.json (per-user, per-machine) above for paths.

Changes that do need a restart:

  • workspace_path or worktrees_path change — the filesystem scanner is built once at launch.
  • repositories list change — the per-repo state is built once at launch.

Changes that reload live without a restart:

  • Everything under open_with, terminal.
  • run / force_stop on an existing repo entry.

See also