When the statusline refreshes while another git process is writing
.git/index.lock (e.g. a concurrent `git restore --staged`), commands
like `git diff --shortstat` fail with "Unable to create index.lock"
because diff tries to refresh the stat cache and races on the lock.
`runGitArgs` now prepends `--no-optional-locks` to every git
invocation so read-only commands cannot interfere with the user's
working git operations. The two call sites that already prefixed the
flag manually are simplified.
* feat: add extra usage widgets and fix null rate-limit buckets for pay-as-you-go plans
Add two new widgets for Anthropic pay-as-you-go (extra usage) plans:
- `extra-usage-utilization` — shows overage utilization as a percentage
- `extra-usage-remaining` — shows remaining monthly overage budget in dollars
Fix a Zod schema bug that caused all usage widgets to time out on PAYG plans.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: handle disabled extra usage widgets
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
The TTY-width probe walked ancestor processes to find a controlling
PTY and ran `stty size < /dev/${tty}` to read its dimensions. That
form fails with ENOTTY on Linux when the calling process has no
controlling terminal — which is now the case under Claude Code
>= 2.1.139, whose changelog reads "hooks now run without terminal
access". The statusLine spawn is hardened the same way. Probe falls
back to `tput cols` (= 80), flexMode "full-minus-40" collapses to
40 columns, and the statusline truncates regardless of the real
terminal width.
GNU coreutils `stty -F <path>` and BSD `stty -f <path>` open the
device themselves (with O_NOCTTY semantics) and succeed regardless
of controlling-tty status. Try `-F` then `-f` then the historical
redirect form so we keep working on every stty variant.
Verified: on a Linux+ptyxis spawn under Claude Code 2.1.140 the probe
goes from returning null to returning the real width (159 cols here)
without restarting the session.
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
Use widget-specific requirements to fetch missing usage fields and merge rate_limits data with API results.
Preserve API errors alongside available usage data so widgets with fulfilled fields still render while missing fields surface fetch failures.
Adds a "Integration Example: AIWatch" section to docs/USAGE.md mirroring
the existing ccusage section, plus a Related Projects link in README, per
the placement the maintainer chose in #362.
AIWatch (ai-watch.dev) monitors live status for 30+ AI APIs/apps; the
Custom Command one-liner surfaces degraded providers in the status line
and renders empty when everything's operational.
Co-authored-by: Bentley <bentley@naemomlab.com>
* feat: add voice status widget showing Claude Code voice input state
* Fix voice status workspace config lookup
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat(jj): add jj utility functions and shared hide-no-jj feature
Add jj VCS utility module mirroring the existing git utilities with
workspace detection, command execution, and diff stat parsing. Add
shared jj-no-jj toggle for hiding 'no jj' messages in jj widgets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(jj): add JjChange and JjBookmark widgets
Add two new Jujutsu VCS widgets mirroring the GitBranch pattern:
- JjChange: displays current jj change ID with `jj:` prefix
- JjBookmark: displays current jj bookmark name(s) with `@` prefix,
showing `(none)` when no bookmarks are set
- Add `runJjRaw` utility to distinguish empty output from errors
Both widgets support raw value mode, hide-no-jj configuration,
and include full test coverage (17 tests).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(jj): add JjChanges, JjInsertions, JjDeletions, JjRootDir, and JjDescription widgets
Add five new Jujutsu VCS widgets with full test coverage:
- JjChanges: combined insertions/deletions count (+ins,-del)
- JjInsertions: insertion count from jj diff --stat
- JjDeletions: deletion count from jj diff --stat
- JjRootDir: workspace root directory name extraction
- JjDescription: current change description via jj log
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(jj): add JjWorkspace widget for displaying current workspace name
Add getJjCurrentWorkspace() utility that parses `jj workspace list` output
to extract the current workspace name from the first line. Implement
JjWorkspaceWidget with blue color, W: prefix, raw value support, and
hide-no-jj toggle. Include tests for both the utility function and widget.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(jj): register all jj widgets in manifest and add shared behavior tests
Add all 8 jj widgets (JjChange, JjBookmark, JjChanges, JjInsertions,
JjDeletions, JjRootDir, JjDescription, JjWorkspace) to the widget
exports and manifest registry. Add shared behavior test suite validating
hide-no-jj keybind, metadata toggling, editor display, and Jujutsu
category across all jj widgets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(jj): consolidate runJj/runJjRaw, fix double subprocess and hideNoJj invariant
- Merge runJjRaw into runJj with allowEmpty parameter to eliminate
duplication while preserving semantic distinction between empty
output and command failure
- Fix JjRootDir calling jj workspace root twice per render by
removing redundant isInsideJjWorkspace guard
- Extract getRootDirName to module-level function (matches project
convention of no private helper methods)
- Fix JjDescription ignoring hideNoJj flag when command fails after
workspace check passes
- Fix JjDescription preview string inconsistency ('(no description
set)' vs '(no description)')
- Add tests for allowEmpty behavior and JjDescription hideNoJj
command failure case
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(git): add hideWhenJj toggle to suppress git widgets in jj workspaces
Add a new per-widget toggle that hides git widgets when a jj workspace
is detected, enabling clean colocated repo support. Users can press 'j'
in the TUI items editor to enable this on any git widget.
- Add git-hide-when-jj.ts shared module with metadata flag, keybind,
and editor display helpers
- Update all 6 git widgets (Branch, Changes, Insertions, Deletions,
RootDir, Worktree) with the new toggle
- Compose modifier text from both hideNoGit and hideWhenJj flags
- Chain action handlers via nullish coalescing
- Add shared behavior tests for toggle, keybind, and modifier display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(jj): remove git widget jj hide keybind
* chore(git): restore git modifier helper usage
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
Previously the Context % and Context % (usable) widgets always
rendered the static label "Ctx: " / "Ctx(u): ", so a user looking
at "Ctx: 9.3%" could not tell whether 9.3% was used or remaining
without opening the editor to check the inverse toggle.
Now the label reflects the current state:
- default (used): "Ctx Used: X%" / "Ctx(u) Used: X%"
- inverse (left): "Ctx Left: X%" / "Ctx(u) Left: X%"
The existing (u) keybind "(u)sed/remaining" and modifier text
"(remaining)" already named these states; this just surfaces the
same distinction in the rendered status line output. Tests updated
to match.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat: add time cursor to usage progress bars
The old progress bar showed only how much of the usage limit had been
consumed. The new makeTimerProgressBar adds an optional cursor marker
that shows the elapsed time position within the current usage window.
Users can toggle the cursor on and off with the t keybind when in
progress display mode.
* fix: show time cursor in short usage bars
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat: add compaction-counter widget
Tracks context compaction events per session. When Claude Code compacts
the conversation, used_percentage drops; this widget detects the drop
and displays ↻N.
Detection: any drop in used_percentage > 2 points between consecutive
renders counts as one compaction. The threshold filters rounding noise
on both 200K and 1M context windows. State stored per-session in
~/.cache/ccstatusline/compaction/ as JSON.
Related: #92
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: sanitize session ID in cache path and handle write failures
- Sanitize session ID to prevent path traversal via crafted StatusJSON
- Wrap cache writes in try/catch — failures no longer crash status line
- Add persistence tests: round-trip, path traversal regression, write failure
- Clarify threshold doc (drop must exceed 2 points)
Co-Authored-By: Claude <noreply@anthropic.com>
* style: align compaction files with project conventions
- Restore EOF newlines (eol-last flipped 'never' → 'always' in 9f779db)
- Use bare module names ('fs', 'os', 'path') with namespace imports
- Use safeParse for cache file parsing
- Refactor test to use a render(options) helper
- Strip @param/@returns from detectCompaction JSDoc
- Collapse trivial single-property object literals to one-line form
- Split guard clause across two lines in CompactionCounter.render
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(widget): harden compaction-counter against bad input and symlinks
Correctness:
- Pass raw used_percentage to detectCompaction; Math.round was missing
real drops between rounded points (40.4 → 37.6 is a 2.8 drop but
rounds to 40 → 38 = 2, failing the > 2 threshold).
- Sentinel prevCtxPct=-1 for fresh state so sessions starting at 0%
are detected correctly (prior prevCtxPct>0 guard excluded that case).
- Guard detectCompaction against NaN/Infinity/negative input; NaN
silently poisoned prevCtxPct and disabled detection forever.
- Always load the persisted count so the widget keeps showing the
historical value when a status update has no used_percentage; only
run detection+save when a current percentage is present.
- Skip the save when state is unchanged, avoiding a redundant fsync
per refreshInterval tick.
Security:
- Atomic save via temp file + rename defeats a planted symlink at the
cache path that would otherwise be written through. Orphan temp
files are cleaned up if rename fails after write succeeded.
- Open with O_NOFOLLOW + fstat on the open fd: rejects symlinks and
non-regular files on read, and closes the TOCTOU window that would
otherwise let the path be swapped between stat and read.
- 4 KiB cache file size cap on read prevents DoS via oversized file.
- Hash session IDs that are empty or contain disallowed characters so
distinct sessions can't collide in the same cache file.
Tests added for NaN/Infinity/negative input, non-integer drops, 0%
session start, sequential compactions, threshold 0, corrupted JSON,
oversized cache file, symlink rejection on read, atomic save replacing
a planted symlink, session-ID collision, and preview ignoring live data.
Symlink tests are skipped on Windows where fs.symlinkSync requires
elevated privileges.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(compaction): harden counter detection and display
fix(compaction): derive usage from shared context percentage metrics when raw used_percentage is missing.
fix(compaction): reset persisted baselines when context window sizes change to avoid false compaction counts.
refactor(context): reuse shared percentage metrics in the context percentage widget.
feat(compaction-counter): add format cycling for icon-space, text label, and number-only displays.
feat(compaction-counter): add Nerd Font icon support only for the icon-space format.
feat(compaction-counter): show zero by default and add an opt-in hide-when-zero toggle.
test(compaction): cover window-size changes, derived percentages, display modes, Nerd Font behavior, and zero visibility.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
In layouts with explicit `{type: "separator"}` items, when a widget
between two separators renders empty, both surrounding separators
still emit — producing "A | | B" instead of "A | B". The walkback
in renderStatusLine that gates each separator was looking for ANY
prior widget with content, not the immediately-prior one. That's
correct for hiding a leading separator (nothing before it) but
wrong when the immediate-prior widget is empty in the middle of a
line.
Fix: stop the walkback at the first non-separator widget and emit
only when that widget actually rendered content. The leading-edge
case still hides correctly (no prior widget → no orphan), and a
merge:'no-padding' chain across a collapsed separator now reaches
the next visible widget as expected. Affects any hide-capable
widget (git-changes with hideNoGit, etc.). The auto-separator
(defaultSeparator) path already filters empty widgets out of its
element chain and isn't touched.
New tests cover:
- empty widget between two separators (the canonical case)
- consecutive empty widgets between content widgets
- leading empty widget (regression — already worked, locks behavior)
- merge:'no-padding' interaction across a collapsed separator
- defaultSeparator path independence (the fix doesn't couple to it)
- all-content happy path (regression guard)
Co-authored-by: Claude <noreply@anthropic.com>
* feat: support local/IANA timezone in reset timer date mode
Closes#337.
Adds an optional 'timezone' metadata key to BlockResetTimer and
WeeklyResetTimer. When provided, the reset timestamp (already exposed
in date mode via #220) is rendered through Intl.DateTimeFormat in the
specified timezone instead of UTC.
## Behavior
- 'timezone' unset or 'UTC': existing UTC behavior is preserved
- 'timezone' = 'local': uses the system timezone
- 'timezone' = an IANA name (e.g. 'Asia/Tokyo'): explicit override
- Invalid timezone names fall back to UTC
## Output examples
| timezone | output (compact=false) | output (compact=true) |
|--------------|-------------------------------|-----------------------|
| (none) / UTC | '2026-03-12 08:30 UTC' | '03-12 08:30Z' |
| Asia/Tokyo | '2026-03-12 17:30 GMT+9' | '03-12 17:30' |
| local | '2026-03-12 17:30 <tz>' | '03-12 17:30' |
## Implementation
- 'formatUsageResetAt' grows an optional 'timezone' parameter
- When set to a non-UTC value, uses 'Intl.DateTimeFormat' with
'timeZone' option to render parts, plus 'timeZoneName: short' for
the trailing label
- Widget render passes 'metadata.timezone' through via a new
'getUsageTimezone(item)' helper
- Falls back to UTC on Intl errors so misconfiguration never blanks
the widget
## Tests
- new cases in 'src/utils/__tests__/usage.test.ts' covering specific
IANA TZ, local TZ, UTC backwards compat, and invalid TZ fallback
* feat: support 'locale' metadata for reset timer date mode
Adds an optional 'locale' metadata key alongside 'timezone' so users
can pick a locale that returns a friendlier short timezone label.
Background: 'Intl.DateTimeFormat' with the default 'en-CA' locale
returns 'GMT+9' for Asia/Tokyo. Switching to 'ja-JP' yields 'JST'.
Same for many other zones — 'en-US' tends to give EST/EDT etc.
## Behavior
- 'locale' omitted: uses 'en-CA' (current behavior, no change)
- 'locale' = an IANA / BCP 47 tag: passed to 'Intl.DateTimeFormat'
- If the supplied locale produces no usable output (rare), falls
back to the default 'en-CA' formatter, then UTC
The numeric date parts come from 'formatToParts', so even with
'ja-JP' the output is still '2026-04-27 18:40 JST' — only the
trailing zone label changes.
## Sample config
{
"type": "weekly-reset-timer",
"metadata": {
"absolute": "true",
"timezone": "Asia/Tokyo",
"locale": "ja-JP"
}
}
## Tests
Added cases in src/utils/__tests__/usage.test.ts for ja-JP yielding
JST, en-CA yielding GMT+9 for the same instant, compact mode behavior,
and lenient locale handling.
* feat(widgets): add reset timer timezone controls
Add reusable timezone picker support for block and weekly reset timers.
Add a timestamp-mode 12/24hr toggle that defaults to 24hr.
* feat(widgets): add reset timer locale controls
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
Adds a new "context-window" widget that displays the total context
window size for the current model (e.g. 1.0M for Opus 1M, 200k for
Sonnet/Haiku). Resolves the size from runtime context_window_size,
falling back to per-model config when absent — same logic as
ContextBarWidget. Useful for users who want "used/total (pct)"
output without the progress bar from context-bar.
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat: add date mode for block and weekly reset timers
* fix(tui): avoid reset timer timestamp keybind conflict
Use t for timestamp mode so d remains the editor delete shortcut, and hide the weekly hours-only toggle while timestamp mode is active.
---------
Co-authored-by: Fan Bot <clawbot@Fandexuniji.local>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
Add a compact progress bar (▓░, 10 chars, no brackets) as a new display
mode for all percentage-based widgets: SessionUsage, WeeklyUsage,
ContextPercentage, ContextPercentageUsable, and ContextBar. The mode
cycles via the existing 'p' keybind alongside the original progress bar
variants.
Renamed bar size labels for consistency: long bar (32 chars),
medium bar (16 chars), short bar (10 chars), short bar only (10 chars
without percentage text).
Added 11 unit tests covering makeSliderBar rendering, display mode
cycling with and without slider, and Context % slider render output.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* fix(widget): claude-account-email respects CLAUDE_CONFIG_DIR
Closes#317.
The widget computed the .claude.json path as ${configDir}/../.claude.json,
which only happens to work when configDir is the default ~/.claude (going
up one level lands at $HOME). When CLAUDE_CONFIG_DIR points elsewhere
(e.g. ~/.claude-work), the same .. heuristic still lands at $HOME, so
all profiles read the same .claude.json and display the same account
email — the symptom reported in the issue.
Fix: branch the path resolution on whether CLAUDE_CONFIG_DIR is set.
Claude Code stores .claude.json inside CLAUDE_CONFIG_DIR when that env
var is set, otherwise it lives at ~/.claude.json.
Also tightened the surrounding code while in the area:
- Drop redundant existsSync check (readFileSync ENOENT is already caught)
- Drop redundant path.resolve on already-absolute paths
- Tighten email field check from truthy (!email) to typeof+length, so a
non-string oauthAccount.emailAddress can't render as Account: 12345
New tests cover:
- $CLAUDE_CONFIG_DIR/.claude.json is read when env var is set (regression)
- $HOME/.claude.json is NOT silently read when CLAUDE_CONFIG_DIR points
to a dir without one (no profile leak)
- Non-string emailAddress returns null (typeof guard)
* chore: Centralize implementation of getClaudeJsonPath similar to getClaudeSettingsPath and getClaudeConfigDir
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
Pressing k on a selected widget inserts a copy just after it and moves
selection to the clone. In Powerline mode, the clone gets a fresh
background color to avoid adjacent duplicate bands.
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat(git-branch): support GitLab and self-hosted hosts for branch link URLs
Replace the GitHub-only parseGitHubBaseUrl helper with a forge-agnostic
buildBranchWebUrl built on the existing parseRemoteUrl + RemoteInfo
plumbing in git-remote.ts. GitBranch links now work for GitHub, GitLab,
and compatible self-hosted remotes that expose the standard
host/owner/repo path.
Uses a single /tree/<branch> suffix because GitLab redirects it to its
canonical /-/tree/<branch> form, so one format covers both forges.
parseGitHubBaseUrl and its helper were the only GitHub-specific URL
builders in hyperlink.ts; remove them and their tests now that GitBranch
is the only caller and has been migrated.
Also rename the metadata key linkToGitHub to linkToRepo to match the
naming used by GitOriginOwnerRepo / GitOriginRepo / GitUpstreamOwner.
Legacy linkToGitHub is preserved as a read-only fallback: toggling the
modifier strips both keys and writes only linkToRepo, so users who
interact with the feature get their settings quietly upgraded. Explicit
linkToRepo:false wins over legacy linkToGitHub:true.
* feat(git-pr): support GitLab merge requests via glab
Replace the GitHub-only gh-pr-cache with a forge-aware git-review-cache.
The widget now renders pull requests for GitHub (via `gh`) and merge
requests for GitLab (via `glab`), picking the CLI per-repo based on the
origin remote host:
- host contains `github` → `gh`
- host contains `gitlab` → `glab`
- unknown/self-hosted host → probe each CLI with
`gh/glab auth status --hostname <h>` and use whichever is authenticated
against that host; if neither is, stay quiet rather than fire wasted
queries
- no origin remote → try both and let the CLI resolve the repo itself
For forks where the CLI would default-resolve to the parent repo, the
fetch falls back to `--repo <origin-url>` after an empty first query so
the user's fork PR/MR is still found.
GitPr.ts now records the provider on the cache entry and renders "MR #N"
for glab and "PR #N" for gh; raw mode stays `#N` for both. Widget display
name becomes "Git PR/MR".
Also rename the widget `type` from `git-pr` to `git-review` to match the
internal `git-review-cache` name. Legacy `git-pr` configs keep rendering
via a resolver in widgets.ts, and loadSettings silently rewrites them to
`git-review` in-memory so the canonical name lands on the next save —
same pattern as the linkToGitHub → linkToRepo rewrite in the previous
commit.
* docs(usage): describe GitHub and GitLab behavior for Git widgets
Update the Git-section intro to mention both forges and the CLI-selection
rules used for self-hosted hosts, and note on the Git PR keybind line
that the widget renders "MR" for GitLab origins.
* chore(gitlab-support): trim verbose comments to match repo style
Most of this repo's TS files carry zero or a handful of one-line comments;
the verbose rationale blocks added during the GitLab work (multi-paragraph
JSDocs on buildBranchWebUrl, getProviderCandidates, fetchFromProvider, etc.
and long inline explainers in the tests) stood out. Trim them to short
one-liners where the why is genuinely non-obvious, and drop the rest.
* fix(git-review): preserve self-hosted remote ports
* fix(tui): clamp status preview to terminal width
---------
Co-authored-by: jmecham <jmecham@foundrydigital.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
Enable circular cursor movement across all TUI menus and lists —
pressing up at the first item wraps to the last, and pressing down
at the last wraps to the first. Also applies to move/reorder modes,
allowing items to be moved cyclically through the list boundaries.
Added 10 unit tests covering wrap-around boundaries for normal
navigation, move mode, picker categories, widgets, and top-level search.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add refreshInterval configuration for Claude Code status line
Add a new "Configure Status Line" menu option (visible when ccstatusline
is installed) that lets users set the Claude Code statusLine
refreshInterval. The setting is written directly to Claude Code's
settings.json.
- Add refreshInterval to ClaudeSettings statusLine interface
- Add getRefreshInterval/setRefreshInterval utility functions
- Add Claude Code version detection (claude --version) with 5s timeout
- Gate refreshInterval behind Claude Code >=2.1.97 version check
- Default to 10s on fresh install, preserve existing value on re-install
- Show disabled state with version requirement message for older versions
- Add RefreshIntervalMenu TUI component with inline numeric input
- Add isKnownCommand path-boundary matching for local dev commands
- Add comprehensive tests for version detection, install flow, and validation
* fix: correct Claude status line state and local install detection
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>