Conversation
Each spinner frame clears its line with `\r\x1b[K`, which only erases the row the cursor sits on. When the status is wider than the terminal it wraps, so every redraw leaves the overflow row on screen and the line reads as flicker. The combined status `Refreshing model pricing from LiteLLM... :: Loading usage logs (0/2) :: Claude, Codex` is ~85 columns, so any terminal narrower than that hits it on every online run; `-O` skips the pricing status and avoids it. Truncate the status to the terminal width minus the spinner prefix and one spare column before writing, so the single-line clear stays correct and the cursor never ends a frame in the wrap-pending state. `table.rs` already had ANSI- and CJK-aware truncation for cells, so that helper moves to `ccusage-terminal::width` as the public `truncate_to_width` instead of being duplicated. Its tests and insta snapshot move with it. Verified under a pty at `COLUMNS=40`: all 284 spinner frames stay within 39 columns, and at `COLUMNS=80` nothing is truncated. Closes #1411
📝 WalkthroughWalkthroughThe terminal crate adds ANSI-aware display-width truncation, table rendering adopts the shared helper, and progress statuses are truncated to fit the terminal while reserving spinner space. ChangesTerminal width rendering
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
ccusage-guide | 7f130ed | Commit Preview URL Branch Preview URL |
Jul 27 2026, 01:54 PM |
There was a problem hiding this comment.
Reviewed changes — moves ANSI/CJK-aware truncation from table.rs to width.rs as a public truncate_to_width, then uses it in the spinner render path so the status always fits in a single row on narrow terminals, fixing flicker (#1411).
- Make
truncate_to_widthpublic inccusage-terminal::width— moves the privatetruncate_visiblefromtable.rs, renames it, adds a docstring and doctests.contains_ansiandchar_display_widthbecome private helpers. - Truncate spinner status to terminal width in
progress.rs—fit_status_to_widthcallstruncate_to_widthwithterminal_width() - SPINNER_PREFIX_WIDTH - 1, reserving space for the spinner frame and one spare column to avoid wrap-pending cursor state. - Update call sites and snapshots —
table.rsusestruncate_to_widthinstead of the old private function. Snapshots and README docs updated.
✅ No new issues found.
@v0 or keep the SHA fresh with Dependabot | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
rust/crates/ccusage-terminal/src/width.rs (1)
3-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate ANSI-escape-skipping logic between
visible_widthandtruncate_to_width.Both functions re-implement the same CSI-sequence-skipping loop (detect
0x1b, optional[, scan to the first ASCII alpha terminator). Extracting a smallfn skip_ansi_escape(bytes: &[u8], index: usize) -> usizehelper would remove the duplication and reduce the risk of the two copies drifting apart.♻️ Proposed extraction
+fn skip_ansi_escape(bytes: &[u8], mut index: usize) -> usize { + index += 1; + if index < bytes.len() && bytes[index] == b'[' { + index += 1; + while index < bytes.len() && !(bytes[index] as char).is_ascii_alphabetic() { + index += 1; + } + index += usize::from(index < bytes.len()); + } + index +}Then both
visible_widthandtruncate_to_widthcan callindex = skip_ansi_escape(bytes, index);(adjustingtruncate_to_widthto also capturestart..indexfor copying).Also applies to: 62-99
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rust/crates/ccusage-terminal/src/width.rs` around lines 3 - 26, Extract the shared ANSI CSI-sequence scanning logic from visible_width and truncate_to_width into a skip_ansi_escape(bytes: &[u8], index: usize) helper. Replace both inline escape-skipping loops with this helper, preserving truncate_to_width’s escape-slice copying and the existing behavior for non-CSI escapes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rust/crates/ccusage-terminal/src/width.rs`:
- Around line 55-99: Update truncate_to_width to return an empty string when
width == 0, while preserving the ellipsis behavior for width == 1 and larger
truncated values. Update the
fits_the_status_when_the_terminal_has_no_room_at_all test in fit_status_to_width
to expect an empty status for the zero-width budget.
---
Nitpick comments:
In `@rust/crates/ccusage-terminal/src/width.rs`:
- Around line 3-26: Extract the shared ANSI CSI-sequence scanning logic from
visible_width and truncate_to_width into a skip_ansi_escape(bytes: &[u8], index:
usize) helper. Replace both inline escape-skipping loops with this helper,
preserving truncate_to_width’s escape-slice copying and the existing behavior
for non-CSI escapes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 99e710b7-6ee2-4e9c-af94-b6ec895b6a5a
⛔ Files ignored due to path filters (2)
rust/crates/ccusage-terminal/src/snapshots/ccusage_terminal__table__tests__snapshots_ansi_truncation_boundary.snapis excluded by!**/*.snaprust/crates/ccusage-terminal/src/snapshots/ccusage_terminal__width__tests__snapshots_ansi_truncation_boundary.snapis excluded by!**/*.snap
📒 Files selected for processing (5)
rust/crates/ccusage-core/src/progress.rsrust/crates/ccusage-terminal/README.mdrust/crates/ccusage-terminal/src/lib.rsrust/crates/ccusage-terminal/src/table.rsrust/crates/ccusage-terminal/src/width.rs
ccusage
@ccusage/ccusage-darwin-arm64
@ccusage/ccusage-darwin-x64
@ccusage/ccusage-linux-arm64
@ccusage/ccusage-linux-x64
@ccusage/ccusage-win32-x64
commit: |
`truncate_to_width` treated a width of 0 like a width of 1 and returned the ellipsis, which occupies one display column and so overran the budget it was given. The spinner reaches this on terminals of three columns or fewer, where `fit_status_to_width` has no room left after the spinner prefix, so the status could still spill into the column that is deliberately kept spare. Return an empty string instead, and fold the CSI-scanning loop that `visible_width` and `truncate_to_width` each carried into a shared `skip_ansi_escape` helper so the two cannot drift apart.
ccusage performance comparisonPR SHA: Performance comparison skipped. Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided. Base package: |
ccusage performance comparisonPR SHA: Performance comparison skipped. Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided. Base package: |
|
The Task list (6/7 completed)
|
There was a problem hiding this comment.
Reviewed changes — the incremental commit extracts a shared skip_ansi_escape helper and makes truncate_to_width(_, 0) return an empty string instead of an ellipsis, then updates the fit_status_to_width test to match.
- Extract
skip_ansi_escapehelper — the duplicated ANSI CSI-sequence scanning logic invisible_widthandtruncate_to_widthis replaced with a shared private function. - Fix zero-width truncation —
truncate_to_widthnow returns""whenwidth == 0rather than"…", since the ellipsis has no column budget at that width.
✅ No new issues found.
@v0 or keep the SHA fresh with Dependabot | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
Co-authored-by: ryoppippi <[email protected]>
|
The This is a race in the Nix sandbox when treefmt runs the schema-gen formatter with 32 threads — one invocation creates Fix: replaced the single Patch (also committed on diff --git apps/ccusage/justfile apps/ccusage/justfile
index f02f5d7b..78b72347 100644
--- apps/ccusage/justfile
+++ apps/ccusage/justfile
@@ -24,6 +24,7 @@ ensure-native-binary:
generate-schema:
cargo run --quiet --manifest-path ../../rust/Cargo.toml -p ccusage-config --bin generate-config-schema -- config-schema.json
oxfmt --write config-schema.json
+ rm -f ../../docs/public/config-schema.json
cp config-schema.json ../../docs/public/config-schema.json
start *args:
diff --git nix/treefmt.nix nix/treefmt.nix
index 3c026979..e7608d59 100644
--- nix/treefmt.nix
+++ nix/treefmt.nix
@@ -58,7 +58,8 @@ in
cp -f "$tmp" apps/ccusage/config-schema.json
fi
if [ -d docs/public ] && ! cmp -s apps/ccusage/config-schema.json docs/public/config-schema.json; then
- cp -f apps/ccusage/config-schema.json docs/public/config-schema.json
+ rm -f docs/public/config-schema.json
+ cp apps/ccusage/config-schema.json docs/public/config-schema.json
fi
'';
};Push is blocked by org-level
|
ccusage performance comparisonPR SHA: Performance comparison skipped. Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided. Base package: |
ccusage performance comparisonPR SHA: Performance comparison skipped. Base package URL was not ready before 300.000s. Fixture performance comparison requires a base package when --base-dir is not provided. Base package: |
ccusage performance comparisonPR SHA: This compares the Rust PR release binary against the configured base package on the same CI runner. Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |
ccusage performance comparisonPR SHA: This compares the PR package against the configured base package on the same CI runner. Package runtime diagnosticsCompares the PR package wrapper, the installed native optional dependency binary, and the workspace release binary on the same large fixture. This identifies whether slow package results come from JavaScript wrapper overhead, the published native binary build, or the Rust core itself. Fixtures: Claude
Committed fixture performanceCommitted small fixtures for stable PR-to-PR feedback and explicit Claude/Codex command coverage. Fixtures: Claude
Large real-world-shaped fixture performanceGenerated fixtures shaped from aggregate local log statistics: thousands of JSONL files, many small sessions, and a long tail of larger sessions. No real prompts, paths, or outputs are stored in the fixtures. Fixtures: Claude
Artifact size
Lower medians and smaller artifacts are better. CI runner noise still applies; use same-run ratios as directional PR feedback, not release guarantees. |

Summary
Fixes the flickering progress spinner on narrow terminals (#1411). Each frame clears its line with
\r\x1b[K, which only erases the row the cursor sits on, so a status wider than the terminal wraps and every redraw leaves the overflow row behind.What changed
ccusage-core::progresstruncates the status to the terminal width minus the spinner prefix and one spare column before writing it, so the line always occupies a single row and the single-line clear stays correct.table.rsalready had ANSI- and CJK-aware truncation for table cells, so that helper moves toccusage-terminal::widthas the publictruncate_to_widthrather than being duplicated. Its tests and insta snapshot move with it.Why
On an 80-column terminal the combined status is ~85 columns:
so it wrapped on every online run.
-Oskips the pricing status, which is why it avoided the flicker.Testing
just rust::test, clippy (--all-targets),just hawk, andjust fmtare clean.COLUMNS=40all 284 spinner frames stay within 39 columns (83 of them truncated, none wrapping); atCOLUMNS=80nothing is truncated.Closes #1411
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is enabled.Summary by cubic
Fixes spinner flicker on narrow terminals by keeping the spinner line on a single row. Also makes truncation ANSI/CJK-aware in
ccusage-terminaland treats zero-width truncation as empty to avoid overflow.Bug Fixes
truncate_to_widthandterminal_widthto prevent wrapping and flicker.truncate_to_width(..., 0)return "" (not "…") to avoid overrunning the width on ultra-narrow terminals.Refactors
table.rstoccusage-terminal::widthas publictruncate_to_width, share ANSI escape scanning viaskip_ansi_escape, and re-export inccusage-terminal; update table code and snapshots.Written for commit 7f130ed. Summary will update on new commits.
Summary by CodeRabbit
New Features
truncate_to_widthhelper for ANSI-aware, Unicode column–based truncation.Bug Fixes
Documentation