On a fresh session Claude Code emits used_percentage=0 before any user
input, while current_usage already holds the real initial-context tokens
(system prompt, tools, memory files — ~9% on a 200k model). The HUD
was treating 0 as a valid native value and skipping the token-based
fallback, so the context bar stayed at 0% even though /context showed 9%.
Treat used_percentage=0 the same as null: fall through to the manual
calculation from current_usage. When tokens are present the bar now
reflects the true initial usage; when both are zero it still shows 0%.
Fixes#417
Adjust two test expectations to account for the trailing spaces
that paddedLabel() adds when aligning shorter labels (Usage, Weekly)
to the width of the longest label (Context = 7 chars).
When Context, Usage, and Weekly lines wrap to separate rows on narrow
terminals, their progress bars now align vertically. A shared
paddedLabel() helper pads each label with spaces to the maximum visual
width across the three labels, accounting for CJK double-width chars.
- Add src/render/lines/label-align.ts with paddedLabel() utility
- Update identity.ts and usage.ts to use paddedLabel()
- Add tests for English and Chinese label alignment
When the Agent/Task tool is called without an explicit subagent_type
parameter, Claude Code's runtime defaults to the general-purpose agent
(per the Agent tool spec). The HUD only sees the transcript input and
previously fell back to the literal string 'unknown', which made
perfectly normal Agent launches look broken or untracked in the HUD's
agents line.
Flip the fallback to 'general-purpose' so the displayed label matches
the tool's actual runtime behavior.
Before: `✓ unknown: Build phase 2 agents`
After: `✓ general-purpose: Build phase 2 agents`
getProviderLabel was string-matching `anthropic.claude-` in model IDs,
which false-positived on cross-region IDs like `us.anthropic.claude-sonnet-4-6`.
Now checks `CLAUDE_CODE_USE_BEDROCK=1` env var instead.
Fixes#466
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When running as a CC statusLine subprocess, stdout/stderr are piped and
COLUMNS may not be set. getTerminalWidth() falls back to
UNKNOWN_TERMINAL_WIDTH (40), which is not a real measurement.
Previously, this fallback value was treated as real terminal width in two
places:
- canCombine in renderExpanded(): context + usage lines were always split
because combined width exceeded 40 columns
- wrapLineToWidth in render(): all output lines were forcefully wrapped
at 40 characters, breaking the layout
Fix: introduce a widthIsReal guard that checks whether the resolved width
is the UNKNOWN_TERMINAL_WIDTH sentinel. When width is unknown:
- canCombine defaults to true (combine lines optimistically)
- wrapLineToWidth is skipped (wrapWidth=0 → no-op)
The host process (Claude Code) handles final display layout, so wrapping
at an arbitrary fallback value is harmful rather than helpful.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the hardcoded Anthropic pricing table and estimateSessionCost()
logic with a direct read of stdin.cost.total_cost_usd — the native field
Claude Code provides after each API response.
- Add cost.* fields to StdinData type
- Extract costUsd in index.ts and pass through RenderContext
- Simplify renderCostEstimate to read ctx.costUsd directly
- Rename i18n key label.estimatedCost → label.cost
- Update label to "Cost ($USD)" / "耗油量(美元)" (zh)
- Remove dead estimateSessionCost, pricing table, and related helpers
- Update tests to use costUsd; add index.test.js coverage for the field
- Update README to reflect native field behavior
Closes#380
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When TodoWrite is called, the taskIdToIndex map was cleared and
rebuilt from scratch. However, TodoItem objects only contain content
and status fields — no taskId. This meant all taskId mappings from
prior TaskCreate calls were permanently lost, causing subsequent
TaskUpdate operations to silently fail.
Fix: Before clearing the map, build a reverse lookup of content →
taskIds from the existing state. After replacing latestTodos with the
new TodoWrite items, re-register taskId mappings for items whose
content matches a previously-known task. Stale mappings (for content
no longer present) are naturally dropped.
Adds two tests:
- TaskCreate → TodoWrite → TaskUpdate flow preserves taskId
- TodoWrite-only regression test (no TaskCreate involved)
Cover both rendering paths (expanded + compact) to verify that:
- modelFormat compact/short correctly strips context suffix and prefix
- modelOverride completely replaces the model name in the badge
- modelOverride takes precedence over modelFormat
- loadConfig structural test validates new display fields
Users can now set a fully custom model name in their config:
{ "display": { "modelOverride": "zane's intelligent opus 4.6" } }
When set, the override completely replaces the auto-detected model
name (while preserving the provider qualifier like "| Bedrock").
Follows the same pattern as customLine: string type, max 80 chars,
empty string means disabled (falls through to modelFormat).
Claude Code may include the context window size in display_name
(e.g. "Opus 4.6 (1M context)"), but the HUD already shows context
usage via the context bar — making the parenthetical redundant.
This adds stripContextSuffix() which removes any parenthetical
containing the word "context" from the display name. It handles
common variants like "(1M context)", "(200k context)",
"(with 1M context)", and "(extended context window)" while
preserving non-context parentheticals like "(beta)" or "(preview)".
Before: [Opus 4.6 (1M context) | Bedrock]
After: [Opus 4.6 | Bedrock]
- Fix detectLanguage() to follow POSIX priority: LC_ALL > LC_MESSAGES > LANG
- Fix Chinese format.resetsIn from broken wrap-around grammar to prefix form
- Fix loadConfig() to call mergeConfig({}) when no config file exists,
ensuring detectLanguage() runs for auto-detection
- Add setLanguage('en') guard to render.test.js for locale-independent tests
- Add dedicated i18n test suite (tests/i18n.test.js) covering t(),
detectLanguage(), and mergeConfig language handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add i18n infrastructure with automatic language detection from system
locale (LANG/LC_ALL/LC_MESSAGES) and manual override via config.json.
Translates all user-facing HUD labels, status messages, and format
strings. English remains the default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When both 5h and 7d usage windows are displayed, the 7d gauge had no
label prefix. This adds a dim-styled "Weekly" label (matching the
Context/Usage label style) and removes colons from usage window labels
for consistency with other HUD labels.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
countConfigs() performs ~15 file I/O operations on every invocation.
Since the statusLine plugin spawns a new process ~every 300ms, this means
~50 filesystem ops/sec per session. With multiple concurrent sessions,
this scales to ~1500 ops/sec for config data that rarely changes.
Add a sentinel-based mtime cache (following the pattern established in
transcript.ts) that stats ~4-10 sentinel paths and serves cached results
when no config files have changed. Steady-state cost drops from ~15
syscalls to ~4-10 stat calls + 1 cache read.
Cache files are stored under ~/.claude/plugins/claude-hud/config-cache/
with SHA-256 hashed keys for cwd+configDir isolation. Cache misses
trigger a fresh recompute and cache write.
Known limitation: nested rules/ subdirectory changes are not monitored
(only the top-level rules/ directory mtime is checked).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>