Skip to content

fix(renderer): truncate at zero width, keep FLEX text, no custom colors at No Color - #678

Merged
sirmalloc merged 5 commits into
sirmalloc:mainfrom
eric-engberg:fix/renderer-edge-cases
Oct 9, 2026
Merged

sirmalloc merged 5 commits into
sirmalloc:mainfrom
eric-engberg:fix/renderer-edge-cases

Conversation

@eric-engberg

@eric-engberg eric-engberg commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What

  • Truncation no longer switches off on narrow terminals. With Full minus 40 (or Full until compact past its threshold) on a terminal of 40 columns or fewer, and with Full at 6 columns or fewer, the status line now truncates instead of printing at full length and wrapping. Both plain and Powerline mode.
  • Widget text that reads FLEX stays on the line. In plain mode, a widget whose output was exactly FLEX with no color codes around it disappeared and turned into a second flex gap.
  • No Color and Basic no longer print 256-color or truecolor escapes. hex: and ansi256: widget colors were emitted at every color level, so No Color still colored those widgets.

Why

Narrow widths. The effective width is the terminal width minus a reserve (40 columns for Full minus 40, 6 for Full). Every width check treated a result of 0 or less as "no width detected". A 60-column line with Full minus 40:

terminal 41 -> "."            (truncated)
terminal 40 -> 60 columns     (not truncated, wraps)
terminal 30 -> 60 columns

The TUI preview cuts each line at the terminal edge, so it showed a truncated line while Claude Code got the full one.

FLEX text. The plain renderer stood each flex separator in the assembled line as the string 'FLEX', then split the line wherever an element equaled that string. [Custom Text "left", Flex, Custom Text "FLEX", Custom Text "right"] with No Color and no padding rendered left right. With colors or padding around it, the text didn't match, which is why this went unnoticed.

Custom colors at No Color. With colorLevel: 0, a widget colored hex:FF0000 on hex:0000FF and one colored ansi256:100 rendered as:

\x1b[48;2;0;0;255m\x1b[38;2;255;0;0mhexc\x1b[39m\x1b[49m\x1b[1m\x1b[38;5;100ma256\x1b[39m\x1b[22m

Terminal Options describes No Color as "Disables all color output". Named colors already follow the level (chalk emits nothing at level 0), and gradients already return no code at Basic and No Color; these two formats didn't.

How

  • Width: the effective width is clamped at 0, and every check in both renderers now treats any detected width, 0 included, as known. When the reserve takes the whole terminal, the line truncates to nothing (an empty line isn't printed), which continues what 43, 42 and 41 columns already show (..., .., .). An undetected width (null, or 0 passed by a caller) keeps today's fallback. The TUI preview applies the same reserve, so it now matches the piped output. Clamping to 1 column (a lone .) instead would also work.
  • FLEX: a flex separator is pushed into the assembled line as null, which no widget output can equal. Separator, padding and fallback (| when the width is unknown) handling are unchanged. Powerline mode already used its own marker and isn't touched.
  • Custom colors: getColorAnsiCode returns no code for hex: and ansi256: at the ansi16 level, which covers both Basic and No Color, as it already does for gradient specs. This matches the TUI: the color menu only offers these formats at 256 Color and Truecolor, and switching to Basic or No Color clears them from widgets. So they only reach these modes through a hand-edited config. One difference remains: the TUI resets a cleared foreground to the widget's default color, while the renderer now prints the text in the terminal's default color. Named colors, and every level above Basic, are unchanged.

Demo

Piped status lines from sample data (scripts/payload.example.json, the width set with CCSTATUSLINE_WIDTH), each wrapped in [ ] so an empty result shows: Full minus 40 from 44 down to 39 columns, a Custom Text widget reading FLEX at No Color, and hex:/ansi256: colors at No Color. The FLEX case only affected plain mode, so its Powerline row doesn't change.

Powerline: before

Renderer edge cases before the fix, Powerline mode

Powerline: after

Renderer edge cases after the fix, Powerline mode

Plain: before

Renderer edge cases before the fix, plain mode

Plain: after

Renderer edge cases after the fix, plain mode

Also: a CI timing fix in the custom command tests

This PR's first CI run failed on a test it doesn't touch:

(fail) custom command capture under node > rejects 4 MiB of stdout with cache TTL 0 [5018.76ms]
  ^ this test timed out after 5000ms.
(pass) custom command capture under node > rejects 4 MiB of stdout with cache TTL 5 [146.20ms]

That case is the first in the job to start node; the identical case right after it took 146 ms and the rest about 60 ms. The capture itself can't have hung (it kills its helper after 2 s and returns [Timeout]), so the time went to Node's first start on a fresh runner, before its binary was in the disk cache. The last commit starts each runtime once in a beforeAll (30 s timeout) before the timed tests; the tests and their own limits are unchanged. custom-command-process.test.ts passes under Bun (5 runs) and Node 26 Vitest.

Testing

  • New tests (7), each failing on main first:
    • Full minus 40 at 40 and 30 columns, Full until compact past the threshold, Full at 6 columns, and the preview all render nothing instead of the full line. Powerline with a flex separator does the same.
    • FLEX widget text survives with a known width and with the unknown-width fallback.
    • getColorAnsiCode returns no code for hex:/ansi256: at ansi16 and still does at 256 Color and Truecolor. Rendered lines at No Color and Basic contain no 38;5/38;2/48;5/48;2 escapes in either mode, and still do at 256 Color and Truecolor.
  • bun test: 2789 pass, 0 fail. bun run lint passes.
  • The three changed test files under Node (Vitest): 53 pass (46 existing, 7 new).
  • Built CLI under Bun 1.4.2 and Node 26.10.0: both runtimes give identical output in plain and Powerline mode, the same as main for the baseline line. The TUI opens and exits cleanly under both.
  • Piped renders of the demo configs on main and this branch, as in the stills above.

Full minus 40, and Full until compact past its threshold, subtract 40
columns from the terminal width; Full subtracts 6. On a narrow terminal
that went to 0 or below, and every width check treated a non-positive
width as "no width detected": the line printed at full length and
wrapped, while 41 columns still truncated to a single ".". The TUI
preview cut the line at the terminal edge instead, so it no longer
matched what Claude Code received.

Clamp the effective width at 0 and treat any detected width, 0
included, as known, in both the plain and Powerline renderers. When the
reserve takes the whole terminal the line now truncates to nothing, the
continuation of what 41-43 columns already show. An undetected width
(null, or 0 from the caller) keeps the fallback behavior.
The plain renderer stood each flex separator in the assembled line as
the string 'FLEX' and later split the line on that string. A widget
whose rendered text was exactly FLEX with no color codes around it (No
Color mode, or a color name the palette doesn't have) matched too: its
text vanished and the line got a second flex gap.

Push null for a flex separator instead, which no widget output can
equal. Powerline mode already uses its own marker and is unchanged.
…olor

getColorAnsiCode returned raw 256-color and truecolor escapes for hex:
and ansi256: colors at every color level. With No Color selected, which
the TUI describes as disabling all color output, a widget colored
hex:FF0000 on hex:0000FF still printed \x1b[48;2;0;0;255m\x1b[38;2;255;0;0m;
Basic did the same.

Return no code for these formats at the ansi16 level (Basic and No
Color), as the function already does for gradient specs. This matches
the TUI, which only offers these formats at 256 Color and Truecolor and
clears them from widgets when switching to Basic or No Color, so they
only reach these modes through a hand-edited config. Named colors are
unchanged.
The else branch re-checked terminalWidth === null, which the preceding
condition (a flex separator and a known width) already implies when a
flex separator is present. Chain it as else-if on hasFlexSeparator
instead of nesting an if/else inside the else.
On a fresh CI machine the first start of a runtime can take seconds while
its binary is read from a cold disk. The capture tests start Node for the
first time in their opening case, so that case once ran past its 5s limit
(5018 ms) while the same case one test later took 146 ms and the rest about
60 ms. Each runtime now starts once in a setup hook with its own timeout,
before the timed tests.
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