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
- Elixir 1.20 / Erlang/OTP 29
ex_ratatui— TUI (Rustler-backed ratatui bindings, precompiled NIFs)req— HTTP / SSE streamingjason— JSONburrito— single-binary releases
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.