DashFloat

A fast, robust CLI AI coding harness in Elixir — Octofriend's successor, owned and built here.

DashFloat streams LLM responses, executes structured tool calls, and manages coding sessions from the terminal. It is built on OTP concurrency — a pure render-loop UI process, an agent-manager GenServer, stream-handler tasks, and isolated subprocess execution — with an ex_ratatui TUI and burrito single-binary distribution.

Website: https://dashfloat.com — the landing page and these docs, rendered.

Status

Pre-alpha — Phase 2. The LLM streaming engine is in (ADR 0005): an OpenAI-compatible SSE client streams tokens into the existing TUI (ADR 0002). Next up per the ADR 0001 roadmap: tool call execution, context management & history persistence.

Run it

Configure your provider — any OpenAI-compatible endpoint, one profile per subscription — in ~/.config/dashfloat/settings.json5. API keys live in a sibling keys.json5, keyed by profile name, so the settings file (and every backup, dotfile sync, or example of it) is structurally incapable of leaking a credential; both files are 0600. The first profile is the default; each profile owns its model menu, whose first entry is its default model (settings.defaultModel can name another). An oauth.json5 sibling is reserved for a future OAuth flow (ignored until one exists). A legacy config.json, or a v1 settings.json5 with inline keys, migrates itself on first run — backups inherit the source's sensitivity, so anything that ever held a credential is 0600 wherever it lives:

// settings.json5 — no credential material, ever
{
  configVersion: 2,
  profiles: [
    {
      name: 'work',
      baseUrl: 'https://api.openai.com/v1',
      models: [{ nickname: 'mini', model: 'gpt-4o-mini', context: 128000 }],
    },
  ],
}
// keys.json5 — the only home for API keys
{ 'work': 'PLACEHOLDER', }

DASHFLOAT_BASE_URL, DASHFLOAT_MODEL, DASHFLOAT_API_KEY, and DASHFLOAT_CONTEXT_TOKENS override the files per field (the effective API key is DASHFLOAT_API_KEY, else keys[profile name]); --profile <name> (startup flag, or DASHFLOAT_PROFILE) selects the active profile and --model <nickname> a model within it. A stray inline apiKey in settings.json5 is warned about and ignored — never a fallback. The status bar names the active model nickname (profile). Put your provider's own base URL in baseUrl; DashFloat appends /chat/completions.

The v1→v2 split never loses a credential silently: if a key it is about to remove would end up in no file (a pre-existing keys.json5 that wins without holding that exact value, and a settings.json5.bak that cannot be re-written), startup fails with cannot split settings.json5: … no home. The error's remedies are real: add the key under its profile name to keys.json5, or move the stale backup aside (mv settings.json5.bak settings.json5.bak.old — the next start tightens the moved file to 0600 automatically) so the next start can write a fresh one. A settings.json5.bak that is a symlink is never read through or replaced — it counts as no backup, and the split refuses if a key would be lost. Details: ADR 0008. Then:

mix run --no-halt

Type a prompt and press Enter: tokens stream into the log and the status bar flips Streaming → Idle when the reply completes. Press Esc mid-stream to cancel — the partial reply is kept and a gray [cancelled] line marks it. Shift+Enter inserts a newline on terminals that report enhanced key events (kitty protocol, modifyOtherKeys) — on others, use Alt+Enter. Up/Down walk input history, PageUp/PageDown and the mouse wheel scroll the log. Ctrl+Q quits; a double Ctrl+C does too (one press shows a prompt, the second within two seconds exits). Click-drag over the log selects text — highlighted live, copied to your system clipboard on release via OSC 52 (works over SSH). Hold Shift while dragging to use the terminal's own selection instead.

Without a provider configured the TUI still starts; submitting logs the setup instructions instead of a reply. This live-key run is the manual smoke step — the test suite is offline and deterministic (recorded SSE fixtures plus a loopback server).

Stack

Development

mix ci

mix ci is the whole gate — the same list CI runs, so local and CI cannot drift (ADR 0003): format, compile with warnings as errors, credo --strict, dialyzer, and the test suite under ExCoveralls with a coverage floor (coveralls.json). It fetches any missing deps itself — there is deliberately no separate mix deps.get first, as it would silently repair a stale lock before the gate could catch it. Coverage counts shipped code only: the test harness (test/) is excluded via skip_files, so the floor guards lib/.

CI runs it on every pull request and every push to main, uploads the HTML coverage report as an artifact, and sends the JSON report to Codecov. The required branch-protection check is Wait for CI. See AGENTS.md for the repository conventions.

License

MIT — see LICENSE.