Skip to content

fix(progress): cap text-mode rendered line length#88

Closed
jdx wants to merge 1 commit into
mainfrom
cap-text-mode-line-length
Closed

fix(progress): cap text-mode rendered line length#88
jdx wants to merge 1 commit into
mainfrom
cap-text-mode-line-length

Conversation

@jdx

@jdx jdx commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary

A user-supplied template can produce arbitrarily large content — e.g. a multi-line shell script embedded in a `check =` value, or a generated command with hundreds of args expanded inline — and text mode prints each render verbatim. With no cap, every prop update on such a job dumps thousands of chars into CI logs.

This cap was the missing piece after #86 — that PR collapsed duplicate lines and dropped UI escape codes from text mode, but the per-line size was still unbounded.

Behavior

  • Default cap: 4096 printable chars (covers any realistic diagnostic, bounds the pathological case).
  • ANSI-aware via `console::truncate_str` — escapes don't get split mid-sequence or counted toward the budget.
  • Override via `CLX_TEXT_MAX_LEN` env var. Set to `0` to disable truncation entirely.
  • Truncated output gets an ellipsis suffix (`…`) so readers see that content was elided.

Test plan

  • `cargo test --lib` (120 pass; 4 new)
  • short input passes unchanged
  • long input capped to N printable chars, ends with `…`
  • cap of 0 disables truncation
  • ANSI escape sequences preserved during truncation

🤖 Generated with Claude Code


Note

Low Risk
Low risk change isolated to ProgressOutput::Text rendering; main impact is potentially truncating very long template output (configurable/disableable via CLX_TEXT_MAX_LEN).

Overview
Adds a default 4096-character cap to rendered ProgressOutput::Text lines to prevent user templates from dumping unbounded content into logs.

Introduces ANSI-aware truncation with an ellipsis suffix via truncate_text_mode_line, caches an optional CLX_TEXT_MAX_LEN override (set to 0 to disable), and applies the cap before the existing "skip duplicate line" check. Includes unit tests covering short/long input, the 0 escape hatch, and ANSI handling.

Reviewed by Cursor Bugbot for commit a2a2444. Bugbot is set up for automated code reviews on this repo. Configure here.

A user-supplied template can produce arbitrarily large content — e.g. a
multi-line shell script embedded in a `check =` value, or a generated
command with hundreds of args expanded inline — and text mode prints
each render verbatim. With no cap, every prop update on such a job
dumps thousands of chars into CI logs.

Cap printable width at 4096 chars by default, ANSI-aware so escapes
don't get split or counted toward the budget. Override with
`CLX_TEXT_MAX_LEN`; set to 0 to disable truncation entirely.
@greptile-apps

greptile-apps Bot commented Apr 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR caps text-mode rendered line length at 4096 printable characters (ANSI-aware via console::truncate_str) to prevent runaway templates from flooding CI logs, with an opt-out via CLX_TEXT_MAX_LEN=0. The implementation and tests are solid, but the doc comment on text_mode_max_len misstates the behavior for unparseable env-var values — the comment says they disable truncation, while the code falls back to the 4096 default.

Confidence Score: 3/5

Safe to merge after fixing the doc/behavior mismatch for unparseable CLX_TEXT_MAX_LEN values.

One P1 finding: the documented behavior for unparseable env-var values contradicts the code, which could mislead users into thinking truncation is disabled when it is not. The truncation logic itself is correct and the four new tests are thorough. Score sits at 3 due to the P1 ceiling.

src/progress/render.rs — specifically the text_mode_max_len doc comment vs. actual fallback behavior.

Important Files Changed

Filename Overview
src/progress/render.rs Adds text-mode line-length capping via truncate_text_mode_line and text_mode_max_len; doc comment for the env-var fallback incorrectly states unparseable values disable truncation when they actually use the default 4096 cap.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[render_text_mode called] --> B{output empty?}
    B -- yes --> Z[return Ok]
    B -- no --> C{contains clx:flex?}
    C -- yes --> D[flex: truncate to terminal width]
    C -- no --> E[use output as-is]
    D --> F[truncate_text_mode_line]
    E --> F
    F --> G{max_len == 0?}
    G -- yes --> H[return unchanged]
    G -- no --> I{measure_text_width <= max_len?}
    I -- yes --> H
    I -- no --> J[console::truncate_str + ellipsis]
    J --> K[final_output]
    H --> K
    K --> L{same as last output?}
    L -- yes --> Z
    L -- no --> M[write_line to terminal]
    M --> Z

    subgraph text_mode_max_len
        N[read CLX_TEXT_MAX_LEN env var] --> O{parseable usize?}
        O -- yes --> P[use parsed value]
        O -- no / unset --> Q[use DEFAULT 4096]
    end
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(progress): cap text-mode rendered li..." | Re-trigger Greptile

Comment thread src/progress/render.rs
Comment on lines +297 to +305
/// `CLX_TEXT_MAX_LEN=0` (or any unparseable value) disables truncation.
fn text_mode_max_len() -> usize {
static CACHED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CACHED.get_or_init(|| {
std::env::var("CLX_TEXT_MAX_LEN")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(DEFAULT_TEXT_MODE_MAX_LEN)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Doc comment contradicts actual behavior for unparseable values

The doc comment says CLX_TEXT_MAX_LEN=0 or any unparseable value disables truncation, but unwrap_or(DEFAULT_TEXT_MODE_MAX_LEN) means an unparseable value (e.g. CLX_TEXT_MAX_LEN=off) silently falls back to the default 4096 cap instead. A user who sets CLX_TEXT_MAX_LEN=none or misspells the number would expect truncation disabled but would get the default cap applied with no warning.

Either fix the comment to match the code, or change the fallback so unparseable strings map to 0 (no-truncation) as documented.

Suggested change
/// `CLX_TEXT_MAX_LEN=0` (or any unparseable value) disables truncation.
fn text_mode_max_len() -> usize {
static CACHED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CACHED.get_or_init(|| {
std::env::var("CLX_TEXT_MAX_LEN")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(DEFAULT_TEXT_MODE_MAX_LEN)
})
/// `CLX_TEXT_MAX_LEN=0` disables truncation. Any unparseable value falls back
/// to the default (`DEFAULT_TEXT_MODE_MAX_LEN`).
fn text_mode_max_len() -> usize {
static CACHED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CACHED.get_or_init(|| {
std::env::var("CLX_TEXT_MAX_LEN")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(DEFAULT_TEXT_MODE_MAX_LEN)
})
}

Fix in Claude Code

Comment thread src/progress/render.rs
Comment on lines +312 to +317
fn truncate_text_mode_line(s: &str, max_len: usize) -> String {
if max_len == 0 || console::measure_text_width(s) <= max_len {
return s.to_string();
}
console::truncate_str(s, max_len, "…").into_owned()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Double-width traversal for each rendered line

truncate_text_mode_line calls console::measure_text_width(s) to decide whether to truncate, and then console::truncate_str internally re-scans the string to find the cut point. For the common case (no truncation needed) this is just one extra pass, but for very long lines with many ANSI codes both passes traverse the entire string. This is a minor performance observation — not blocking.

Fix in Claude Code

@jdx jdx closed this Apr 30, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to truncate text-mode output in src/progress/render.rs to prevent excessively large logs from runaway templates. It includes a configurable cap via environment variables, a truncation helper that handles ANSI escapes, and corresponding unit tests. Feedback highlights a discrepancy between the documentation and the implementation regarding unparseable environment variables, as well as a redundant text width measurement in the truncation logic.

Comment thread src/progress/render.rs
const DEFAULT_TEXT_MODE_MAX_LEN: usize = 4096;

/// Resolve the text-mode line cap from the env, falling back to the default.
/// `CLX_TEXT_MAX_LEN=0` (or any unparseable value) disables truncation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The docstring states that any unparseable value disables truncation, but the implementation falls back to DEFAULT_TEXT_MODE_MAX_LEN (4096) if parsing fails (via unwrap_or on line 304). To align the code with the documentation, you could change the fallback to 0, or update the docstring to reflect that invalid values fall back to the default cap. Given that this is a safety feature, updating the docstring is likely the safer choice.

Comment thread src/progress/render.rs
Comment on lines +313 to +316
if max_len == 0 || console::measure_text_width(s) <= max_len {
return s.to_string();
}
console::truncate_str(s, max_len, "…").into_owned()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The call to console::measure_text_width(s) is redundant because console::truncate_str already performs this check internally. Removing it avoids an unnecessary extra pass over the string, which is beneficial when handling the large "pathological" inputs this PR aims to address.

Suggested change
if max_len == 0 || console::measure_text_width(s) <= max_len {
return s.to_string();
}
console::truncate_str(s, max_len, "…").into_owned()
if max_len == 0 {
return s.to_string();
}
console::truncate_str(s, max_len, "…").into_owned()

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.

1 participant