Skip to content

refactor(progress): write inline progress directly to the stderr handle - #1858

Open
ndizazzo wants to merge 2 commits into
codex/1763-runtime-eventsfrom
codex/1763-progress-renderers
Open

ndizazzo wants to merge 2 commits into
codex/1763-runtime-eventsfrom
codex/1763-progress-renderers

Conversation

@ndizazzo

@ndizazzo ndizazzo commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Closes the console-print ratchet for issue #1763. The inline progress renderers were the last two files holding approved occurrences, and they now write to stderr through an explicit handle instead of a print macro. The allowlist goes from 6 approvals in 2 files to zero, and just no-console-print passes with an empty allowlist.

Nothing the CLI prints changes. models updates, runtime list, and the plugin subcommands render byte-for-byte the same output on both streams, in pretty and --json mode alike.

Stacked on #1851 (codex/1763-runtime-events); review that first.

What changed

Both terminal_progress.rs copies take a single std::io::stderr() handle at each of the three write sites, write! into it, and flush once:

let mut stderr = std::io::stderr();
write!(stderr, "\r\x1b[2K{gauge}").context("Write determinate progress")?;
stderr.flush().context("Flush determinate progress")?;

eprint! expands to stderr().write_fmt(..), so this is the same call against the same internally-locked handle: one lock, same bytes, same ordering. The only behavioral difference is that the two ?-returning sites now propagate a write error instead of panicking.

Each file also gained a module doc explaining why a direct terminal handle is legitimate in this specific module: it is the console output facility's renderer, every entry point early-returns on json_mode_enabled(), progress is a transient cursor-addressed redraw with no structured representation, and routing it back through the writer would be circular. A later pass that forbids direct io::stdout() / io::stderr() handles in library crates can point its exemption rule at that comment.

Dedup decision: fix both copies in place (option a)

The issue offered collapsing the two near-duplicate renderers into mesh-llm-events and re-pointing the three mesh-llm-tui consumers. That is the better end state and it is mechanically easy — the events module is a strict superset of every symbol the tui consumers import, both crates already depend on mesh-llm-events, and the two json_mode_enabled() gates are provably equivalent (OutputManager::init_global sets the events sink and the tui manager together).

I did not do it here, because the required proof fails. fit_inline_gauge_label differs between the copies: the tui version does max_label_len.saturating_sub(3) and always appends a three-character "...", while the events version emits 0/1/2 dots when the budget is smaller than the ellipsis. They agree for every max_label_len >= 3 and diverge only below that — a gauge width of 25–27 columns, reachable on a terminal of roughly 26–28 columns for plugin install and models updates.

Worse, the byte-diff harness this PR requires cannot detect that. It redirects both streams to files, crossterm::terminal::size() fails, and the gauge falls back to INLINE_GAUGE_WIDTH = 96. A clean diff at 96 columns would be a false proof of equivalence, so presenting one would be misleading. The dedup belongs in its own change where the truncation difference can be reviewed as the deliberate fix it is; I will file it separately.

Validation

cargo check --workspace --all-targets and cargo clippy --workspace --all-targets -- -D warnings were run workspace-wide rather than per-crate, since these two crates have many dependents.

  • cargo check --workspace --all-targets — clean, no warnings.
  • cargo clippy --workspace --all-targets -- -D warnings — one failure, clippy::chunks_exact_to_as_chunks in crates/skippy-model/src/gguf_writer_tests/conversion.rs:179. Pre-existing and unrelated: that crate is untouched by this branch, and a run without -D warnings shows it is the workspace's only warning. The repo pins no toolchain file; local clippy is 0.1.98 (2026-08-18) against CI's pinned stable 2026-08-20, which likely explains why CI does not flag it.
  • just no-console-print — passes. tools/xtask/data/console_print_allowlist.json regenerated to {}. Left in place deliberately; deleting the file is the final step of task: retire the console-print ratchet — convert the remaining 1,056 allowlisted call sites and delete the allowlist #1763.
  • just ci-validate — passes.

Byte-diff, stdout and stderr captured separately

Base binary built from 700e70454 in a detached worktree, new binary from this branch. Each case captures stdout and stderr to separate files and compares each stream independently with cmp -s — combining them would hide a stream migration, which is the exact bug class this refactor could introduce.

Case stdout stderr
models updates --all --check identical (0 B) identical (10034 B)
models updates --all --check --json identical (93 B) identical (10034 B)
models updates backpack-run/Qwen2.5-0.5B-Instruct-GGUF identical (0 B) identical (503 B)
models updates backpack-run/... --json identical (131 B) identical (503 B)
models list identical (21797 B) identical (0 B)
runtime list identical (102 B) identical (0 B)
plugin search identical (0 B) identical (98 B)
plugin list differs (see below) identical (0 B)
models updates --all --check 2>&1 | cat (non-tty) identical (10034 B merged)

Both --json stdouts still parse as valid JSON on the new binary. The non-tty pipe is byte-identical, so the is_terminal / json_mode_enabled gating is unchanged.

The one plugin list difference is a harness artifact, not a rendering change. That command prints the registered plugin's own launch command, which embeds the absolute path of the binary that registered it:

< blobstore	kind=runtime	command=/tmp/stage4-base/target/debug/mesh-llm	args=--log-format json --plugin blobstore
> blobstore	kind=runtime	command=/Users/.../1763-stage4-progress-renderers/target/debug/mesh-llm	args=--log-format json --plugin blobstore

The converted paths were genuinely exercised. Inspecting the captured stderr shows the LineGauge and the ANSI redraw:

\201━━━━━━━━━━━━━━━━━━\033[0m\r\033[2K📬 Update summary$
   repos with updates: 3$

That is DeterminateProgressLine::draw_counts plus the \r\x1b[2K clear — two of the three converted sites — producing 10,034 identical bytes across 56 repos.

Adjacent finding, not addressed here

Under --json, one-shot commands emit clean JSON on stdout but still write the human progress bar to stderr (about 10 KB for models updates --all --check). json_mode_enabled() only returns true once an OutputSink is installed, and one-shot commands never call OutputManager::init_global, so the early returns in these renderers cannot fire. It is byte-identical on the base binary, so it predates this work and stdout stays parseable — cosmetic, but worth its own issue. Fixing it would be a behavior change, not plumbing, so it is out of scope here.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability and error reporting for terminal progress output.
    • Preserved existing progress display behavior while providing clearer diagnostics when terminal writes fail.
  • Documentation

    • Added documentation explaining the use of direct terminal output handles for progress rendering.
  • Chores

    • Updated console output validation configuration to reflect the revised terminal progress handling.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 5645eb4d-5320-4434-a9e6-49caf7a1157f

📥 Commits

Reviewing files that changed from the base of the PR and between 700e704 and 28fe0c1.

📒 Files selected for processing (3)
  • crates/mesh-llm-events/src/terminal_progress.rs
  • crates/mesh-llm-tui/src/terminal_progress.rs
  • tools/xtask/data/console_print_allowlist.json

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Both terminal progress modules now write through explicit stderr handles. Clear and determinate progress writes add error context. Spinner write and flush errors remain ignored. The console print allowlist is empty.

Changes

Terminal progress output

Layer / File(s) Summary
Explicit stderr writes and allowlist cleanup
crates/mesh-llm-events/src/terminal_progress.rs, crates/mesh-llm-tui/src/terminal_progress.rs, tools/xtask/data/console_print_allowlist.json
Progress clearing, spinner redraws, and determinate progress output use explicit std::io::stderr() handles. Clear and determinate writes add error context. Module documentation explains the direct stderr usage. The eprint! allowlist is cleared.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to 7b4b0

The terminal progress refactor preserves CLI output and passes validation, so it is ready to merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 2 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: refactoring inline progress output to write directly to the stderr handle.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 2 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/1763-progress-renderers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ndizazzo
ndizazzo added this pull request to stack #1849 September 13, 2026 23:41
The console-print ratchet forbids the print macros, not console output.
The two inline progress renderers were the last approved occurrences, so
they now take a single `std::io::stderr()` handle, `write!` into it, and
flush once, which empties the ratchet without a permanent exemption.

Both copies of the renderer are fixed in place rather than collapsed into
one, because the two `fit_inline_gauge_label` implementations genuinely
differ on very narrow terminals and de-duplicating them would change what
the CLI prints. The dedup is worth doing on its own terms, not under
cover of a plumbing change.

Each renderer now documents why holding a terminal handle directly is
legitimate here, so a later pass that forbids direct handles in library
crates has a named exception to point at.

@i386 i386 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the synchronized exact head and its parent integration. Progress renderers use the sanctioned event/output path and the stack passes the full repository gate locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants