Skip to content

fix(renderer): keep separators placed next to each other - #773

Merged
sirmalloc merged 1 commit into
sirmalloc:mainfrom
eric-engberg:fix/stacked-separators
Oct 30, 2026
Merged

sirmalloc merged 1 commit into
sirmalloc:mainfrom
eric-engberg:fix/stacked-separators

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

In plain mode, manual separators placed next to each other between visible widgets all render again. Two spaces give a two-space gap, ' ' '•' ' ' gives • , and two pipes give two pipes. The status line and the TUI preview both change.

Separators on either side of a widget that renders empty still collapse, now a run at a time: a run with a visible separator keeps the boundary, and a run of spaces is replaced by the separators after the empty widget. With a single separator on each side, nothing changes.

One case that looks different: deleting the widget between two separators leaves both in the line, and both now show, as they do in the editor's list. Before, the preview showed one.

Why

renderStatusLine scans back from each manual separator to decide whether it renders. Since #525, an earlier separator met on that scan either owns the boundary (a visible one: the current separator is dropped) or is replaced (a spacing one: it is popped from the output). That rule is for separators on either side of a widget that renders empty, where one of them has to go. But the scan applied it whether or not a widget lay between the two, so separators placed side by side were deduplicated too. Each #525 test with two separators puts an empty widget between them, so none covers adjacent ones with every widget visible.

The TUI can build these lines: Space cycles a separator through |, -, , and space, and k copies the selected one.

Piped render with a scratch HOME, colorLevel: 0, custom-text widgets, Bun 1.4.2:

Line main this branch before #525 (ef521ee9^)
A ' ' ' ' B A B A B A B
C '|' '|' D C | D C | | D C | | D
E ' ' '•' ' ' F E•F E • F E • F

Calling renderStatusLine directly gives the same results for [A, ' ', '|', B] (main: A | B, now A | B) and [A, '|', ' ', B] (main: A | B, now A | B).

How

  • In the backward scan, a separator met before any empty widget is part of the current separator's run. The scan steps over it to the content before the run, so every separator in the run renders.
  • Across an empty widget, fix(renderer): preserve separators after hidden widgets #525's rule now applies per run. A run that contains a visible separator keeps the boundary, and the run after the empty widget is dropped: [A, ' ', '•', ' ', hidden, '|', C] renders A • C (main: A•C). A run of spaces is replaced by the run after the empty widget, and that whole run renders. Only that run's first separator removes the spaces it replaces, so the rest of its run is kept: [A, ' ', ' ', hidden, ' ', '|', C] renders A | C (main: A | C).
  • With Inherit Separator Colors on, each separator in a run takes the colors of the widget before the run, which is what fix(renderer): preserve separators after hidden widgets #525 set up for single separators. Before fix(renderer): preserve separators after hidden widgets #525, only the first separator in a run inherited.
  • Six new tests in renderer-separator-collapse.test.ts: four cases of adjacent separators between visible widgets, plus the two cases above around an empty widget. fix(renderer): preserve separators after hidden widgets #525's tests are unchanged and pass.

Left as is:

  • Powerline mode drops manual separators ("manual separators disabled"). That doesn't change.
  • docs/USAGE.md describes collapsing only around widgets that render empty, which matches this behaviour, so the docs are unchanged.

Demo

Line 1 is Model, ' ' '•' ' ', Context Length, |, Git Branch, |, Git Changes. The demo opens it, turns the | after Context Length into a space with Space, then copies it twice with k for a three-space gap. Before, the preview shows Claude•Ctx and a single space, and k leaves it unchanged. After, it shows Claude • Ctx and the gap widens with each copy. Powerline mode ignores manual separators, so its two recordings look the same; they are there as a control.

Powerline

Before:

Stacked manual separators in Powerline mode before the fix

After:

Stacked manual separators in Powerline mode after the fix

Plain

Before:

Stacked manual separators in plain mode before the fix

After:

Stacked manual separators in plain mode after the fix

Testing

  • On main as of [low] perf(claude-status): hold a lock while the status page fetch is in flight #635 and [medium] perf(git-review): key the PR cache through the cached runGit #639 (62a371b): merges cleanly, bun run lint is clean, and bun test gives 3709 pass, 0 fail. The built CLI under Bun 1.4.4 and Node 26.13.0 matches main in plain and Powerline mode with the baseline configs, and the TUI opens and exits cleanly.
  • bun test: 3174 pass, 0 fail (178 files); main: 3168 pass, 0 fail. The 6 new tests all fail on main. bun run lint is clean.
  • renderer-separator-collapse.test.ts under vitest on Node 26.10.0: 25 pass (main: 19 pass).
  • Built CLI, Bun 1.4.2 and Node 26.10.0: with the baseline demo configs, both runtimes agree and match main in plain and Powerline mode. With this demo's configs, plain differs from main as intended, Powerline matches main, and both runtimes agree. The TUI opens and exits cleanly under both.

In plain mode, manual separators placed next to each other between two
visible widgets rendered as one: [A, ' ', ' ', B] printed "A B",
[A, '|', '|', B] printed "A | B" and [A, ' ', '•', ' ', B] printed
"A•B". The TUI preview showed the same, so there was no way to stack
separators for a wider gap or to pad a custom glyph with spaces.

The backward scan that decides whether a separator renders treated any
earlier separator as if a widget that rendered empty sat between the
two. A visible earlier separator kept the boundary, so the current one
was dropped; a spacing-only one was removed and replaced by the current
one. That rule is for separators on either side of an empty widget,
where one of them has to go, but the scan applied it whether or not a
widget lay between them.

Separators with no widget between them now form one run that renders
whole: the scan steps over the run's earlier separators to the content
before it. Across an empty widget the rule is unchanged, per run: a run
with a visible separator keeps the boundary, and a spacing-only run is
replaced by the run after the empty widget. Only that run's first
separator removes the spacing it replaces, so the rest of the run is
kept.
@sirmalloc
sirmalloc merged commit 3a9fee7 into sirmalloc:main Oct 30, 2026
3 checks passed
pcvelz added a commit to pcvelz/ccstatusline-usage that referenced this pull request Nov 2, 2026
Upstream: weekly limit timers (sirmalloc#768), prompt cache miss widgets (sirmalloc#770), context window size env override (sirmalloc#352, sirmalloc#407), windowed speed fixes (sirmalloc#769, sirmalloc#771), separator/renderer fixes (sirmalloc#773), custom-command styling (sirmalloc#775), clickable cwd links (sirmalloc#744), account email from live credentials (sirmalloc#724), cache timer reported expiry + short toggle (sirmalloc#576, sirmalloc#592), claude-status lock (sirmalloc#635, sirmalloc#772), dedup token metrics (sirmalloc#622, sirmalloc#725, sirmalloc#735).
Not adopted: sirmalloc#765 terminal width from COLUMNS with fixed reserve - the fork keeps flexMode/compactThreshold and tmux pane priority.

Hand edits outside conflicts:
- README.md: kept fork Configure flex separator behavior bullet and older-updates details wrapper; dropped upstream Related Projects additions (claude-eta)
- AGENTS.md: kept fork terminal width wording (sirmalloc#765 not adopted)
- src/types/FlexMode.ts, src/tui/components/TerminalWidthMenu.tsx, src/tui/components/__tests__/TerminalWidthMenu.test.ts, src/tui/components/TerminalOptionsMenu.tsx, src/tui/components/__tests__/TerminalOptionsMenu.test.ts: restored, fork keeps terminal width settings
- src/tui/App.tsx, src/tui/components/index.ts, src/types/index.ts: re-wired terminal width menu and FlexMode export
- src/types/StatusJSON.ts: dropped fork duplicate prompt_cache key, upstream schema is a superset
- src/utils/terminal.ts: COLUMNS source gated on non-tmux so tmux pane width keeps priority
- src/widgets/WeeklyLimitTimer.ts, src/utils/usage-prefetch.ts, src/widgets/__tests__/WeeklyLimitTimer.test.ts: compat fix - fableUsage/fableResetAt -> weeklyFableUsage/weeklyFableResetAt
- src/widgets/ApiUsage.tsx: compat fix - Context Bar honors effective context window (override, clamp, 1.0M format); Weekly renders a valid value despite a session error
- src/widgets/__tests__/ApiUsage.localModelFallback.test.ts: output tokens no longer counted in context (upstream sirmalloc#730)
- src/utils/__tests__/renderer-flex-width.test.ts, src/utils/__tests__/renderer-ansi.test.ts, src/utils/__tests__/gradient.test.ts, src/utils/__tests__/renderer-separator-collapse.test.ts, src/utils/__tests__/config.test.ts, src/tui/__tests__/App.test.ts: fork deviation - fork width model (flexMode, 6-column full reserve, kept settings, Terminal Width menu entry)
- src/utils/__tests__/default-colors.test.ts: fork deviation - accept ansi256:/hex: defaults
- src/utils/__tests__/usage-prefetch.test.ts: fork deviation - weekly usage comes from the API, fork widget imports from ApiUsage
- src/widgets/__tests__/CurrentWorkingDir.test.ts, src/widgets/__tests__/CustomCommand.test.ts: Settings literal gains flexMode/compactThreshold
- src/utils/__tests__/context-window.test.ts, src/utils/__tests__/jsonl-metrics.test.ts: restored fork tests upstream dropped
- docs/test-retirements.md: ledger for the dev-reload snapshot-path and RemoteControlStatus glyph (sirmalloc#605) tests upstream replaced
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