Conversation
With several concurrent Claude Code sessions every statusline invocation is a fresh process, so per-render fixed costs multiply by session count. - terminal: probe each ancestor with a single `ps -o ppid=,tty=` call instead of two spawns per ancestor; the walk runs per render on macOS where numeric widths are deliberately not persisted - usage-fetch, claude-service-status: load https-proxy-agent (and its transitive graph) on demand, only when a proxy is actually configured Benchmark (13MB transcript, 4 parallel workers x 20 renders via stdin): 27.2 -> 20.6 CPU-seconds total, 0.34 -> 0.26 CPU-s per render (-24%), wall 7.5s -> 5.6s. node --cpu-prof shows the undici/proxy graph gone from startup (compileForInternalLoader 40.7ms -> 7.5ms per render). Co-Authored-By: Claude Code <noreply@anthropic.com>
#1) * feat(widgets): add Tokens Last Turn widget for per-message token usage Implements sirmalloc#44: a widget showing token usage for the most recent assistant message only. The usage is collected in the shared single-pass transcript scan (getTranscriptAnalysis) behind an includeLastTurnTokens opt-in and is deduplicated by message.id: Claude Code writes one JSONL entry per content block of a single API call, all sharing one message.id, so entries are grouped per id and only the highest output_tokens is kept instead of summing every entry (sirmalloc#549). Streaming partials resolve to the finalized value, and the in-flight call still counts while it streams. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(jsonl): keep sidechain and API-error usage out of the last-turn widget trackLastTurnTokens now runs only for main-chain usage entries, matching the recency rule already used for context length. While a Task subagent runs, its assistant entries are the newest usage rows in the transcript, so without the gate Turn: reported the subagent call as the user's turn. Also documents the widget in docs/USAGE.md. Co-Authored-By: Claude Code <noreply@anthropic.com> * ci: trigger workflow registration for PR branch Co-Authored-By: Claude Code <noreply@anthropic.com> * ci: trigger CI after enabling Actions on the fork Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
…wiring - terminal: check the ancestor TTY before reassigning pid so each loop iteration reads as "one ps answer, one pid"; no call-order change - claude-service-status: cover the lazy proxy-agent branch with tests (agent attached when HTTPS_PROXY is set, absent otherwise), matching the usage-fetch proxy coverage Co-Authored-By: Claude Code <noreply@anthropic.com>
…ation - ps: use two -o flags instead of the comma list; FreeBSD's parser treats everything after the first '=' as one header, which collapsed the combined form into a single bogus column and dropped the probe to the tput fallback. Still one spawn per ancestor. - walk: start at process.ppid and read each generation's TTY before terminating on its PPID, restoring the old two-spawn walk's generation coverage (1..8) and keeping a dead-end PPID from hiding a valid width on the same ps row. Known narrowing vs the old sequence: if the combined lookup fails entirely, the walk stops (the old two-call flow could still continue on the separately obtained PPID). Co-Authored-By: Claude Code <noreply@anthropic.com>
* perf: cut per-repaint subprocess and module-graph cost (sirmalloc#397) With several concurrent Claude Code sessions every statusline invocation is a fresh process, so per-render fixed costs multiply by session count. - terminal: probe each ancestor with a single `ps -o ppid=,tty=` call instead of two spawns per ancestor; the walk runs per render on macOS where numeric widths are deliberately not persisted - usage-fetch, claude-service-status: load https-proxy-agent (and its transitive graph) on demand, only when a proxy is actually configured Benchmark (13MB transcript, 4 parallel workers x 20 renders via stdin): 27.2 -> 20.6 CPU-seconds total, 0.34 -> 0.26 CPU-s per render (-24%), wall 7.5s -> 5.6s. node --cpu-prof shows the undici/proxy graph gone from startup (compileForInternalLoader 40.7ms -> 7.5ms per render). Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(review): clarify width-walk invariant and test status-page proxy wiring - terminal: check the ancestor TTY before reassigning pid so each loop iteration reads as "one ps answer, one pid"; no call-order change - claude-service-status: cover the lazy proxy-agent branch with tests (agent attached when HTTPS_PROXY is set, absent otherwise), matching the usage-fetch proxy coverage Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(terminal): restore walk parity and BSD-safe ps format per verification - ps: use two -o flags instead of the comma list; FreeBSD's parser treats everything after the first '=' as one header, which collapsed the combined form into a single bogus column and dropped the probe to the tput fallback. Still one spawn per ancestor. - walk: start at process.ppid and read each generation's TTY before terminating on its PPID, restoring the old two-spawn walk's generation coverage (1..8) and keeping a dead-end PPID from hiding a valid width on the same ps row. Known narrowing vs the old sequence: if the combined lookup fails entirely, the walk stops (the old two-call flow could still continue on the separately obtained PPID). Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
…der path (#3) * fix(review): clarify width-walk invariant and test status-page proxy wiring - terminal: check the ancestor TTY before reassigning pid so each loop iteration reads as "one ps answer, one pid"; no call-order change - claude-service-status: cover the lazy proxy-agent branch with tests (agent attached when HTTPS_PROXY is set, absent otherwise), matching the usage-fetch proxy coverage Co-Authored-By: Claude Code <noreply@anthropic.com> * perf(widgets): load Ink editors only when opened Extend the lazy TUI boundary from 35440e4 to widget editors: the registry previously initialized Ink's useInput/reconciler/Yoga graph on every piped render. Move editor implementations into lazy chunks and retain synchronous renderEditor APIs through React Suspense. Add an isolated startup regression check and wait for lazy editors in interaction tests.\n\nVerified: bun test (2361 pass), bun run lint, Node/Bun built renders and 13MB four-worker benchmarks. Node14 distribution build failure predates this change and is tracked separately in the perf verification report. * docs(perf): record PR 2 adversarial findings and follow-up benchmarks --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
The fork's main had PR #2's pre-review revision (177686f); this brings in the final revision from ao/ccstatusline-2/root: per-repaint subprocess and module-graph cost cut (sirmalloc#397), the width-walk invariant clarification, and the BSD-safe two-flag ps format with restored walk parity (0fc829c). Conflicted on src/utils/terminal.ts: kept the TTY-probe-before-break ordering from the final revision and dropped the obsolete duplicate check after the parentPid break. Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix(jsonl): dedup cumulative token totals by message.id (#8, sirmalloc#549) Claude Code writes one JSONL entry per content block of a single API call, all sharing one message.id. The cumulative token path summed every entry, inflating Tokens Input/Output/Total by up to ~1.84x. Mirror the grouping already used for Tokens Last Turn: consecutive counted entries sharing an id contribute one usage (prompt side once, output as the running max). - transcripts without duplicate-id entries are unchanged (existing tests) - streaming partials resolve to the finalized value, counted once mid-stream - sidechain/API-error rows keep their current treatment in sums and recency Added vitest cases on a synthetic sirmalloc#549-shaped transcript asserting exact cumulative totals (12 in / 292 out / 16660 total vs inflated 16/393/...), streaming partials with ids, and a sidechain per-content-block group. Co-Authored-By: Claude Code <noreply@anthropic.com> * docs(jsonl): note the pinned recency snapshot within a message.id group Review follow-up on #9: the duplicate early return deliberately keeps the context-length snapshot from the group's first counted entry. Write the invariant down at the return so it survives as a conscious assumption. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
…es (#10) * feat(widgets): compact label presets with global and per-widget toggles Add abbreviated label presets for labeled widgets, complementary to the free-form label editing proposed upstream (sirmalloc#601): - New `compactLabels` settings flag (default off). Additive v4 key: old configs parse unchanged and render identically, no version bump needed. - Presets live in one map (`Model:` -> `M:`, `Context:` -> `Ctx:`, `Cost:` -> `` — the value's own `$` is the glyph) inside `formatRawOrLabeledValue`, so every widget routed through the helper picks them up with no per-widget changes. Model, Session Cost, and Context Bar moved onto the helper. - One-click global toggle in Global Overrides `(j)`; per-widget override via `(j) compact label` in the items editor (metadata `compactLabel`), which wins over the global setting in both directions. Capability for the editor keybind is derived from a cached preview probe of the widget registry. - Extend the Git Branch width-cap pattern to Current Working Dir and Session Name (`w` max-width keybind). - Document per-widget padding/separator/label width overhead in docs/USAGE.md; defaults unchanged. Tests: preset resolution, toggle metadata, global injection precedence, migration parity (v1->v4 and schema-less v4 parse), registry-wide `j` keybind reservation, capable-type probe sanity, Global Overrides `(j)` keystroke test. bun test 2400 pass; bun run lint clean. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(tui): compact-label toggle cycles over the effective state The per-widget (j) toggle only wrote/removed 'true', so with the global Compact Labels setting on it became a visual no-op: the render-time injection re-compactified the widget after the key was removed, and no row marker showed the effective state. - toggleCompactLabel now takes the settings and cycles the EFFECTIVE state: compact (saved 'true' or inherited global on) -> write 'false'; forced off -> drop the key and inherit; otherwise -> write 'true'. - getCompactLabelModifierText reports the effective state: '(compact label)' / '(compact label: on)' / '(compact label: off)'. - ItemsEditor/input-handlers pass settings through to both. - docs/USAGE.md: fix preset savings arithmetic (Model -4, Context -4). Co-Authored-By: Claude Code <noreply@anthropic.com> * test(renderer): cover (j) toggle forcing labels off while global compact is on Co-Authored-By: Claude Code <noreply@anthropic.com> * style: fix import order in compact-labels renderer test Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
…sirmalloc#602) (#11) * feat(cli): non-interactive subcommands for agent-driven configuration (sirmalloc#602) Add `src/utils/cli.ts` and wire it into mode detection in src/ccstatusline.ts: args present + TTY stdin dispatches the CLI before the interactive TUI; piped stdin keeps the statusline render path. Subcommands operating on the same settings file (load/save/migration via src/utils/config.ts): - get [--json] - print the effective post-migration config - widget add <line> <widget> [--index N] [--option value ...] - widget remove <line> <index-or-type> - widget move <line> <index> --to <index> - set <option-path> <value> - global options, JSON or plain string values - validate [--file <path>] - exit 0/1 with a machine-readable report - help Guarantees preserved: mutations refuse to run on an unreadable/invalid config (never overwrite it with in-memory defaults), every write is validate-before-write through SettingsSchema and persists via saveSettings() (atomic temp+rename, symlink write-through). All errors are single-line and machine-parseable with a --json variant. No new dependencies; runtime stays Bun+Node compatible. Vitest coverage (34 cases) for every subcommand including error paths, idempotency, invalid-config refusal, and symlink write-through. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(cli): review follow-ups - hasOwn option guard, --json argv walk, README section - set <path>: guard option paths with Object.prototype.hasOwnProperty instead of `in`, so inherited keys like toString/constructor cannot pass the check, get stripped by the schema, and report a false success (Node 14-safe; no Object.hasOwn). Regression test: set toString / powerline.constructor fail and leave the file untouched. - extractJsonFlag(): walk argv and treat only a standalone --json as the output flag; tokens filling a value or boolean option are kept intact, so 'widget add 0 custom-text --customText --json' no longer loses its value. Unit tests for the flag/value boundary plus an add-persistence case. - README: new 'Non-Interactive CLI' section documenting the subcommands and noting that get/validate write defaults on a missing settings.json (first run) while never overwriting an existing unreadable/invalid file. Co-Authored-By: Claude Code <noreply@anthropic.com> * docs(cli): note first-run default config creation in help output Adds a line to the CLI usage block so agent loops see that get and validate write the default config when settings.json is missing (the behavior itself is unchanged, matching the TUI first-run contract). Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
Fork builds carry the base upstream version plus a .forkN suffix (currently 2.2.30.fork1), so the TUI title, --version output and any build are visibly distinguishable from the npm release. Rebase the suffix onto the new base version at each upstream sync. Co-Authored-By: Claude Code <noreply@anthropic.com>
…only (#12) Add a top-level `theme` setting that cycles a POWERLINE_THEMES segment palette across widgets as foreground colors when powerline is disabled. The theme's bg[] array is the vivid palette (fg[] holds the dark text-on-segment colors); ansi16-level bg* names are converted with the existing bgToFg() helper. Semantics: explicit per-widget colors win over the theme, theme beats widget defaults; explicit colors keep their palette slot so the rhythm of the surrounding widgets is stable. Separators and flex separators are neither themed nor consume slots; merged widgets share one slot; preserves-colors widgets keep their intrinsic ANSI output and still advance the slot - both mirroring the powerline renderer. Unset, 'custom', or unknown names disable theming. Additive optional schema key - configs written before it parse unchanged (no version bump); `ccstatusline set theme <name>` works through the existing generic `set` allowlist with zero CLI changes. Closes the feature half of sirmalloc#604. Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com>
…CLI warnings Follow-up to the regular-mode theme feature: - Parametrized tests: every POWERLINE_THEMES entry (custom excluded) cycles its bg[] palette identically at color levels 1/2/3, plus a guard that every theme ships a non-empty palette at every level. - TUI: the theme selector now also serves regular (non-powerline) mode via a new mode prop - the main menu gains a Theme entry (hidden while powerline is enabled, which has its own picker); None clears the setting; the powerline-only "copy theme to widget colors" flow stays powerline-only. - CLI: validate reports unknown theme names as warnings (valid stays true, warnings array in --json output), set theme warns on unknown names while still writing them - a typo must not invalidate a config. Co-Authored-By: Claude Code <noreply@anthropic.com>
…nt is detected Powerline/Nerd Font glyphs (private-use area) render as mojibake when the terminal has no matching font; checkPowerlineFonts() could detect this, but only the TUI setup flow surfaced it. Now every powerline separator, cap, and user-typed separator character is checked at render time: PUA glyphs degrade to '|' when no font is detected and pass through untouched otherwise. Detection is a directory scan, cached for the process lifetime; installing fonts through the TUI resets the cache. Tests can pin the detection via setCachedPowerlineFontStatus() so renders stay deterministic regardless of the machine's fonts. Closes the bug half of sirmalloc#604. Co-Authored-By: Claude Code <noreply@anthropic.com>
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.
Closes the bug half of #604 (the feature half is handled by #607).
Problem
The default powerline separator is the private-use-area glyph U+E0B0. Without a Powerline/Nerd Font it renders as mojibake, and while
checkPowerlineFonts()can detect the absence of fonts, that warning only surfaced in the TUI setup flow — enabling powerline via config/CLI on a machine without fonts produced garbage glyphs in every render with no hint of the cause. Regular-mode separators suffer the same when users type PUA characters as their separator.Fix
Render-time font-safe pass, applied to:
separators,startCaps,endCaps(renderer's powerline path);characteranddefaultSeparator(renderer's regular path).Mechanics:
fontSafeSeparator(): text containing a PUA glyph (BMP PUAU+E000–F8FFplus the astral Nerd Font planes) degrades to'|'when the detection reports no font; everything else passes through untouched, and glyphs are kept when a font is detected.checkPowerlineFonts(), a directory scan) is cached for the process lifetime so renders stay cheap;installPowerlineFonts()resets the cache so newly installed fonts stop the fallback.setCachedPowerlineFontStatus()is a small test seam: tests pin the detection result, so renderer output is deterministic regardless of the machine's fonts (several existing glyph-asserting tests now seedinstalled: true).Known limitation
Detection is disk-based (font directories + name patterns) — it cannot know which font the terminal actually uses. A machine with a Nerd Font installed on disk but a non-Nerd font selected in the terminal will keep the glyphs (and the mojibake). Surfacing a hint in that case would need a config knob or TUI message; left out to keep this change small.
Verification
separator-font-fallback.test.ts: glyph detection (BMP + astral), fallback with fonts absent, pass-through with fonts present, and full renders in powerline mode and regular mode (PUA separator character) asserting|and noU+E0B0.renderer-dim,renderer-flex-width) pinned via the test seam — all green.bun testfull suite: only the pre-existing timing-flaky subprocess tests flicker (pass in isolation);bun run lintclean;bun run buildOK.U+E0B0separators rendered through the built binary on a font-less machine emits|and zero mojibake bytes.🤖 Generated with Claude Code