Skip to content

Commit 5f1a374

Browse files
committed
release: v3.6.0 — daemon-mode by default, fix --inline opt-out, py3.9 compat
`cs --setup` now writes `cs render` + refreshInterval=1 by default and auto-starts the daemon, so the out-of-box experience is the daemon path (~3-5ms/tick, <1% CPU) instead of inline (~30ms/tick, ~3% CPU at 1Hz). Most users never opted into --fast manually; the inline default was costing them CPU for no reason. Bug fixed along the way: `cs --setup --inline` previously didn't downgrade existing fast-mode users because ensure_statusline_configured used a binary fast=True/False where False meant 'preserve existing'. Switched to tri-state: None=preserve (auto-repair), True=force fast, False=force inline. Daily auto-repair still preserves the user's choice; explicit user requests are now respected. Also folds in the py3.9 compat fixes (bf31092, b428037) and the CI/CoC/ hero/launch-kit work shipped in 650a575..4b86169 since 3.5.1.
1 parent 4b86169 commit 5f1a374

8 files changed

Lines changed: 121 additions & 44 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "claude-statusbar",
3-
"description": "Lightweight Claude Code status-line monitor with switchable styles, themes, and slash commands. v3.4 adds per-segment color management (each metric colors itself by its own severity), classic theme adoption, two new themes (catppuccin-mocha, tokyo-night), and per-severity color overrides via `cs config set color_ok|warn|hot`. v3.2 daemon fast-mode (`cs --setup --fast`) still ships for ~5× lower CPU at refreshInterval=1. Requires the `cs` CLI from PyPI (`pip install claude-statusbar` or `uv tool install claude-statusbar`).",
4-
"version": "3.5.1",
3+
"description": "Lightweight Claude Code status-line monitor with switchable styles, themes, and slash commands. v3.6.0 makes daemon (fast) mode the default for `cs --setup` — under 1% CPU continuously instead of ~3% inline at refreshInterval=1; pass `--inline` to opt back. v3.4 added per-segment color management, classic theme adoption, two new themes (catppuccin-mocha, tokyo-night), per-severity color overrides via `cs config set color_ok|warn|hot`. Requires the `cs` CLI from PyPI (`pip install claude-statusbar` or `uv tool install claude-statusbar`).",
4+
"version": "3.6.0",
55
"author": {
66
"name": "leeguooooo",
77
"email": "leeguooooo@gmail.com"

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,41 @@ For a quick overview of the latest release, see the
99

1010
---
1111

12+
## v3.6.0 — 2026-05-08
13+
14+
### Changed
15+
- **`cs --setup` defaults to daemon (fast) mode.** Previously you had to remember
16+
`--fast` to get the long-lived daemon + `cs render` thin client; the bare
17+
`cs --setup` wrote inline mode at refreshInterval=1, which costs ~3% CPU
18+
continuously. Daemon mode keeps it under 1% with smoother per-second ticks.
19+
Existing users running `cs --setup` after upgrading will be auto-bumped from
20+
inline to daemon. To opt out, run `cs --setup --inline`. The legacy `--fast`
21+
flag still works (no-op now). Daily auto-repair (background) preserves the
22+
user's existing fast/inline choice — it doesn't reach into your settings to
23+
change policy on you.
24+
25+
### Fixed
26+
- **`cs --setup --inline` now actually downgrades.** In 3.5.x, passing `fast=False`
27+
to `ensure_statusline_configured` quietly preserved an existing fast-mode
28+
config (the OR-with-existing logic was meant for the auto-repair path but
29+
blocked explicit user requests). The function is now tri-state:
30+
`fast=None` preserves existing (auto-repair); `fast=True/False` is an
31+
explicit user request and is respected.
32+
- **Python 3.9 compatibility.** `updater.py` and `cli.py` used PEP-604 union
33+
syntax (`X | None`) at runtime, which 3.9 doesn't support. Added
34+
`from __future__ import annotations` so all annotations are lazy strings.
35+
CI on 3.9 was failing in 3.5.x — verified now passing on 3.9–3.12.
36+
37+
### Added
38+
- GitHub Actions CI: pytest matrix on Python 3.9–3.12, ruff lint job,
39+
cancel-in-progress concurrency, `contents: read` permissions only.
40+
- `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1).
41+
- Animated hero GIF at `docs/images/hero.gif` (driven by `scripts/hero.tape`,
42+
rebuilt via `bash scripts/build-hero-gif.sh`).
43+
- Issue & PR templates under `.github/`.
44+
45+
---
46+
1247
## v3.5.1 — 2026-05-08
1348

1449
### Changed

README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ Lightweight Claude Code status-line monitor. Shows your 5h / 7d rate-limit usage
3737

3838
## Latest release
3939

40-
**v3.5.1** (2026-05-08) — `npx skills add` install path, `show_cache_age` on by default.
40+
**v3.6.0** (2026-05-08) — **`cs --setup` now defaults to daemon (fast) mode**: under 1% CPU continuously instead of ~3% inline. Pass `--inline` to opt back. Also: py3.9 compat fixes, GitHub Actions CI, animated hero GIF.
41+
42+
**v3.5.1**`npx skills add` install path, `show_cache_age` on by default.
4143

4244
**v3.5.0** — consolidated `claude-statusbar` skill: say *"switch theme to nord"* / *"余量颜色改成 #4ec85b"* and Claude Code routes it to the right `cs` command.
4345

4446
**v3.4** — per-segment color management (each metric colors itself by its own severity), classic style finally respects themes, two new themes (`catppuccin-mocha`, `tokyo-night`), per-severity color overrides via `cs config set color_ok|warn|hot`.
4547

46-
**v3.2** — daemon fast-mode for ~5× lower CPU at `refreshInterval=1`.
48+
**v3.2** — daemon fast-mode (now the default in v3.6.0) for ~5× lower CPU at `refreshInterval=1`.
4749

4850
Full release notes in [CHANGELOG.md](CHANGELOG.md).
4951

@@ -90,13 +92,13 @@ Then add to `~/.claude/settings.json`:
9092
{
9193
"statusLine": {
9294
"type": "command",
93-
"command": "cs",
95+
"command": "cs render",
9496
"refreshInterval": 1
9597
}
9698
}
9799
```
98100

99-
`cs --setup` writes `refreshInterval: 1` by default so the cache-age countdown ticks visibly. At 1Hz, the inline `cs` command runs ~30ms per render (~3% CPU continuously); if that's a concern, run `cs --setup --fast` instead — daemon mode brings it under 1%. To go quieter, set `refreshInterval` to a higher value (`30`, `60`) — `cs --setup` will preserve any explicit value you've already chosen.
101+
`cs --setup` (since v3.6.0) writes `cs render` + `refreshInterval: 1` by default — daemon mode, under 1% CPU continuously, smooth per-second ticks for the cache-age countdown. The daemon is auto-started by `cs --setup` and lazy-respawns on `cs render` if it ever dies, so you never see a frozen bar. To opt out and use the legacy inline path, run `cs --setup --inline` (writes plain `cs`, ~3% CPU at 1Hz). To go quieter regardless of mode, set `refreshInterval` to a higher value (`30`, `60`) — `cs --setup` preserves any explicit value you've already chosen.
100102

101103
### Skill-only install (already have `cs`)
102104

@@ -215,21 +217,20 @@ Set via `cs config set <key> <value>`. Wipe everything back to defaults with `cs
215217

216218
Override per-invocation via `--style` / `--theme` flags or `CLAUDE_STATUSBAR_STYLE` / `CLAUDE_STATUSBAR_THEME` env vars.
217219

218-
## Fast mode — for `refreshInterval: 1`
220+
## Fast mode (daemon) — default since v3.6.0
219221

220-
If you've set `"refreshInterval": 1` in `settings.json` (so the cache-age widget ticks every second), the default `cs` command runs ~45ms per render = ~4% CPU continuously. Fast mode brings that down to ~3-5ms per render = under 1% CPU by keeping a long-lived `cs daemon` that pre-renders into `~/.cache/claude-statusbar/rendered.ansi`. The statusLine command becomes `cs render` a thin reader that just `cat`s the file.
222+
`cs --setup` writes `cs render` + `refreshInterval: 1` by default. A long-lived `cs daemon` pre-renders into `~/.cache/claude-statusbar/rendered.ansi`; the statusLine command (`cs render`) is a thin reader that just `cat`s the file. Each tick is ~3-5ms, so total CPU stays under 1% continuously. The legacy inline path (~30ms/tick, ~3% CPU at 1Hz) is still available via `cs --setup --inline`.
221223

222224
```bash
223-
cs --setup --fast # writes settings.json + spins up the daemon
224-
cs daemon status # check it's alive
225+
cs --setup # default: daemon mode, auto-starts the daemon
226+
cs --setup --inline # opt out, use legacy inline path
227+
cs daemon status # check the daemon is alive
225228
cs daemon stop # stop the daemon (statusLine falls back to inline)
226229
cs daemon start # start it again
227230
```
228231

229232
Crash safety: if the daemon dies or freezes, `cs render` notices `rendered.meta.json` is older than 5s and falls back to inline render — and lazily re-spawns the daemon in the background. You never see a frozen status line.
230233

231-
To revert: `cs --setup` (no `--fast`) restores the bare-`cs` legacy command.
232-
233234
### Optional: auto-start on login (launchd / systemd)
234235

235236
Lazy-spawn (above) covers most cases — the daemon comes up on first `cs render`. If you want stronger guarantees (auto-start at login, OS restarts the daemon on crash, survives reboots without `cs render` needing to fire first):
@@ -297,8 +298,9 @@ cs preview # render every style × theme with YOUR real dat
297298
cs preview --theme nord # filter to one theme
298299
cs preview --style hairline --theme dracula # one specific combo
299300

300-
# Daemon mode (v3.2+, opt-in)
301-
cs --setup --fast # switch statusLine to `cs render` + start daemon
301+
# Daemon mode (default since v3.6.0; v3.2 introduced it as opt-in)
302+
cs --setup # default: writes `cs render` + starts daemon
303+
cs --setup --inline # opt out, use legacy inline path
302304
cs daemon start # start daemon (manual)
303305
cs daemon stop # stop daemon
304306
cs daemon status # pid + rendered.ansi freshness

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "claude-statusbar"
7-
version = "3.5.1"
7+
version = "3.6.0"
88
authors = [
99
{name = "leeguooooo", email = "leeguooooo@gmail.com"}
1010
]

src/claude_statusbar/cli.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,10 +302,14 @@ def main():
302302
parser.add_argument(
303303
"--fast",
304304
action="store_true",
305+
help=argparse.SUPPRESS, # daemon mode is the default since 3.6.0
306+
)
307+
parser.add_argument(
308+
"--inline",
309+
action="store_true",
305310
help=(
306-
"When used with --setup, install the Phase B daemon mode: "
307-
"statusLine command becomes `cs render` (3-5ms) backed by a "
308-
"long-lived daemon. Use this when refreshInterval is 1 second."
311+
"When used with --setup, opt out of daemon mode and use the "
312+
"legacy inline path (no background daemon). Higher per-tick CPU."
309313
),
310314
)
311315
parser.add_argument(
@@ -422,7 +426,10 @@ def env_float(name: str) -> float | None:
422426

423427
if args.setup:
424428
from .setup import run_setup
425-
return run_setup(verbose=True, fast=args.fast)
429+
# Daemon (fast) mode is the default since 3.6.0; --inline opts out.
430+
# Keep the legacy --fast flag accepted (no-op) so existing scripts work.
431+
fast = not args.inline
432+
return run_setup(verbose=True, fast=fast)
426433

427434
if args.install_deps:
428435
print("Installing claude-monitor for full functionality...")

src/claude_statusbar/setup.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import shutil
1616
import sys
1717
from pathlib import Path
18-
from typing import Tuple
18+
from typing import Optional, Tuple
1919

2020
from .cache import atomic_write_text
2121

@@ -140,28 +140,34 @@ def _existing_uses_render(existing) -> bool:
140140
return len(parts) >= 2 and parts[1] == "render"
141141

142142

143-
def ensure_statusline_configured(fast: bool = False) -> Tuple[bool, str]:
143+
def ensure_statusline_configured(fast: Optional[bool] = None) -> Tuple[bool, str]:
144144
"""Silently ensure settings.json has *our* statusLine config.
145145
146+
`fast` is tri-state:
147+
- ``None`` (default): preserve the user's existing fast/inline choice;
148+
for fresh writes (no statusLine yet), use daemon mode (the 3.6.0
149+
default). This is what the daily auto-repair path passes — it
150+
should never make a policy decision on the user's behalf.
151+
- ``True``: force daemon mode (``cs render``). Used when the user runs
152+
``cs --setup`` (3.6.0 default) or the explicit ``cs --setup --fast``.
153+
- ``False``: force inline mode. Used when the user runs
154+
``cs --setup --inline`` to opt out.
155+
146156
Behavior:
147-
- missing → write our config (honors `fast` arg)
157+
- missing → write our config (fast=True if None, else honors arg)
148158
- foreign cmd → leave alone (don't overwrite another tool's setup)
149-
- our cmd, stale path → refresh, *preserving* the user's existing
150-
fast/inline choice (so the daily auto-repair doesn't downgrade
151-
a user who explicitly opted into `cs --setup --fast`).
152-
- our cmd, current → no-op
153-
154-
`fast=True` only forces the write to `cs render`. `fast=False` does
155-
NOT force a downgrade — it just means "if you have to write fresh,
156-
pick the inline form". Existing fast-mode entries are left alone.
159+
- our cmd → refresh path; ``fast=None`` preserves existing,
160+
``fast=True/False`` forces the write.
157161
158162
Returns (changed, message).
159163
"""
160164
settings = _read_settings()
161165
existing = settings.get("statusLine")
162166

163167
if existing is None:
164-
desired = _statusline_config(fast=fast)
168+
# Fresh install: default to fast/daemon since 3.6.0.
169+
write_fast = True if fast is None else fast
170+
desired = _statusline_config(fast=write_fast)
165171
settings["statusLine"] = desired
166172
if _write_settings(settings):
167173
return True, f"Added statusLine config to {SETTINGS_PATH}"
@@ -177,11 +183,13 @@ def ensure_statusline_configured(fast: bool = False) -> Tuple[bool, str]:
177183
f"manually if you want claude-statusbar."
178184
)
179185

180-
# Ours already. Compute desired command preserving (a) fast mode if the
181-
# user currently uses it (so the daily auto-repair never downgrades them)
182-
# and (b) any explicit refreshInterval the user already set, so we don't
183-
# silently bump someone's 60s cadence to our 1s default.
184-
effective_fast = fast or _existing_uses_render(existing)
186+
# Ours already. Determine effective_fast:
187+
# - fast=None → preserve user's existing choice
188+
# - fast=bool → respect it as an explicit user request
189+
if fast is None:
190+
effective_fast = _existing_uses_render(existing)
191+
else:
192+
effective_fast = fast
185193
existing_refresh = existing.get("refreshInterval")
186194
if isinstance(existing_refresh, (int, float)) and existing_refresh > 0:
187195
effective_refresh = int(existing_refresh)
@@ -287,12 +295,14 @@ def install_skills(force: bool = False) -> Tuple[int, list[str]]:
287295
return installed, skipped
288296

289297

290-
def run_setup(verbose: bool = True, install_cmds: bool = True, fast: bool = False) -> int:
298+
def run_setup(verbose: bool = True, install_cmds: bool = True, fast: bool = True) -> int:
291299
"""Interactive setup: configure the statusLine and install slash commands.
292300
293-
`fast=True` opts into Phase B daemon mode (statusLine command becomes
294-
``cs render``). Also kicks off ``cs daemon start`` so the user sees
295-
the speedup immediately.
301+
`fast=True` (default since 3.6.0) installs Phase B daemon mode: statusLine
302+
command becomes ``cs render``, backed by a long-lived daemon. Each tick is
303+
~3-5ms vs ~30ms inline, which keeps continuous CPU under 1% at
304+
refreshInterval=1 (vs ~3% in inline mode). Pass ``fast=False`` to opt back
305+
into the legacy inline path.
296306
297307
Returns exit code (0 success, 1 partial failure, 2 unrecoverable).
298308
"""

tests/test_daemon.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ def test_existing_uses_render_detects_fast_mode():
486486

487487

488488
def test_ensure_statusline_preserves_fast_mode(tmp_path: Path, monkeypatch):
489-
"""MUST-FIX from codex review: the daily auto-repair (default fast=False)
489+
"""MUST-FIX from codex review: the daily auto-repair (fast=None default)
490490
must NOT downgrade a user who already opted into `cs render`."""
491491
from claude_statusbar import setup as setup_mod
492492
settings = tmp_path / "settings.json"
@@ -496,8 +496,8 @@ def test_ensure_statusline_preserves_fast_mode(tmp_path: Path, monkeypatch):
496496
"statusLine": {"type": "command", "command": "/abs/path/cs render"},
497497
}), encoding="utf-8")
498498

499-
# Daily auto-repair tick — default fast=False.
500-
changed, message = setup_mod.ensure_statusline_configured(fast=False)
499+
# Daily auto-repair tick — fast=None (preserve), the new default in 3.6.0.
500+
changed, message = setup_mod.ensure_statusline_configured()
501501

502502
# Read what's there now.
503503
after = json.loads(settings.read_text(encoding="utf-8"))
@@ -508,6 +508,25 @@ def test_ensure_statusline_preserves_fast_mode(tmp_path: Path, monkeypatch):
508508
)
509509

510510

511+
def test_ensure_statusline_inline_explicit_downgrades(tmp_path: Path, monkeypatch):
512+
"""fast=False is now an EXPLICIT user request to switch to inline mode,
513+
not a preserve-existing signal. Verify it actually downgrades."""
514+
from claude_statusbar import setup as setup_mod
515+
settings = tmp_path / "settings.json"
516+
monkeypatch.setattr(setup_mod, "SETTINGS_PATH", settings)
517+
settings.write_text(json.dumps({
518+
"statusLine": {"type": "command", "command": "/abs/path/cs render"},
519+
}), encoding="utf-8")
520+
521+
changed, _ = setup_mod.ensure_statusline_configured(fast=False)
522+
523+
after = json.loads(settings.read_text(encoding="utf-8"))
524+
assert changed is True
525+
assert not after["statusLine"]["command"].endswith(" render"), (
526+
"explicit fast=False (cs --setup --inline) should downgrade to inline"
527+
)
528+
529+
511530
def test_render_payload_signal_alarm_aborts_slow_render(monkeypatch):
512531
"""Codex-flagged gap: the signal.alarm timeout in _render_payload was
513532
never exercised by tests. Mock core.main with time.sleep > timeout and

tests/test_setup.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ def test_creates_statusline_when_missing(isolated):
5454
assert "Added" in msg
5555
data = json.loads(settings.read_text(encoding="utf-8"))
5656
assert data["statusLine"]["type"] == "command"
57-
assert Path(data["statusLine"]["command"]).name in setup_mod.OUR_COMMAND_NAMES
57+
# Since 3.6.0 a fresh install defaults to daemon mode (`<cs> render`),
58+
# so the command is split into the binary path + " render".
59+
cmd_path = data["statusLine"]["command"].split()[0]
60+
assert Path(cmd_path).name in setup_mod.OUR_COMMAND_NAMES
61+
assert data["statusLine"]["command"].endswith(" render")
5862

5963

6064
def test_idempotent_when_already_configured(isolated):

0 commit comments

Comments
 (0)