Repository navigation
fix(custom-command): keep styling after the command's resets, drop output with no visible text, pipe the JSON as it arrived - #775
Merged
sirmalloc merged 5 commits intoOct 30, 2026
Conversation
…ble text A command with preserve colors on that colors a value which can be empty, such as printf '\033[32m%s\033[0m' "$branch", prints only the color codes when the value is empty. With preserve colors off those codes are stripped, the output is empty and the widget drops out. With it on, the codes were returned as the widget's text. The renderer decides whether a widget is there by whether its text is empty, not by its width, so the invisible widget kept its separators: "A | | B" in plain mode, and an empty segment with an arrow on each side in Powerline mode. Output with no visible text now renders the widget empty, whether or not its colors are preserved.
…ived The docs say a Custom Command receives the same JSON ccstatusline gets from Claude Code, plus terminal_width, so another status line tool can be chained in. The command got the schema-validated copy instead. Only the top level of that schema keeps keys it doesn't list. Nested objects drop them, so workspace.added_dirs, workspace.git_worktree and workspace.repo, which Claude Code sends, never reached the command, and neither did any new key under model, cost, context_window or rate_limits. Number strings also arrived as numbers. The entry point now keeps the parsed JSON next to the validated copy and passes it in the render context. The widget pipes that, and the other widgets keep reading the validated copy.
…and's resets A Custom Command with preserve colors keeps the command's own escape codes, and the renderer wraps them in the widget's background, bold and dim. A reset in the output (\e[0m, \e[22m or \e[49m) clears that styling too. Powerline mode re-applied the segment background after each reset, but not bold or dim. Plain mode re-applied nothing: with a widget background or a global background override, the text after the command's first reset sat on the terminal's default background, between the widget's colored padding cells, and lost its bold. Both modes now re-apply the widget's background, bold and dim after each color sequence in the output that clears them. 0 or an empty parameter clears all three, 49 the background, and 22 bold and dim. Each is tracked on its own: when the same sequence sets one again (1 for bold, 2 for dim, or a background color), the command's setting stays and only the others are re-applied. In plain mode the background is the global override when one is set, as for the rest of the widget.
# Conflicts: # src/ccstatusline.ts
# Conflicts: # docs/USAGE.md
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three fixes to Custom Command output and input:
\e[0mlost the widget's background, bold and dim. With a widget background or a global background override, that left a stretch of terminal background between the widget's colored padding cells. Powerline mode kept the background but lost bold and dim.printf '\033[32m%s\033[0m' "$value"when the value is empty, now drops out of the line the same way it does with preserve colors off. Before, it still took a slot: a doubled separator in plain mode (A | | Binstead ofA | B), or an empty segment with an arrow on each side in Powerline mode.terminal_width, asdocs/USAGE.mdsays. Before, it got the copy ccstatusline had checked against its schema. That copy drops nested fields the schema doesn't list (such asworkspace.added_dirs,workspace.git_worktree,workspace.repo, and any new key undermodel,cost,context_windoworrate_limits) and turns number strings into numbers. So a status line tool chained in as a Custom Command saw less than it would on its own.Why
Styling after resets. The plain renderer wraps preserve-colors output once: widget bold, dim and background in front, and their off codes after. Any
\e[0m,\e[22mor\e[49min the command's output turns that styling off for the rest of the widget. Powerline mode already put the segment background back after each such reset, but only the background. A widget with backgroundhex:112233, bold on and the commandprintf '\033[32m+5\033[0m \033[31m-3\033[0m', piped throughmainunder Bun in plain mode, renders the widget as:Nothing restores the background or bold after
+5\e[0m, so-3is drawn on the terminal's background in normal weight. The same happens with a global background override and no widget background. In Powerline mode the same widget gives\e[1m\e[48;2;17;34;51m \e[32m+5\e[0m\e[48;2;17;34;51m \e[31m-3\e[0m\e[48;2;17;34;51m \e[0m: the background comes back, bold doesn't.Output with no visible text. The widget strips escape codes only when preserve colors is off, then returns
output || null. With it on,\e[32m\e[0mis a non-empty string, so it was returned as the widget's text. The renderer decides whether a widget is there by whether its text is empty, not by its width, so the zero-width widget kept its separators and padding. The lineCustom Text "A", separator, that command with preserve colors on, separator,Custom Text "B", piped throughmain, prints[ A | | B ](brackets added). With preserve colors off it prints[ A | B ]. In Powerline mode, with backgrounds on the three widgets,maindraws an extra segment in the command's background color, with its two arrows, between A and B.The JSON piped to the command.
mainparses stdin, validates it withStatusJSONSchema, and keeps only the validated copy. The widget then serializescontext.data. The schema keeps unknown keys only at the top level (z.looseObject). Its nested objects are plainz.object, which drops keys it doesn't list, and its number fields convert number strings. With a Custom Command ofcat > captured.json; echo okand this payload:{"session_id":"s","model":{"id":"claude-opus-4-6","display_name":"Opus","family":"opus"},"workspace":{"current_dir":"/p","project_dir":"/p","added_dirs":["/e"],"git_worktree":"wt","repo":{"name":"app"}},"cost":{"total_cost_usd":"1.5"},"context_window":{"context_window_size":200000,"new_key":1},"rate_limits":{"five_hour":{"used_percentage":42,"resets_at":1774020000,"extra":true}},"top_level_unknown":{"a":1}}mainwrites:{"session_id":"s","model":{"id":"claude-opus-4-6","display_name":"Opus"},"workspace":{"current_dir":"/p","project_dir":"/p"},"cost":{"total_cost_usd":1.5},"context_window":{"context_window_size":200000},"rate_limits":{"five_hour":{"used_percentage":42,"resets_at":1774020000}},"top_level_unknown":{"a":1},"terminal_width":245}model.family,workspace.added_dirs,workspace.git_worktree,workspace.repo,context_window.new_keyandrate_limits.five_hour.extraare gone, and"1.5"became1.5. This branch writes the payload back unchanged, withterminal_widthadded.How
restoreBackgroundAfterResetsinansi.tsbecomesrestoreStyleAfterResets, which takes the background code plus the widget's bold and dim. For each color sequence in the text, it reads the parameters in order and tracks the background, bold and dim separately.0(or an empty parameter) clears all three,49the background, and22bold and dim. A later1,2or background color in the same sequence counts as the command's own setting, so it stays. After the sequence, it re-applies whatever was cleared and not set again. Extended color arguments are still skipped, so the22in38;5;22is not read as a reset.resolveBackgroundColorhelper thatapplyColorsWithOverridenow uses too),globalBoldor the widget's bold, and dim when it is on for the whole widget (not the parentheses-only setting). Claude Status's history strip also preserves its colors, but it only emits foreground codes, so this changes nothing for it. The sentence indocs/USAGE.mdthat said only Powerline backgrounds are restored now says both modes restore background, bold and dim.CustomCommandWidget.renderreturnsnullwhen the output has no visible text, whether or not colors are preserved. It checks withgetVisibleText, which also drops OSC 8 links with no text.rawDatain the render context. Custom Command serializesrawData(falling back todata) plusterminal_width. All other widgets still read the validateddata.Left as is:
\e[22m. The widget's bold, dim and background cover the whole widget. Powerline mode already did this for the background.[cmd: …]and never runs the command, so it isn't affected.Demo
Each case quits the TUI, then pipes a payload through the CLI. The dim text above the status line says what the command does.
printf '\033[32m12 added\033[0m, \033[31m3 removed\033[0m, 2 files'. Before, plain mode drops the background and bold after12 added, and Powerline drops the bold. After, the whole widget is gray and bold in both.printf '\033[32m%s\033[0m' "${VIRTUAL_ENV##*/}"with no virtualenv active, with preserve colors off on line 1 and on on line 2. Before, line 2 has a doubled separator in plain mode, and in Powerline an empty segment that also moves Ctx on to the next theme color. After, both lines match..workspace.git_worktreeand.workspace.added_dirsfrom its stdin withjq, fed a payload that sets both. Before, it printsworktree: none, added dirs: 0. After, it printsworktree: feature-login, added dirs: 1.Powerline
Styling after the command's resets, before:
Styling after the command's resets, after:
Output with no visible text, before:
Output with no visible text, after:
JSON piped to the command, before:
JSON piped to the command, after:
Plain
Styling after the command's resets, before:
Styling after the command's resets, after:
Output with no visible text, before:
Output with no visible text, after:
JSON piped to the command, before:
JSON piped to the command, after:
Testing
mainas 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 lintis clean, andbun testgives 3730 pass, 0 fail. The built CLI under Bun 1.4.4 and Node 26.13.0 matchesmainin plain and Powerline mode with the baseline configs, and the TUI opens and exits cleanly.bun test: 3195 pass, 0 fail across 178 files (main: 3168 pass, 0 fail). There are 27 new tests, and all 27 fail againstmain's source:ansi.test.ts, 20: bold and dim restored after0, an empty parameter and22, each attribute tracked on its own, and sequences that leave bold on, so it must not be re-applied after them (22;1,2,49,38;5;22and others). Each of those comes after a\e[0mthat must bring bold back, so it fails onmaintoo.mainhas norestoreStyleAfterResets, so these ran againstmainthrough a wrapper under the new name that passes the style's background tomain'srestoreBackgroundAfterResets. The existing background tests move to the new name and pass on both.renderer-ansi.test.ts, 2: plain mode keeps a widget background with bold, and a global background override, after the command's resets.renderer-powerline-separators.test.ts, 2: Powerline re-applies bold and dim in a segment with a background, and bold in one without.CustomCommand.test.ts, 3: color codes around an empty value, and an OSC 8 link with no text, render nothing with preserve colors on. The command gets the JSON as it arrived, not the validated copy.bun run lint: clean.main: 92 pass, 2 fail). The 2 failures are the same on both: existingCustomCommand.test.tscache tests whose temp-home helper callsvi.spyOn(os, 'homedir'), which Node's ESM doesn't allow. All 27 new tests pass under Node. Againstmain's source, with the same wrapper, Node gives 92 pass and 29 fail: the 27 new tests plus the same 2.distunder Bun 1.4.2 and Node 26.10.0:main.mainas intended.HOME,mainagainst this branch: the outputs quoted under Why, plus the global background override case in plain mode, which now re-applies the override after each reset.