ADR-021: TTY progress via indicatif, gated by is_rich()
Date: 2026-06-05 Status: Accepted Author: Rômulo Febasi
Context
Three commands do visible work with no feedback: syn embed --all downloads
~920 MB of models then embeds the whole workspace, syn reindex rebuilds the
SQLite index over every entity, and syn watch reindexes on each change. Today
embed --all streams one [n/total] line per item to stderr; reindex shows
nothing until its final summary. On a slow embed run the tool looks hung.
The CLI-UX brief (docs/researchs/2026-06-05-cli-ux-and-agent-ergonomics.md)
set a premium (“AAA”) bar: a real progress indicator on a TTY, but no
animation when stdout is not a TTY (progress bars become “christmas trees” in
CI logs), and nothing that pollutes the data a pipe or an agent reads.
Two constraints, as with ADR-019: (1) no new dependency without an ADR, and (2) never regress the machine-readable path.
A relevant finding: indicatif (0.17 and 0.18) and its console backend are
already in Cargo.lock: fastembed/hf-hub pull them for the model
download bar. Adopting indicatif 0.18 as a direct dependency adds zero
new transitive weight to the default build; it shares the copy fastembed
already compiles.
Decision
Adopt indicatif 0.18 as a direct dependency and wrap it in
output/progress.rs behind a single gate: a Progress draws only when
output::is_rich() and not --quiet: i.e. a TTY, text mode, no
--plain/SYNAPSE_AGENT. Otherwise it is a ProgressBar::hidden() no-op.
embed --all: a determinate bar on a rich TTY; the existing one-line-per-item stream is kept for non-TTY text (readable in CI logs, no\rspam).reindex/watch: a steady-tick spinner with the current stage + counts, cleared on completion so the final summary stands alone.
Progress draws to stderr, so stdout (and --format json) is never touched.
Colour is left to console, which already honours NO_COLOR/CLICOLOR.
Consequences
- Better. Long operations show live progress on a TTY; the tool no longer
looks hung. The bar is auto-suppressed in CI/pipes/agents and under
--quiet. - Worse. A direct dep on a non-trivial crate. On a core-only build (no
v05_backends)indicatifis newly compiled (small); in the default build it is already present.indicatif0.17 and 0.18 coexist in the tree (via hf-hub 0.4 vs 0.5/fastembed): a pre-existing duplication this change does not add to (we pin 0.18, fastembed’s copy). - Open. The bar templates are fixed; per-command tuning can come later.
embed --all’s model-download phase still uses fastembed/hf-hub’s own bar.
Alternatives considered
- Hand-roll a spinner. Rejected: doing TTY width, steady ticks, redraw and
NO_COLORcorrectly is exactly whatindicatifalready does, and it is already compiled in. Reinventing it is strictly worse and more code to own. - A heavier TUI (
ratatui). Rejected: massive new surface for a few progress bars; Synapse is a line-oriented CLI, not a full-screen app. - Keep the line stream everywhere. Rejected: no live feedback on a TTY, and the brief explicitly asked for a premium indicator there.
- Draw to stdout. Rejected: would corrupt piped output and
--format json. Progress is stderr-only.