* 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>
- Add xhigh to TranscriptThinkingEffort, ThinkingEffortLevel, and
ClaudeSettings.effortLevel unions
- Accept xhigh and xHigh casings from /model stdout and settings.json
- Pass unknown-but-word-shaped effort values through with a trailing "?"
marker (e.g. "super-max?"), so future Claude Code effort levels render
gracefully without code changes
- Display "Thinking: default" when no effort signal is available from
the transcript or Claude settings, replacing the prior silent medium
fallback
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix token overcounting from streaming duplicate JSONL entries
Claude Code writes multiple JSONL entries per API call during streaming:
intermediate entries have stop_reason: null, and only the final entry has
a string value like "end_turn" or "tool_use". The getTokenMetrics function
was summing all entries, inflating the total by ~2.5x.
Now only counts final entries (those with a truthy stop_reason). Falls
back to counting all entries when no stop_reason data is present, for
backward compatibility with older transcript formats.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(jsonl): dedupe live streaming token metrics
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
Removes trailing parenthetical (e.g., "(1M context)", "(200K context)")
from model display names. "Opus 4.6 (1M context)" becomes "Opus 4.6".
Users who want the context window size displayed can add the existing
context-length widget to their layout.
Fixes#238
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat: add git status, git remote, worktree, and custom symbol widgets
Add 19 new widgets split out from PR #255 per reviewer request:
- Git status widgets (addresses #20): git-status, git-staged,
git-unstaged, git-untracked, git-ahead-behind, git-conflicts, git-sha
- Git remote widgets: git-origin-owner, git-origin-repo,
git-origin-owner-repo, git-upstream-owner, git-upstream-repo,
git-upstream-owner-repo, git-is-fork
- Worktree widgets: worktree-mode, worktree-name, worktree-branch,
worktree-original-branch
- Custom symbol widget
Supporting changes:
- Add git command cache and status functions to git.ts
- Add git-remote utilities for fork detection and URL parsing
- Add customSymbol, hide, getNumericValue to Widget types
- Add worktree field to StatusJSON
- Add gitData field to RenderContext
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct git status detection for unstaged changes and merge conflicts
Fixed three issues in git status parsing:
1. Unstaged pattern was incomplete - only detected M/D, missed merge conflicts
(UU, AU, DU, AA, UA, UD) and other status codes (R, C, T)
2. Added -z flag for NUL-terminated output to properly handle filenames with
special characters
3. Changed trim() to trimEnd() to preserve significant leading spaces in git
porcelain format (e.g., ' M file.txt' for unstaged modifications)
Added comprehensive test coverage with 20 new test cases covering all merge
conflict scenarios, rename/copy/type-change detection, and edge cases.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add conflict detection to git status widget
Added '!' indicator to GitStatus widget to show merge conflicts with priority
ordering: !+*? (conflicts, staged, unstaged, untracked).
Conflicts are shown first as they're blocking - work cannot proceed until
resolved. The priority ordering reflects urgency: blocking issues, intentional
work, unsaved changes, then undecided files.
Added conflict detection for all merge conflict states: DD, AU, UD, UA, DU, AA, UU
Added test coverage for DD (both deleted) case and mixed status with conflicts
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(git-widgets): refine worktree naming and git parsing
Tighten the git widget plumbing and align the new worktree widgets with the Git naming scheme.
- parse nested namespace remotes correctly for both SCP-style and URL-based git remotes
- ignore rename and copy source-path entries when reading porcelain -z status output
- keep the git utility regression coverage aligned with the parser changes
- rename Worktree* widget files and classes to GitWorktree* and update display names
- preserve the existing worktree widget type ids while rewiring exports and manifest entries
* fix(git-widgets): correct conflict rendering and upstream resolution
Improve git widget behavior in preview and runtime rendering.
- treat text-presentation pictographs like the warning symbol as narrow unless explicitly emoji-style
- keep git conflicts visible at zero and return numeric raw values for the conflict count
- add regression coverage for glyph width handling and conflict widget rendering
- resolve git-upstream widgets through the branch tracking remote when no literal upstream remote exists
- cover tracked-upstream fallback behavior in git remote utility tests
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* Add Claude account email widget for ccswitch integration
* feat(widgets): add Claude account email widget
Adds a new ClaudeAccountEmail widget in the Session category that displays
the user's email by reading from git config user.email or falling back to
common environment variables (GIT_AUTHOR_EMAIL, GIT_COMMITTER_EMAIL, EMAIL, USER_EMAIL).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(widgets): read Claude account email from ~/.claude.json
Replaces the git config approach with reading oauthAccount.emailAddress
directly from ~/.claude.json — the actual Claude account credentials file.
Respects CLAUDE_CONFIG_DIR environment variable. No external tools required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: Remove unnecessary package-lock.json as this repo uses bun.lock
* fix(widgets): label claude account email output
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat: add a global minimalist mode that defaults widgets to raw mode
Adds the foundational plumbing for a global minimalist mode toggle:
- RenderContext.minimalist flag (optional boolean)
- Settings.minimalistMode field (default false, Zod-managed)
- Threads the flag from settings into RenderContext at render time in
both piped mode (ccstatusline.ts) and TUI preview (StatusLinePreview.tsx)
Adds an (m) keybind to the Global Overrides TUI menu to toggle minimalist
mode on/off. The setting is persisted immediately via onUpdate, and the
preview reflects the change in real time via the RenderContext flag.
When minimalist mode is active, the renderer forces rawValue: true on all
widgets before calling render(), stripping decorative labels and prefixes
globally. No per-widget changes needed.
* fix: keep strict settings literals for minimalist mode
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat: fzf when adding widgets
Adds fuzzy matching to the widget picker as a fallback when the query
has no exact substring match anywhere.
Characters must appear in order but need not be adjacent; matches are
scored by span, consecutive runs, and word boundaries, and ranked below
all exact matches.
Selection now resets to the top-ranked result on every keystroke instead
of staying pinned to the previously-selected widget type.
Matched characters are highlighted in the picker list: yellow-bold for
unselected rows, green-bold for the selected row.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: prefer widget picker initialisms over incidental matches
Give display-name initialism matches their own ranking path so abbreviation searches like tw, tc, ti, and to select the intended widget instead of description or type fallbacks. Keep picker highlighting aligned with the ranking logic and add regressions for the reported fuzzy-match cases.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat: use native rate_limits field from Claude Code 2.1.80
Claude Code 2.1.80 now sends rate_limits in the statusline JSON input
with five_hour and seven_day windows (used_percentage + resets_at).
When present, usage widgets consume this data directly instead of
fetching from the Anthropic API. Falls back to the API fetch for
older Claude Code versions that don't send rate_limits.
Also updates the example payload to match the real 2.1.80 format.
* fix: revert example payload to use placeholder values
* chore: align test values with example payload
* fix: Fallback to API usage query if any of the rate limit fields are missing
---------
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
* feat: add hyperlink support to GitBranch and GitRootDir widgets
- GitBranch: toggle (l)ink wraps branch name in OSC 8 link to
github.com/owner/repo/tree/<branch>; only activates for GitHub
remotes, falls back to plain text otherwise
- GitRootDir: toggle (l)ink wraps project root name in OSC 8
cursor://file/<path> link to open the directory in Cursor
- Add shared src/utils/hyperlink.ts with renderOsc8Link and
parseGitHubBaseUrl helpers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Improve git and IDE hyperlink handling
- encode GitHub branch refs so reserved characters do not break branch links
- accept ssh:// and credentialed GitHub remotes when building GitHub URLs
- replace the Cursor-only repo root toggle with IDE link modes for VS Code and Cursor
- build IDE file links from encoded paths so Windows, spaces, #, and UNC paths work
- reuse the shared OSC-8 hyperlink renderer from the Link widget
- add focused coverage for hyperlink parsing and the updated git widgets
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>