Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ show_cost=false # Hide session cost
show_effort=true # (default) Show effort like Opus 4.6·high
show_effort=false # Hide effort level

# Suppress the one-line "statusLine is not wired" startup hint (see below)
suppress_setup_hint=false # (default) hint shown while statusLine is missing from settings.json
suppress_setup_hint=true # never print the hint; same as CONTEXT_STATS_SUPPRESS_SETUP_HINT=1

# Model Intelligence (MI) score display
show_mi=false # (default) MI score hidden
show_mi=true # Enable MI display in status line and summary
Expand Down Expand Up @@ -312,6 +316,39 @@ color_cyan=bright_cyan # Git change count

Unrecognized color values are ignored with a warning to stderr. Omitted slots use defaults.

## Setup Hint

`pip install context-stats` installs the commands but cannot wire `statusLine`
into Claude Code's `~/.claude/settings.json` for you — that activation step
lives in the README, and `context-stats doctor` (added in #187) diagnoses it.
Because the CLI is the one context-stats process guaranteed to run while the
status line is unwired, every `context-stats` invocation volunteers a one-line
hint on stderr until the wiring exists (see `docs/troubleshooting.md`):

```bash
! statusLine is not wired into ~/.claude/settings.json — the status line will never run. Fix: context-stats doctor --fix
```

The hint goes to stderr only, never to stdout, and never changes the exit
code. It is silent when the wiring exists, and also when `settings.json` is
missing, unreadable, or malformed (doctor diagnoses those on their own
terms). Once you run `context-stats doctor --fix` the hint disappears.

To suppress it without wiring the status line, either set the config key:

```bash
suppress_setup_hint=true # in ~/.claude/statusline.conf
```

or export the environment variable:

```bash
export CONTEXT_STATS_SUPPRESS_SETUP_HINT=1
```

Either one suppresses the hint (both are honored; the env var needs no
config file). Default is `false` / unset — the hint is shown when unwired.

## Config File Format

The config file uses simple `key=value` syntax:
Expand Down
32 changes: 32 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,38 @@ context-stats doctor --fix --force

It exits non-zero when any check fails. Restart Claude Code after a `--fix`.

## The "statusLine is not wired" startup hint

Because the CLI is the one context-stats process guaranteed to run while the
status line is unwired, every `context-stats` invocation prints a one-line
hint on stderr until `statusLine` is configured:

```
! statusLine is not wired into ~/.claude/settings.json — the status line will never run. Fix: context-stats doctor --fix
```

The hint is stderr-only and never changes the command's output or exit code.
It appears once `settings.json` exists and parses but has no effective
`statusLine` block; it stays silent when the file is missing, unreadable, or
malformed (doctor diagnoses those on its own terms). Running
`context-stats doctor --fix` wires the status line and the hint disappears.

To suppress the hint without wiring the status line:

```bash
# in ~/.claude/statusline.conf
suppress_setup_hint=true
```

or with the environment variable (no config file needed):

```bash
export CONTEXT_STATS_SUPPRESS_SETUP_HINT=1
```

Either one suppresses it; see [`docs/configuration.md`](configuration.md) for
the full key reference.

## Common Issues

### Status line not appearing
Expand Down
8 changes: 8 additions & 0 deletions examples/statusline.conf
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ show_effort=true
# false = icon hidden
show_pacman=true

# Suppress the one-line stderr hint the context-stats CLI prints while the
# statusLine wiring is missing from ~/.claude/settings.json. The hint lives
# outside the renderer, so this key only affects the context-stats command;
# the matching CONTEXT_STATS_SUPPRESS_SETUP_HINT env var does the same.
# false = hint shown when unwired (default)
# true = hint never shown
suppress_setup_hint=false


# ─── Model Intelligence (MI) ────────────────────────────────────────────────
#
Expand Down
2 changes: 2 additions & 0 deletions scripts/statusline.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ def _migrate_legacy_state_files(state_dir, old_state_dir):
"show_cost",
"show_effort",
"show_pacman",
"suppress_setup_hint",
}
)

Expand Down Expand Up @@ -487,6 +488,7 @@ def read_config():
"show_cost": True,
"show_effort": True,
"show_pacman": True,
"suppress_setup_hint": False,
"colors": {},
"zone_config": {},
"compaction_drop_threshold": COMPACTION_DROP_THRESHOLD,
Expand Down
72 changes: 72 additions & 0 deletions src/claude_statusline/cli/context_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from __future__ import annotations

import argparse
import os
import signal
import sys
import time
Expand Down Expand Up @@ -186,6 +187,73 @@ def show_help() -> None:
# Known action names — used to distinguish actions from session IDs in argv
_KNOWN_ACTIONS = {"graph", "export", "explain", "cache-warm", "report", "sessions", "doctor"}

#: One-line stderr hint volunteering that the status line is installed but
#: unwired (issue #188). Emitted by main() until the statusLine wiring exists
#: or is suppressed (config key / env var below).
_SETUP_HINT = (
"! statusLine is not wired into ~/.claude/settings.json — "
"the status line will never run. Fix: context-stats doctor --fix"
)

#: Env var that suppresses the setup hint (default: unset = hint shown).
_SUPPRESS_SETUP_HINT_ENV = "CONTEXT_STATS_SUPPRESS_SETUP_HINT"


def _setup_hint_suppressed() -> bool:
"""True when the user opted out via the conf key or the env var.

Read-only by design: a missing conf file means "not suppressed" without
ever materializing it (Config.load() would otherwise auto-create
~/.claude/statusline.conf as a new side effect on commands that never
wrote it — issue #188 review). The path comes from the doctor's shared
helper so patched HOME stays honored.
"""
if os.environ.get(_SUPPRESS_SETUP_HINT_ENV, "").lower() in ("1", "true"):
return True
try:
from claude_statusline.cli.doctor import config_path

if not config_path().exists():
return False
return Config.load().suppress_setup_hint
except Exception:
return False


def _maybe_warn_setup_hint(args: argparse.Namespace) -> None:
"""Volunteer that the status line is installed-but-unwired, if it is.

One stderr line; never raises, never changes the exit code, never writes
to stdout. Silent when the wiring exists, when settings.json is missing,
unreadable, or malformed (doctor diagnoses those on its own terms), when
suppressed via the ``suppress_setup_hint`` config key or the
``CONTEXT_STATS_SUPPRESS_SETUP_HINT`` env var, and on help requests
(subcommand ``-h``/``--help`` lands in ``remaining`` after parse_args;
top-level help already exits inside parse_args).
"""
try:
from claude_statusline.cli.doctor import (
_effective_statusline,
_read_settings,
settings_path,
)

if any(a in ("-h", "--help") for a in getattr(args, "remaining", [])):
return
if _setup_hint_suppressed():
return
if _effective_statusline() is not None:
return
path = settings_path()
if not path.exists():
return
_, error = _read_settings(path)
if error is not None:
return
except Exception:
return
sys.stderr.write(_SETUP_HINT + "\n")


def _normalize_argv(argv: list[str]) -> tuple[str, str | None, list[str]]:
"""Determine action, session_id, and remaining args from raw argv.
Expand Down Expand Up @@ -900,6 +968,10 @@ def main() -> None:

args = parse_args()

# One-line stderr hint when the status line is installed but unwired
# (issue #188): never raises, never changes the exit code or stdout.
_maybe_warn_setup_hint(args)

if args.action == "explain":
import json

Expand Down
34 changes: 27 additions & 7 deletions src/claude_statusline/cli/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,31 @@ def _status_line_overrides() -> tuple[list[tuple[Path, dict]], list[str]]:
return overrides, warnings


def _effective_statusline() -> tuple[Path, dict] | None:
"""Resolve the ``statusLine`` block that would actually run, or None.

The single source of truth for "is the status line wired?", shared by
:func:`check_settings` and the CLI's startup hint. The user settings
file's ``statusLine`` wins unless a higher-precedence project file
overrides it. Pure file reads — no subprocess, never raises: a missing,
unreadable, or malformed user settings file yields None (doctor reports
those distinctly; the startup hint stays silent and points at doctor).
"""
path = settings_path()
settings, error = _read_settings(path)
if error is not None:
return None
assert settings is not None # narrowed by the error branch above
overrides, _warnings = _status_line_overrides()
block = settings.get("statusLine")
effective: tuple[Path, dict] | None = None
if isinstance(block, dict) and block.get("command"):
effective = (path, block)
if overrides:
effective = overrides[-1] # highest precedence wins
return effective


def _check_status_line_block(report: DoctorReport, block: dict, source: Path) -> None:
"""Validate the ``statusLine`` that actually runs, naming its source file."""
is_user = _same_file(source, settings_path())
Expand Down Expand Up @@ -448,13 +473,7 @@ def check_settings(report: DoctorReport) -> None:
),
)

block = settings.get("statusLine")
effective: tuple[Path, dict] | None = None
if isinstance(block, dict) and block.get("command"):
effective = (path, block)
if overrides:
effective = overrides[-1] # highest precedence wins

effective = _effective_statusline()
if effective is None:
report.add(
"Claude Code settings",
Expand All @@ -471,6 +490,7 @@ def check_settings(report: DoctorReport) -> None:
)
return

assert effective is not None # narrowed by the None return above
_check_status_line_block(report, effective[1], effective[0])


Expand Down
10 changes: 10 additions & 0 deletions src/claude_statusline/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
show_cost=true
show_effort=true
show_pacman=true
suppress_setup_hint=false
"""


Expand Down Expand Up @@ -97,6 +98,12 @@ class Config:
# ExDump/Dead) — an emotional at-a-glance cue alongside the zone text.
# On by default; set show_pacman=false to hide it.
show_pacman: bool = True

# Suppress the one-line stderr hint the context-stats CLI prints while
# the statusLine wiring is missing from ~/.claude/settings.json. The
# renderer never branches on this — only the CLI startup hint reads it.
# Default false (hint shown when unwired).
suppress_setup_hint: bool = False
tps_precision: int = 1 # decimal places for the tok/s value
tps_unit: str = "tok/s" # unit label appended to the value
tps_window: int = 5 # number of recent turns averaged for rolling tok/s
Expand Down Expand Up @@ -205,6 +212,8 @@ def _read_config(self) -> None:
self.show_effort = value_lower != "false"
elif key == "show_pacman":
self.show_pacman = value_lower != "false"
elif key == "suppress_setup_hint":
self.suppress_setup_hint = value_lower != "false"
elif key == "tps_precision":
try:
v = int(raw_value)
Expand Down Expand Up @@ -311,6 +320,7 @@ def to_dict(self) -> dict[str, Any]:
"show_cost": self.show_cost,
"show_effort": self.show_effort,
"show_pacman": self.show_pacman,
"suppress_setup_hint": self.suppress_setup_hint,
"tps_unit": self.tps_unit,
"tps_window": self.tps_window,
"zone_1m_plan_max": self.zone_1m_plan_max,
Expand Down
8 changes: 8 additions & 0 deletions src/claude_statusline/data/statusline.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ show_effort=true
# false = icon hidden
show_pacman=true

# Suppress the one-line stderr hint the context-stats CLI prints while the
# statusLine wiring is missing from ~/.claude/settings.json. The hint lives
# outside the renderer, so this key only affects the context-stats command;
# the matching CONTEXT_STATS_SUPPRESS_SETUP_HINT env var does the same.
# false = hint shown when unwired (default)
# true = hint never shown
suppress_setup_hint=false


# ─── Model Intelligence (MI) ────────────────────────────────────────────────
#
Expand Down
Loading
Loading