Compare commits

..

25 Commits

Author SHA1 Message Date
Matthew Breedlove e31c7908cd chore: Version bump and README update
CI / Lint & Type Check (push) Waiting to run
CI / Test (push) Waiting to run
CI / Build (push) Blocked by required conditions
2026-03-15 02:42:43 -04:00
Ereli d916afc2ec feat: add hyperlink support to GitBranch and GitRootDir widgets (#224)
* 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>
2026-03-15 01:44:56 -04:00
Matthew Breedlove e31a78add1 chore: Add vim widget to README
CI / Lint & Type Check (push) Waiting to run
CI / Test (push) Waiting to run
CI / Build (push) Blocked by required conditions
2026-03-15 01:22:57 -04:00
Matthew Breedlove 687de09eb4 chore: Version bump and README update 2026-03-15 01:05:56 -04:00
Jiabao Ji 823c45f054 feat: add vim-mode widget (#237)
* feat: add vim-mode widget

Adds a new status line widget that displays the current vim mode when
Claude Code's vim mode is enabled.

Claude Code includes a `vim` field in the status hook JSON when vim mode
is active. The widget reads this field and renders a compact indicator.
When vim mode is not enabled, the widget returns null and hides itself.

Five display formats are supported via metadata.format:
- icon-dash-letter (default): -N / -I
- icon-letter:  N /  I
- icon: icon only
- letter: N / I
- word: NORMAL / INSERT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix vim mode defaults and editor toggles

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
2026-03-15 00:59:02 -04:00
Matthew Breedlove 5b49868de7 Fix macOS usage token keychain fallback
Closes #236

Closes #218
2026-03-15 00:31:02 -04:00
Jaakko d1e35aa832 fix(timer): show days in formatUsageDuration for durations >= 24h (#228)
* fix(timer): show days in formatUsageDuration for durations >= 24h

Durations of 24+ hours now display a days component instead of
raw hours (e.g. "1d 12hr 30m" instead of "36hr 30m"). Applies to
both normal and compact formats. Fixes #210.

* refactor(timer): simplify formatUsageDuration and show 0m for zero duration

Unify compact/normal branches into a single code path differing only
in hour label and separator. Zero duration now displays "0m" instead
of "0hr"/"0h".

* Add weekly reset hours toggle

Default weekly reset timers to day-based formatting while adding an hours-only toggle and keeping preview text aligned with live rendering.

Also move custom keybind visibility back into widgets and clear disabled toggle metadata when those options become unavailable in the editor.

  Supersedes #235
  Closes #235

---------

Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
2026-03-14 23:06:44 -04:00
Matthew Breedlove 1e69ae6898 Fixes for ESLint 10 2026-03-14 22:14:31 -04:00
Matthew Breedlove a9e4e3ea2d Merge remote-tracking branch 'origin/dependabot/bun/eslint/js-10.0.1' 2026-03-14 22:02:37 -04:00
Matthew Breedlove 01766b79e5 Merge remote-tracking branch 'origin/dependabot/bun/eslint-10.0.0' 2026-03-14 22:01:28 -04:00
Matthew Breedlove abb8038c12 Merge branch 'dependabot/bun/ink-gradient-4.0.0' 2026-03-14 21:57:34 -04:00
Matthew Breedlove fcc5734b8d fix: Fix broken ThinkingEffort widget
CI / Build (push) Blocked by required conditions
CI / Lint & Type Check (push) Waiting to run
CI / Test (push) Waiting to run
2026-03-10 02:45:34 -04:00
elliotllliu 49c1750c19 feat: add ThinkingEffort widget to display thinking effort level (#217)
Adds a new "thinking-effort" widget that shows the current
thinking effort level (low, medium, high) in the status bar.

Data source priority:
1. StatusJSON "thinking.effort" field (from Claude Code status hook)
2. "thinkingMode" key in ~/.claude/settings.json (user fallback)

Usage in config:
  { "type": "thinking-effort", "color": "magenta" }

Closes #209

Changes:
- src/types/StatusJSON.ts: add optional "thinking" field
- src/widgets/ThinkingEffort.ts: new widget implementation
- src/widgets/index.ts: export ThinkingEffortWidget
- src/utils/widget-manifest.ts: register "thinking-effort" type
- src/widgets/__tests__/ThinkingEffort.test.ts: 13 unit tests

Co-authored-by: GitHub User <user@example.com>
2026-03-10 02:09:28 -04:00
dependabot[bot] d1634c113c chore(deps-dev): bump ink-gradient from 3.0.0 to 4.0.0
Bumps [ink-gradient](https://github.com/sindresorhus/ink-gradient) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/sindresorhus/ink-gradient/releases)
- [Commits](https://github.com/sindresorhus/ink-gradient/compare/v3.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: ink-gradient
  dependency-version: 4.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:57:02 +00:00
dependabot[bot] 36a6169f4e chore(deps-dev): bump @eslint/js from 9.39.3 to 10.0.1
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.39.3 to 10.0.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:56:55 +00:00
dependabot[bot] 60bf9b8c50 chore(deps-dev): bump eslint from 9.39.3 to 10.0.0
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.3 to 10.0.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.3...v10.0.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:56:49 +00:00
Matthew Breedlove dfa009e7df Handle usage API 429 backoff
CI / Lint & Type Check (push) Waiting to run
CI / Test (push) Waiting to run
CI / Build (push) Blocked by required conditions
Closes #204
2026-03-08 01:11:47 -05:00
Matthew Breedlove db950f3f7a fix: install menu escape navigation 2026-03-06 17:49:58 -05:00
j3r0lin fe9cc15818 feat(timer): add compact time format option for timer widgets (#203)
* feat(timer): add compact time format option for timer widgets

Add an optional compact display mode (s key) to BlockTimer, BlockResetTimer,
and WeeklyResetTimer that renders durations as '5h30m' instead of '5hr 30m'.

- Add `compact` boolean parameter to `formatUsageDuration`
- Add `isUsageCompact` / `toggleUsageCompact` helpers in usage-display.ts
- Add `toggle-compact` action and `s` keybind across all three timer widgets
- Preview text reflects compact format when flag is set
- Editor modifier text shows 'compact' when flag is enabled
- Tests cover compact format output, keybind exposure, and toggle behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix compact timer widget editor state

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
2026-03-06 17:24:52 -05:00
Jack Allen 34fa512778 feat(list): add custom list component (#60)
* feat(list): add custom list component

* feat(list): add sublabel and disabled options for list items

* Refactor TUI menu screens onto shared List component

Replace the remaining menu-style terminal and powerline screens with the shared List component so they share navigation, descriptions, back-row behavior, disabled states, and confirm dialog rendering.

This migrates TerminalOptionsMenu, TerminalWidthMenu, PowerlineSetup, PowerlineThemeSelector, and ConfirmDialog, and adds focused tests for the extracted menu builders and Ink-level regressions.

It also fixes two regressions uncovered during manual testing: PowerlineThemeSelector now memoizes theme data and List selection callbacks so live theme preview no longer triggers a maximum update depth loop, and TerminalWidthMenu now restores focus to the active full-until-compact row after returning from threshold entry instead of resetting to the first item.

---------

Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
2026-03-06 02:19:29 -05:00
Matthew Breedlove e63591492a feat: add usage API HTTPS_PROXY support
CI / Lint & Type Check (push) Waiting to run
CI / Test (push) Waiting to run
CI / Build (push) Blocked by required conditions
2026-03-05 18:07:10 -05:00
Matthew Breedlove 7a0b979b01 test: isolate config test Claude config dir 2026-03-05 14:35:43 -05:00
Matthew Breedlove eaab442794 chore: standardize lint scripts and CI 2026-03-05 14:31:46 -05:00
Bap 78d6dcc28c feat: Add Skills widget with cross-platform hook framework (#201)
CI / Lint & Type Check (push) Waiting to run
CI / Test (push) Waiting to run
CI / Build (push) Blocked by required conditions
* feat: Add Skills widget with cross-platform hook framework

Replaces bash/jq hook with a built-in --hook CLI handler that works
on all platforms. Widgets can declare hooks via getHooks() which are
auto-synced to Claude settings when the statusline config is saved.

* feat: tighten hook lifecycle and upgrade skills widget

Notes:\n- Hook sync now follows saved-state semantics: installing statusline syncs hooks from saved ccstatusline settings on disk, and uninstall always removes all ccstatusline-managed hooks.\n- syncWidgetHooks always strips managed hooks first, including when statusline.command is missing, then persists cleanup.\n- Skills metrics storage path is now ~/.cache/ccstatusline/skills/skills-<sessionId>.jsonl.\n- Skills unique list ordering is now most-recent-first.\n- Skills widget updates: view toggle on (v), hide-when-empty toggle on (h), explicit empty outputs when not hidden, and new list-only (l)imit editor option where 0 means unlimited (default).\n- Added list-limit custom editor flow and list-only keybind visibility in ItemsEditor.\n- Maintained Node 14 compatibility by avoiding Array.prototype.at in skills metrics parsing.\n- Added/updated regression tests for hook sync/install/uninstall behavior, skills metrics path/order, Skills widget rendering/editing, and items-editor custom keybind handling.

* chore: bump version to 2.2.1 and update release notes

---------

Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
2026-03-05 01:35:37 -05:00
Matthew Breedlove 53c8200c5a chore: sync available widgets list in README 2026-03-04 22:08:08 -05:00
77 changed files with 5345 additions and 1270 deletions
+1 -2
View File
@@ -13,8 +13,7 @@ jobs:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun tsc --noEmit
- run: bunx eslint . --config eslint.config.js --max-warnings=999999
- run: bun run lint
test:
name: Test
+1
View File
@@ -21,6 +21,7 @@
"ccstatusline",
"Powerline",
"statusline",
"sublabel",
"Worktree",
"worktrees"
]
+5 -2
View File
@@ -33,7 +33,10 @@ bun test
bun test --watch
# Lint and type check
bun run lint # Runs TypeScript type checking and ESLint with auto-fix
bun run lint # Runs TypeScript type checking and ESLint without modifying files
# Apply ESLint auto-fixes intentionally
bun run lint:fix
```
## Architecture
@@ -136,7 +139,7 @@ Default to using Bun instead of Node.js:
2. `postbuild`: Runs scripts/replace-version.ts to replace `__PACKAGE_VERSION__` placeholder with actual version from package.json
- **ESLint configuration**: Uses flat config format (eslint.config.js) with TypeScript and React plugins
- **Dependencies**: All runtime dependencies are bundled using `--packages=external` for npm package
- **Type checking and linting**: Only run via `bun run lint` command, never using `npx eslint` or `eslint` directly. Never run `tsx`, `bun tsc` or any other variation
- **Type checking and linting**: Run checks via `bun run lint` and use `bun run lint:fix` only when you intentionally want ESLint auto-fixes. Never use `npx eslint`, `eslint`, `tsx`, `bun tsc`, or any other variation directly
- **Lint rules**: Never disable a lint rule via a comment, no matter how benign the lint warning or error may seem
- **Testing**: Uses Vitest (via Bun) with 6 test files and ~40 test cases covering:
- Model context detection and token calculation (src/utils/__tests__/model-context.test.ts)
+48 -32
View File
@@ -46,12 +46,18 @@
## 🆕 Recent Updates
### v2.2.0 - New Token Speed widgets with optional windows
### v2.2.0 - v2.2.6 - Speed, widgets, links, and reliability updates
- **🚀 New Token Speed widgets** - Added three widgets: **Input Speed**, **Output Speed**, and **Total Speed**.
- Each speed widget supports a configurable window of `0-120` seconds in the widget editor (`w` key).
- `0` disables window mode and uses a full-session average speed.
- `1-120` calculates recent speed over the selected rolling window.
- **🧩 New Skills widget controls (v2.2.1)** - Added configurable Skills modes (last/count/list), optional hide-when-empty behavior, and list-size limiting with most-recent-first ordering.
- **🌐 Usage API proxy support (v2.2.2)** - Usage widgets honor the uppercase `HTTPS_PROXY` environment variable for their direct API call to Anthropic.
- **🧠 New Thinking Effort widget (v2.2.4)** - Added a widget that shows the current Claude Code thinking effort level.
- **🍎 Better macOS usage lookup reliability (v2.2.5)** - Improved reliability when loading usage API tokens on macOS.
- **⌨️ New Vim Mode widget (v2.2.5)** - Added a widget that shows the current vim mode, with ASCII and optional Nerd Font icon display.
- **🔗 Git widget link modes (v2.2.6)** - `Git Branch` can render clickable GitHub branch links, and `Git Root Dir` can render clickable IDE links for VS Code and Cursor.
- **🤝 Better subagent-aware speed reporting** - Token speed calculations continue to include referenced subagent activity so displayed speeds better reflect actual concurrent work.
### v2.1.0 - v2.1.10 - Usage widgets, links, new git insertions / deletions widgets, and reliability fixes
@@ -202,6 +208,8 @@ The interactive configuration tool provides a terminal UI where you can:
> $env:CLAUDE_CONFIG_DIR="C:\custom\path\.claude"
> ```
> 🌐 **Usage API proxy:** Usage widgets honor the uppercase `HTTPS_PROXY` environment variable for their direct API call to Anthropic.
### Claude Code settings.json format
When you install from the TUI, ccstatusline writes a `statusLine` command object to your Claude Code settings:
@@ -424,39 +432,44 @@ bun run example
### 📊 Available Widgets
- **Model Name** - Shows the current Claude model (e.g., "Claude 3.5 Sonnet")
- **Git Branch** - Displays current git branch name
- **Git Changes** - Shows uncommitted insertions/deletions (e.g., "+42,-10")
- **Git Insertions** - Shows uncommitted insertions only (e.g., "+42")
- **Git Deletions** - Shows uncommitted deletions only (e.g., "-10")
- **Model** - Displays the Claude model name (e.g., "Claude 3.5 Sonnet")
- **Output Style** - Shows the current Claude Code output style
- **Git Branch** - Shows the current git branch name
- **Git Changes** - Shows git changes count (`+insertions`, `-deletions`)
- **Git Insertions** - Shows git insertions count
- **Git Deletions** - Shows git deletions count
- **Git Root Dir** - Shows the git repository root directory name
- **Git Worktree** - Shows the name of the current git worktree
- **Session Clock** - Shows elapsed time since session start (e.g., "2hr 15m")
- **Session Usage** - Shows current 5-hour/session API usage percentage
- **Weekly Usage** - Shows rolling 7-day API usage percentage
- **Session Cost** - Shows total session cost in USD (e.g., "$1.23")
- **Session Name** - Shows the session name set via `/rename` command in Claude Code
- **Git Worktree** - Shows the current git worktree name
- **Current Working Dir** - Shows current working directory with segment limit, fish-style abbreviation, and optional `~` home abbreviation
- **Tokens Input** - Shows input token count for the current session
- **Tokens Output** - Shows output token count for the current session
- **Tokens Cached** - Shows cached token count for the current session
- **Tokens Total** - Shows total token count (`input + output + cache`) for the current session
- **Input Speed** - Shows session-average input token speed (`tokens/sec`) with optional per-widget window (`0-120` seconds; `0` = full-session average)
- **Output Speed** - Shows session-average output token speed (`tokens/sec`) with optional per-widget window (`0-120` seconds; `0` = full-session average)
- **Total Speed** - Shows session-average total token speed (`tokens/sec`) with optional per-widget window (`0-120` seconds; `0` = full-session average)
- **Context Length** - Shows the current context window size in tokens
- **Context %** - Shows percentage of context window used or remaining
- **Context % (usable)** - Shows percentage of usable context used or remaining (80% of max before auto-compact)
- **Session Clock** - Shows elapsed time since current session started
- **Session Cost** - Shows the total session cost in USD
- **Block Timer** - Shows current 5-hour block elapsed time or progress
- **Terminal Width** - Shows current terminal width in columns
- **Version** - Shows Claude Code CLI version number
- **Custom Text** - Displays user-defined custom text
- **Custom Command** - Executes a custom shell command and displays output (refreshes whenever Claude Code updates the status line)
- **Link** - Displays a clickable terminal hyperlink using OSC 8
- **Claude Session ID** - Shows the current Claude Code session ID from status JSON
- **Block Timer** - Shows time elapsed in current 5-hour block or progress bar
- **Block Reset Timer** - Shows time remaining until the current 5-hour block resets
- **Weekly Reset Timer** - Shows time remaining until the weekly usage window resets
- **Current Working Directory** - Shows current working directory with segment limit, fish-style abbreviation, and optional `~` home abbreviation
- **Version** - Shows Claude Code version
- **Output Style** - Shows the currently set output style in Claude Code
- **Tokens Input** - Shows input tokens used
- **Tokens Output** - Shows output tokens used
- **Tokens Cached** - Shows cached tokens used
- **Tokens Total** - Shows total tokens used
- **Context Length** - Shows current context length in tokens
- **Context Percentage** - Shows percentage of context limit used (dynamic: 1M for model IDs with long-context labels like `[1m]` or `1M context`, 200k otherwise)
- **Context Percentage (usable)** - Shows percentage of usable context (dynamic: 800k for model IDs with long-context labels like `[1m]` or `1M context`, 160k otherwise, accounting for auto-compact at 80%)
- **Session Name** - Shows the session name set via `/rename` in Claude Code
- **Memory Usage** - Shows system memory usage (used/total)
- **Session Usage** - Shows daily/session API usage percentage
- **Weekly Usage** - Shows weekly API usage percentage
- **Block Reset Timer** - Shows time remaining until current 5-hour block reset window
- **Weekly Reset Timer** - Shows time remaining until weekly usage reset
- **Context Bar** - Shows context usage as a progress bar with short/full display modes
- **Terminal Width** - Shows detected terminal width (for debugging)
- **Memory Usage** - Shows system memory usage (used/total, e.g., "Mem: 12.4G/16.0G")
- **Custom Text** - Add your own custom text to the status line
- **Custom Command** - Execute shell commands and display their output (refreshes whenever the statusline is updated by Claude Code)
- **Link** - Add clickable terminal hyperlinks (OSC 8) with configurable URL and display text
- **Separator** - Visual divider between widgets (customizable: |, -, comma, space; available when Powerline mode is off and no default separator is configured)
- **Skills** - Shows skill activity as last used, total count, or unique list (with optional list limit and hide-when-empty toggle)
- **Vim Mode** - Displays current vim editor mode
- **Separator** - Visual divider between widgets (available when Powerline mode is off and no default separator is configured)
- **Flex Separator** - Expands to fill available space (available when Powerline mode is off)
---
@@ -668,9 +681,12 @@ bun run example
# Run tests
bun test
# Run typecheck + eslint autofix
# Run typecheck + eslint checks without modifying files
bun run lint
# Apply ESLint auto-fixes intentionally
bun run lint:fix
# Build for distribution
bun run build
+152 -222
View File
@@ -1,24 +1,26 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"configVersion": 1,
"workspaces": {
"": {
"name": "ccstatusline",
"devDependencies": {
"@eslint/js": "^9.33.0",
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.2.3",
"@types/bun": "latest",
"@types/pluralize": "^0.0.33",
"@types/react": "^19.1.10",
"chalk": "^5.5.0",
"eslint": "^9.33.0",
"eslint": "^10.0.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-import-newlines": "^1.4.0",
"eslint-plugin-import-newlines": "^2.0.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^14.0.0",
"https-proxy-agent": "^7.0.0",
"ink": "6.2.0",
"ink-gradient": "^3.0.0",
"ink-gradient": "^4.0.0",
"ink-select-input": "^6.2.0",
"pluralize": "^8.0.0",
"react": "^19.1.1",
@@ -74,87 +76,33 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
"@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
"@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "^1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="],
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
"@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
"@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="],
"@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="],
"@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.23.0", "", { "dependencies": { "@shikijs/engine-oniguruma": "^3.23.0", "@shikijs/langs": "^3.23.0", "@shikijs/themes": "^3.23.0", "@shikijs/types": "^3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
@@ -172,45 +120,41 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.49.0", "", { "os": "android", "cpu": "arm" }, "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA=="],
"@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.49.0", "", { "os": "android", "cpu": "arm64" }, "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w=="],
"@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.49.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.9", "", { "os": "android", "cpu": "arm64" }, "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.49.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.49.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.49.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm" }, "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.49.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.9", "", { "os": "none", "cpu": "arm64" }, "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.49.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.9", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "x64" }, "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.49.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.49.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.49.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
@@ -226,16 +170,18 @@
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.9.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-FqqSkvDMYJReydrMhlugc71M76yLLQWNfmGq+SIlLa7N3kHp8Qq8i2PyWrVNAfjOyOIY+xv9XaaYwvVW7vroMA=="],
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/gradient-string": ["@types/gradient-string@1.1.6", "", { "dependencies": { "@types/tinycolor2": "*" } }, "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ=="],
@@ -246,7 +192,7 @@
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/pluralize": ["@types/pluralize@0.0.33", "", {}, "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg=="],
@@ -256,25 +202,25 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
@@ -314,25 +260,27 @@
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
"@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="],
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
"@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="],
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
"@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "^2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="],
"@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="],
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
"@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="],
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
"@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
@@ -358,21 +306,23 @@
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -380,9 +330,7 @@
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001776", "", {}, "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001779", "", {}, "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
@@ -396,10 +344,6 @@
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
@@ -416,7 +360,7 @@
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -424,27 +368,29 @@
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
"electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="],
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="],
"es-iterator-helpers": ["es-iterator-helpers@1.3.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" } }, "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
@@ -456,13 +402,11 @@
"es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="],
"eslint": ["eslint@10.0.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ=="],
"eslint-import-context": ["eslint-import-context@0.1.9", "", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" }, "optionalPeers": ["unrs-resolver"] }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="],
@@ -474,19 +418,19 @@
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
"eslint-plugin-import-newlines": ["eslint-plugin-import-newlines@1.4.1", "", { "peerDependencies": { "eslint": ">=6.0.0 < 10.0.0" }, "bin": { "import-linter": "lib/index.js" } }, "sha512-C9PQEJ4jS5tKoE9k0yY/5j4l1bxkxmVjrWvAFc1EToCnuQuwZGl1kS1JBlqYNF8svp0pnXLlkSdjByYa3JALcg=="],
"eslint-plugin-import-newlines": ["eslint-plugin-import-newlines@2.0.0", "", { "peerDependencies": { "eslint": ">=10.0.0" }, "bin": { "import-linter": "lib/index.js" } }, "sha512-xKcuSkpQvkAHWCvAysqCk8GAD+rabLokiK4rmeJjCB+CQtGn6Ptgs909miphvN51JyZxOJWz4reGMsoHSbjbIg=="],
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
@@ -496,7 +440,7 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@@ -514,7 +458,7 @@
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
@@ -526,9 +470,11 @@
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -536,7 +482,7 @@
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -546,12 +492,10 @@
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"gradient-string": ["gradient-string@2.0.2", "", { "dependencies": { "chalk": "^4.1.2", "tinygradient": "^1.1.5" } }, "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw=="],
"gradient-string": ["gradient-string@3.0.0", "", { "dependencies": { "chalk": "^5.3.0", "tinygradient": "^1.1.5" } }, "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
@@ -566,9 +510,9 @@
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -576,7 +520,7 @@
"ink": ["ink@6.2.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-NQbNokT11cuxlIcCDfBMk1vEwaqc/cjTSqc4R4JugBO4BpWVe2B2A6ElC2koZQ9Vj91z0C40zid/jxOF2hJL9A=="],
"ink-gradient": ["ink-gradient@3.0.0", "", { "dependencies": { "@types/gradient-string": "^1.1.2", "gradient-string": "^2.0.2", "prop-types": "^15.8.1", "strip-ansi": "^7.1.0" }, "peerDependencies": { "ink": ">=4" } }, "sha512-OVyPBovBxE1tFcBhSamb+P1puqDP6pG3xFe2W9NiLgwUZd9RbcjBeR7twLbliUT9navrUstEf1ZcPKKvx71BsQ=="],
"ink-gradient": ["ink-gradient@4.0.0", "", { "dependencies": { "@types/gradient-string": "^1.1.6", "gradient-string": "^3.0.0", "strip-ansi": "^7.1.2" }, "peerDependencies": { "ink": ">=6" } }, "sha512-Yx227CStr4DaXVkRAQPbBufSUTqe4a4FLOPVoypXZyae5h3A5jWyqZpTmAIbm7iiiqNYCkKIFBUPJM6nSICfxA=="],
"ink-select-input": ["ink-select-input@6.2.0", "", { "dependencies": { "figures": "^6.1.0", "to-rotated": "^1.0.0" }, "peerDependencies": { "ink": ">=5.0.0", "react": ">=18.0.0" } }, "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ=="],
@@ -606,7 +550,7 @@
"is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="],
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
@@ -646,8 +590,6 @@
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@@ -664,12 +606,34 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -678,7 +642,7 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -686,7 +650,7 @@
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -694,10 +658,12 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-postinstall": ["napi-postinstall@0.3.3", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow=="],
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -728,8 +694,6 @@
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
@@ -748,7 +712,7 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@@ -770,15 +734,13 @@
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
"rollup": ["rollup@4.49.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.49.0", "@rollup/rollup-android-arm64": "4.49.0", "@rollup/rollup-darwin-arm64": "4.49.0", "@rollup/rollup-darwin-x64": "4.49.0", "@rollup/rollup-freebsd-arm64": "4.49.0", "@rollup/rollup-freebsd-x64": "4.49.0", "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", "@rollup/rollup-linux-arm-musleabihf": "4.49.0", "@rollup/rollup-linux-arm64-gnu": "4.49.0", "@rollup/rollup-linux-arm64-musl": "4.49.0", "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", "@rollup/rollup-linux-ppc64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-musl": "4.49.0", "@rollup/rollup-linux-s390x-gnu": "4.49.0", "@rollup/rollup-linux-x64-gnu": "4.49.0", "@rollup/rollup-linux-x64-musl": "4.49.0", "@rollup/rollup-win32-arm64-msvc": "4.49.0", "@rollup/rollup-win32-ia32-msvc": "4.49.0", "@rollup/rollup-win32-x64-msvc": "4.49.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA=="],
"rolldown": ["rolldown@1.0.0-rc.9", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="],
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
@@ -824,7 +786,7 @@
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
@@ -844,23 +806,19 @@
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="],
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="],
@@ -886,13 +844,13 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
"typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
@@ -900,9 +858,9 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@7.1.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw=="],
"vite": ["vite@8.0.0", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="],
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
"vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -912,7 +870,7 @@
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
@@ -920,13 +878,13 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
@@ -940,47 +898,33 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@typescript-eslint/eslint-plugin/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"eslint/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-import-resolver-node/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"eslint-import-resolver-typescript/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"eslint-import-resolver-node/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-plugin-react-hooks/zod": ["zod@4.0.17", "", {}, "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ=="],
"eslint-plugin-import/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"gradient-string/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"ink-gradient/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"is-bun-module/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
@@ -990,32 +934,18 @@
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"typedoc/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"typedoc/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"gradient-string/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"ink-gradient/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"slice-ansi/is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"eslint-plugin-react/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"typedoc/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"eslint-plugin-react/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"typedoc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
}
}
+9 -6
View File
@@ -1,6 +1,6 @@
{
"name": "ccstatusline",
"version": "2.2.0",
"version": "2.2.6",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
@@ -16,25 +16,28 @@
"postbuild": "bun run scripts/replace-version.ts",
"example": "cat scripts/payload.example.json | bun start",
"prepublishOnly": "bun run build",
"lint": "bun tsc --noEmit && eslint . --config eslint.config.js --max-warnings=999999 --fix",
"lint": "bun tsc --noEmit && eslint . --config eslint.config.js --max-warnings=0",
"lint:fix": "bun tsc --noEmit && eslint . --config eslint.config.js --max-warnings=0 --fix",
"docs": "typedoc",
"docs:clean": "rm -rf docs"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.2.3",
"@types/bun": "latest",
"@types/pluralize": "^0.0.33",
"@types/react": "^19.1.10",
"chalk": "^5.5.0",
"eslint": "^9.33.0",
"eslint": "^10.0.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-import-newlines": "^1.4.0",
"eslint-plugin-import-newlines": "^2.0.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^14.0.0",
"https-proxy-agent": "^7.0.0",
"ink": "6.2.0",
"ink-gradient": "^3.0.0",
"ink-gradient": "^4.0.0",
"ink-select-input": "^6.2.0",
"pluralize": "^8.0.0",
"react": "^19.1.1",
+68
View File
@@ -3,6 +3,7 @@ import chalk from 'chalk';
import { runTUI } from './tui';
import type {
SkillsMetrics,
SpeedMetrics,
TokenMetrics
} from './types';
@@ -27,6 +28,10 @@ import {
renderStatusLine
} from './utils/renderer';
import { advanceGlobalSeparatorIndex } from './utils/separator-index';
import {
getSkillsFilePath,
getSkillsMetrics
} from './utils/skills';
import {
getWidgetSpeedWindowSeconds,
isWidgetSpeedWindowEnabled
@@ -130,6 +135,11 @@ async function renderMultipleLines(data: StatusJSON) {
windowedSpeedMetrics = speedMetricsCollection.windowed;
}
let skillsMetrics: SkillsMetrics | null = null;
if (data.session_id) {
skillsMetrics = getSkillsMetrics(data.session_id);
}
// Create render context
const context: RenderContext = {
data,
@@ -138,6 +148,7 @@ async function renderMultipleLines(data: StatusJSON) {
windowedSpeedMetrics,
usageData,
sessionDuration,
skillsMetrics,
isPreview: false
};
@@ -213,10 +224,67 @@ function parseConfigArg(): string | undefined {
return configPath;
}
interface HookInput {
session_id?: string;
hook_event_name?: string;
tool_name?: string;
tool_input?: { skill?: string };
prompt?: string;
}
async function handleHook(): Promise<void> {
const input = await readStdin();
if (!input) {
console.log('{}');
return;
}
try {
const data = JSON.parse(input) as HookInput;
const sessionId = data.session_id;
if (!sessionId) {
console.log('{}');
return;
}
let skillName = '';
if (data.hook_event_name === 'PreToolUse' && data.tool_name === 'Skill') {
skillName = data.tool_input?.skill ?? '';
} else if (data.hook_event_name === 'UserPromptSubmit') {
const match = /^\/([a-zA-Z0-9_:-]+)/.exec(data.prompt ?? '');
if (match) {
skillName = match[1] ?? '';
}
}
if (!skillName) {
console.log('{}');
return;
}
const filePath = getSkillsFilePath(sessionId);
const fs = await import('fs');
const path = await import('path');
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const entry = JSON.stringify({
timestamp: new Date().toISOString(),
session_id: sessionId,
skill: skillName,
source: data.hook_event_name
});
fs.appendFileSync(filePath, entry + '\n');
} catch { /* ignore parse errors */ }
console.log('{}');
}
async function main() {
// Parse --config before anything else
initConfigPath(parseConfigArg());
// Handle --hook mode (cross-platform hook handler for widgets)
if (process.argv.includes('--hook')) {
await handleHook();
return;
}
// Check if we're in a piped/non-TTY environment first
if (!process.stdin.isTTY) {
await ensureWindowsUtf8CodePage();
+48 -17
View File
@@ -63,15 +63,47 @@ interface FlashMessage {
color: 'green' | 'red';
}
type AppScreen = 'main'
| 'lines'
| 'items'
| 'colorLines'
| 'colors'
| 'terminalWidth'
| 'terminalConfig'
| 'globalOverrides'
| 'confirm'
| 'powerline'
| 'install';
interface ConfirmDialogState {
message: string;
action: () => Promise<void>;
cancelScreen?: Exclude<AppScreen, 'confirm'>;
}
export function getConfirmCancelScreen(confirmDialog: ConfirmDialogState | null): Exclude<AppScreen, 'confirm'> {
return confirmDialog?.cancelScreen ?? 'main';
}
export function clearInstallMenuSelection(menuSelections: Record<string, number>): Record<string, number> {
if (menuSelections.install === undefined) {
return menuSelections;
}
const next = { ...menuSelections };
delete next.install;
return next;
}
export const App: React.FC = () => {
const { exit } = useApp();
const [settings, setSettings] = useState<Settings | null>(null);
const [originalSettings, setOriginalSettings] = useState<Settings | null>(null);
const [hasChanges, setHasChanges] = useState(false);
const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colorLines' | 'colors' | 'terminalWidth' | 'terminalConfig' | 'globalOverrides' | 'confirm' | 'powerline' | 'install'>('main');
const [screen, setScreen] = useState<AppScreen>('main');
const [selectedLine, setSelectedLine] = useState(0);
const [menuSelections, setMenuSelections] = useState<Record<string, number>>({});
const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise<void> } | null>(null);
const [confirmDialog, setConfirmDialog] = useState<ConfirmDialogState | null>(null);
const [isClaudeInstalled, setIsClaudeInstalled] = useState(false);
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
const [powerlineFontStatus, setPowerlineFontStatus] = useState<PowerlineFontStatus>({ installed: false });
@@ -163,6 +195,7 @@ export const App: React.FC = () => {
setConfirmDialog({
message,
cancelScreen: 'install',
action: async () => {
await installStatusLine(useBunx);
setIsClaudeInstalled(true);
@@ -176,13 +209,20 @@ export const App: React.FC = () => {
}, []);
const handleNpxInstall = useCallback(() => {
setMenuSelections(prev => ({ ...prev, install: 0 }));
handleInstallSelection(CCSTATUSLINE_COMMANDS.NPM, 'npx', false);
}, [handleInstallSelection]);
const handleBunxInstall = useCallback(() => {
setMenuSelections(prev => ({ ...prev, install: 1 }));
handleInstallSelection(CCSTATUSLINE_COMMANDS.BUNX, 'bunx', true);
}, [handleInstallSelection]);
const handleInstallMenuCancel = useCallback(() => {
setMenuSelections(clearInstallMenuSelection);
setScreen('main');
}, []);
if (!settings) {
return <Text>Loading settings...</Text>;
}
@@ -308,20 +348,12 @@ export const App: React.FC = () => {
<Box marginTop={1}>
{screen === 'main' && (
<MainMenu
onSelect={(value) => {
onSelect={(value, index) => {
// Only persist menu selection if not exiting
if (value !== 'save' && value !== 'exit') {
const menuMap: Record<string, number> = {
lines: 0,
colors: 1,
powerline: 2,
terminalConfig: 3,
globalOverrides: 4,
install: 5,
starGithub: hasChanges ? 8 : 7
};
setMenuSelections(prev => ({ ...prev, main: menuMap[value] ?? 0 }));
setMenuSelections(prev => ({ ...prev, main: index }));
}
void handleMainMenuSelect(value);
}}
isClaudeInstalled={isClaudeInstalled}
@@ -448,7 +480,7 @@ export const App: React.FC = () => {
message={confirmDialog.message}
onConfirm={() => void confirmDialog.action()}
onCancel={() => {
setScreen('main');
setScreen(getConfirmCancelScreen(confirmDialog));
setConfirmDialog(null);
}}
/>
@@ -459,9 +491,8 @@ export const App: React.FC = () => {
existingStatusLine={existingStatusLine}
onSelectNpx={handleNpxInstall}
onSelectBunx={handleBunxInstall}
onCancel={() => {
setScreen('main');
}}
onCancel={handleInstallMenuCancel}
initialSelection={menuSelections.install}
/>
)}
{screen === 'powerline' && (
+39
View File
@@ -0,0 +1,39 @@
import {
describe,
expect,
it
} from 'vitest';
import {
clearInstallMenuSelection,
getConfirmCancelScreen
} from '../App';
describe('App confirm navigation helpers', () => {
it('defaults confirmation cancel navigation to the main menu', () => {
expect(getConfirmCancelScreen(null)).toBe('main');
expect(getConfirmCancelScreen({
message: 'Confirm install?',
action: () => Promise.resolve()
})).toBe('main');
});
it('returns to the install menu when the confirm dialog requests it', () => {
expect(getConfirmCancelScreen({
message: 'Confirm install?',
action: () => Promise.resolve(),
cancelScreen: 'install'
})).toBe('install');
});
it('clears saved install selection when leaving the install menu', () => {
expect(clearInstallMenuSelection({
main: 5,
install: 1
})).toEqual({ main: 5 });
const menuSelections = { main: 5 };
expect(clearInstallMenuSelection(menuSelections)).toBe(menuSelections);
});
});
+45 -35
View File
@@ -3,7 +3,12 @@ import {
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import React from 'react';
import {
List,
type ListEntry
} from './List';
export interface ConfirmDialogProps {
message?: string;
@@ -12,52 +17,57 @@ export interface ConfirmDialogProps {
inline?: boolean;
}
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ message, onConfirm, onCancel, inline = false }) => {
const [selectedIndex, setSelectedIndex] = useState(0); // Default to "Yes"
const CONFIRM_OPTIONS: ListEntry<boolean>[] = [
{
label: 'Yes',
value: true
},
{
label: 'No',
value: false
}
];
useInput((input, key) => {
if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(1, selectedIndex + 1));
} else if (key.return) {
if (selectedIndex === 0) {
onConfirm();
} else {
onCancel();
}
} else if (key.escape) {
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ message, onConfirm, onCancel, inline = false }) => {
useInput((_, key) => {
if (key.escape) {
onCancel();
}
});
const renderOptions = () => {
const yesStyle = selectedIndex === 0 ? { color: 'cyan' } : {};
const noStyle = selectedIndex === 1 ? { color: 'cyan' } : {};
return (
<Box flexDirection='column'>
<Text {...yesStyle}>
{selectedIndex === 0 ? '▶ ' : ' '}
Yes
</Text>
<Text {...noStyle}>
{selectedIndex === 1 ? '▶ ' : ' '}
No
</Text>
</Box>
);
};
if (inline) {
return renderOptions();
return (
<List
items={CONFIRM_OPTIONS}
onSelect={(confirmed) => {
if (confirmed) {
onConfirm();
return;
}
onCancel();
}}
color='cyan'
/>
);
}
return (
<Box flexDirection='column'>
<Text>{message}</Text>
<Box marginTop={1}>
{renderOptions()}
<List
items={CONFIRM_OPTIONS}
onSelect={(confirmed) => {
if (confirmed) {
onConfirm();
return;
}
onCancel();
}}
color='cyan'
/>
</Box>
</Box>
);
+50 -50
View File
@@ -3,16 +3,19 @@ import {
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import React from 'react';
import { getClaudeSettingsPath } from '../../utils/claude-settings';
import { List } from './List';
export interface InstallMenuProps {
bunxAvailable: boolean;
existingStatusLine: string | null;
onSelectNpx: () => void;
onSelectBunx: () => void;
onCancel: () => void;
initialSelection?: number;
}
export const InstallMenu: React.FC<InstallMenuProps> = ({
@@ -20,39 +23,44 @@ export const InstallMenu: React.FC<InstallMenuProps> = ({
existingStatusLine,
onSelectNpx,
onSelectBunx,
onCancel
onCancel,
initialSelection = 0
}) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const maxIndex = 2; // npx, bunx (if available), and back
useInput((input, key) => {
useInput((_, key) => {
if (key.escape) {
onCancel();
} else if (key.upArrow) {
if (selectedIndex === 2) {
setSelectedIndex(bunxAvailable ? 1 : 0); // Skip bunx if not available
} else {
setSelectedIndex(Math.max(0, selectedIndex - 1));
}
} else if (key.downArrow) {
if (selectedIndex === 0) {
setSelectedIndex(bunxAvailable ? 1 : 2); // Skip bunx if not available
} else if (selectedIndex === 1 && bunxAvailable) {
setSelectedIndex(2);
} else {
setSelectedIndex(Math.min(maxIndex, selectedIndex + 1));
}
} else if (key.return) {
if (selectedIndex === 0) {
onSelectNpx();
} else if (selectedIndex === 1 && bunxAvailable) {
onSelectBunx();
} else if (selectedIndex === 2) {
onCancel();
}
}
});
function onSelect(value: string) {
switch (value) {
case 'npx':
onSelectNpx();
break;
case 'bunx':
if (bunxAvailable) {
onSelectBunx();
}
break;
case 'back':
onCancel();
break;
}
}
const listItems = [
{
label: 'npx - Node Package Execute',
value: 'npx'
},
{
label: 'bunx - Bun Package Execute',
sublabel: bunxAvailable ? undefined : '(not installed)',
value: 'bunx',
disabled: !bunxAvailable
}
];
return (
<Box flexDirection='column'>
<Text bold>Install ccstatusline to Claude Code</Text>
@@ -71,29 +79,21 @@ export const InstallMenu: React.FC<InstallMenuProps> = ({
<Text dimColor>Select package manager to use:</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
<Box>
<Text color={selectedIndex === 0 ? 'blue' : undefined}>
{selectedIndex === 0 ? '▶ ' : ' '}
npx - Node Package Execute
</Text>
</Box>
<List
color='blue'
marginTop={1}
items={listItems}
onSelect={(line) => {
if (line === 'back') {
onCancel();
return;
}
<Box>
<Text color={selectedIndex === 1 && bunxAvailable ? 'blue' : undefined} dimColor={!bunxAvailable}>
{selectedIndex === 1 && bunxAvailable ? '▶ ' : ' '}
bunx - Bun Package Execute
{!bunxAvailable && ' (not installed)'}
</Text>
</Box>
<Box marginTop={1}>
<Text color={selectedIndex === 2 ? 'blue' : undefined}>
{selectedIndex === 2 ? '▶ ' : ' '}
Back
</Text>
</Box>
</Box>
onSelect(line);
}}
initialSelection={initialSelection}
showBackButton={true}
/>
<Box marginTop={2}>
<Text dimColor>
+4 -13
View File
@@ -94,21 +94,12 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
setCustomEditorWidget(null);
};
const shouldShowCustomKeybind = (widget: WidgetItem, keybind: CustomKeybind): boolean => {
if (keybind.action !== 'toggle-invert') {
return true;
}
const mode = widget.metadata?.display;
return mode === 'progress' || mode === 'progress-short';
};
const getVisibleCustomKeybinds = (widgetImpl: Widget, widget: WidgetItem): CustomKeybind[] => {
const getCustomKeybindsForWidget = (widgetImpl: Widget, widget: WidgetItem): CustomKeybind[] => {
if (!widgetImpl.getCustomKeybinds) {
return [];
}
return widgetImpl.getCustomKeybinds().filter(keybind => shouldShowCustomKeybind(widget, keybind));
return widgetImpl.getCustomKeybinds(widget);
};
const openWidgetPicker = (action: WidgetPickerAction) => {
@@ -208,7 +199,7 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
setMoveMode,
setShowClearConfirm,
openWidgetPicker,
getVisibleCustomKeybinds,
getCustomKeybindsForWidget,
setCustomEditorWidget
});
});
@@ -271,7 +262,7 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
if (widgetImpl) {
canToggleRaw = widgetImpl.supportsRawValue();
// Get custom keybinds from the widget
customKeybinds = getVisibleCustomKeybinds(widgetImpl, currentWidget);
customKeybinds = getCustomKeybindsForWidget(widgetImpl, currentWidget);
} else {
canToggleRaw = false;
}
+56 -45
View File
@@ -14,6 +14,7 @@ import type { Settings } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import { ConfirmDialog } from './ConfirmDialog';
import { List } from './List';
interface LineSelectorProps {
lines: WidgetItem[][];
@@ -47,6 +48,10 @@ const LineSelector: React.FC<LineSelectorProps> = ({
setLocalLines(lines);
}, [lines]);
useEffect(() => {
setSelectedIndex(initialSelection);
}, [initialSelection]);
const selectedLine = useMemo(
() => localLines[selectedIndex],
[localLines, selectedIndex]
@@ -60,7 +65,7 @@ const LineSelector: React.FC<LineSelectorProps> = ({
};
const deleteLine = (lineIndex: number) => {
// Don't allow deleting the last remaining line
// Don't allow deleting the last remaining line
if (localLines.length <= 1) {
return;
}
@@ -125,7 +130,7 @@ const LineSelector: React.FC<LineSelectorProps> = ({
}
return;
case 'd':
if (allowEditing && localLines.length > 1) {
if (allowEditing && localLines.length > 1 && selectedIndex < localLines.length) {
setShowDeleteDialog(true);
}
return;
@@ -138,16 +143,6 @@ const LineSelector: React.FC<LineSelectorProps> = ({
if (key.escape) {
onBack();
} else if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(localLines.length, selectedIndex + 1));
} else if (key.return) {
if (selectedIndex === localLines.length) {
onBack();
} else {
onSelect(selectedIndex);
}
}
});
@@ -197,7 +192,6 @@ const LineSelector: React.FC<LineSelectorProps> = ({
<Text>
<Text>
Line
{' '}
{selectedIndex + 1}
</Text>
{' '}
@@ -228,6 +222,12 @@ const LineSelector: React.FC<LineSelectorProps> = ({
);
}
const lineItems = localLines.map((line, index) => ({
label: `☰ Line ${index + 1}`,
sublabel: `(${line.length > 0 ? pluralize('widget', line.length, true) : 'empty'})`,
value: index
}));
return (
<>
<Box flexDirection='column'>
@@ -253,44 +253,55 @@ const LineSelector: React.FC<LineSelectorProps> = ({
</Text>
)}
<Box marginTop={1} flexDirection='column'>
{localLines.map((line, index) => {
const isSelected = selectedIndex === index;
const suffix = line.length
? pluralize('widget', line.length, true)
: 'empty';
{moveMode ? (
<Box marginTop={1} flexDirection='column'>
{localLines.map((line, index) => {
const isSelected = selectedIndex === index;
const suffix = line.length
? pluralize('widget', line.length, true)
: 'empty';
return (
<Box key={index}>
<Text color={isSelected ? (moveMode ? 'blue' : 'green') : undefined}>
<Text>{isSelected ? (moveMode ? '◆ ' : '▶ ') : ' '}</Text>
<Text>
return (
<Box key={index}>
<Text color={isSelected ? 'blue' : undefined}>
<Text>{isSelected ? '◆ ' : ' '}</Text>
<Text>
Line
<Text>
Line
{' '}
{index + 1}
</Text>
{' '}
{index + 1}
</Text>
{' '}
<Text dimColor={!isSelected}>
(
{suffix}
)
<Text dimColor={!isSelected}>
(
{suffix}
)
</Text>
</Text>
</Text>
</Text>
</Box>
);
})}
</Box>
);
})}
</Box>
) : (
<List
marginTop={1}
items={lineItems}
onSelect={(line) => {
if (line === 'back') {
onBack();
return;
}
{!moveMode && (
<Box marginTop={1}>
<Text color={selectedIndex === localLines.length ? 'green' : undefined}>
{selectedIndex === localLines.length ? '▶ ' : ' '}
Back
</Text>
</Box>
)}
</Box>
onSelect(line);
}}
onSelectionChange={(_, index) => {
setSelectedIndex(index);
}}
initialSelection={selectedIndex}
showBackButton={true}
/>
)}
</Box>
</>
);
+170
View File
@@ -0,0 +1,170 @@
import type { ForegroundColorName } from 'chalk';
import {
Box,
Text,
useInput,
type BoxProps
} from 'ink';
import {
useEffect,
useMemo,
useRef,
useState,
type PropsWithChildren
} from 'react';
export interface ListEntry<V = string | number> {
label: string;
sublabel?: string;
disabled?: boolean;
description?: string;
value: V;
props?: BoxProps;
}
interface ListProps<V = string | number> extends BoxProps {
items: (ListEntry<V> | '-')[];
onSelect: (value: V | 'back', index: number) => void;
onSelectionChange?: (value: V | 'back', index: number) => void;
initialSelection?: number;
showBackButton?: boolean;
color?: ForegroundColorName;
wrapNavigation?: boolean;
}
export function List<V = string | number>({
items,
onSelect,
onSelectionChange,
initialSelection = 0,
showBackButton,
color,
wrapNavigation = false,
...boxProps
}: ListProps<V>) {
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
const latestOnSelectionChangeRef = useRef(onSelectionChange);
const _items = useMemo(() => {
if (showBackButton) {
return [...items, '-' as const, { label: '← Back', value: 'back' as V }];
}
return items;
}, [items, showBackButton]);
const selectableItems = _items.filter(item => item !== '-' && !item.disabled) as ListEntry<V>[];
const selectedItem = selectableItems[selectedIndex];
const selectedValue = selectedItem?.value;
const actualIndex = _items.findIndex(item => item === selectedItem);
useEffect(() => {
latestOnSelectionChangeRef.current = onSelectionChange;
}, [onSelectionChange]);
useEffect(() => {
const maxIndex = Math.max(selectableItems.length - 1, 0);
setSelectedIndex(Math.min(initialSelection, maxIndex));
}, [initialSelection, selectableItems.length]);
useEffect(() => {
if (selectedValue !== undefined) {
latestOnSelectionChangeRef.current?.(selectedValue, selectedIndex);
}
}, [selectedIndex, selectedValue]);
useInput((_, key) => {
if (key.upArrow) {
const prev = selectedIndex - 1;
const prevIndex = prev < 0
? (wrapNavigation ? selectableItems.length - 1 : 0)
: prev;
setSelectedIndex(prevIndex);
return;
}
if (key.downArrow) {
const next = selectedIndex + 1;
const nextIndex = next > selectableItems.length - 1
? (wrapNavigation ? 0 : selectableItems.length - 1)
: next;
setSelectedIndex(nextIndex);
return;
}
if (key.return && selectedItem) {
onSelect(selectedItem.value, selectedIndex);
return;
}
});
return (
<Box flexDirection='column' {...boxProps}>
{_items.map((item, index) => {
if (item === '-') {
return <ListSeparator key={index} />;
}
const isSelected = index === actualIndex;
return (
<ListItem
key={index}
isSelected={isSelected}
color={color}
disabled={item.disabled}
{...item.props}
>
<Text>
<Text>
{item.label}
</Text>
{item.sublabel && (
<Text dimColor={!isSelected}>
{' '}
{item.sublabel}
</Text>
)}
</Text>
</ListItem>
);
})}
{selectedItem?.description && (
<Box marginTop={1} paddingLeft={2}>
<Text dimColor wrap='wrap'>
{selectedItem.description}
</Text>
</Box>
)}
</Box>
);
}
interface ListItemProps extends PropsWithChildren, BoxProps {
isSelected: boolean;
color?: ForegroundColorName;
disabled?: boolean;
}
export function ListItem({
children,
isSelected,
color = 'green',
disabled,
...boxProps
}: ListItemProps) {
return (
<Box {...boxProps}>
<Text color={isSelected ? color : undefined} dimColor={disabled}>
<Text>{isSelected ? '▶ ' : ' '}</Text>
<Text>{children}</Text>
</Text>
</Box>
);
}
export function ListSeparator() {
return <Text> </Text>;
}
+106 -89
View File
@@ -1,13 +1,14 @@
import {
Box,
Text,
useInput
Text
} from 'ink';
import React, { useState } from 'react';
import React from 'react';
import type { Settings } from '../../types/Settings';
import { type PowerlineFontStatus } from '../../utils/powerline';
import { List } from './List';
export type MainMenuOption = 'lines'
| 'colors'
| 'powerline'
@@ -19,7 +20,7 @@ export type MainMenuOption = 'lines'
| 'exit';
export interface MainMenuProps {
onSelect: (value: MainMenuOption) => void;
onSelect: (value: MainMenuOption, index: number) => void;
isClaudeInstalled: boolean;
hasChanges: boolean;
initialSelection?: number;
@@ -28,111 +29,127 @@ export interface MainMenuProps {
previewIsTruncated?: boolean;
}
export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasChanges, initialSelection = 0, powerlineFontStatus, settings, previewIsTruncated }) => {
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
export const MainMenu: React.FC<MainMenuProps> = ({
onSelect,
isClaudeInstalled,
hasChanges,
initialSelection = 0,
powerlineFontStatus,
settings,
previewIsTruncated
}) => {
// Build menu structure with visual gaps
const menuItems = [
{ label: '📝 Edit Lines', value: 'lines', selectable: true },
{ label: '🎨 Edit Colors', value: 'colors', selectable: true },
{ label: '⚡ Powerline Setup', value: 'powerline', selectable: true },
{ label: '', value: '_gap1', selectable: false }, // Visual gap
{ label: '💻 Terminal Options', value: 'terminalConfig', selectable: true },
{ label: '🌐 Global Overrides', value: 'globalOverrides', selectable: true },
{ label: '', value: '_gap2', selectable: false }, // Visual gap
{ label: isClaudeInstalled ? '🔌 Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install', selectable: true }
const menuItems: ({
label: string;
value: MainMenuOption;
description: string;
} | '-')[] = [
{
label: '📝 Edit Lines',
value: 'lines',
description:
'Configure any number of status lines with various widgets like model info, git status, and token usage'
},
{
label: '🎨 Edit Colors',
value: 'colors',
description:
'Customize colors for each widget including foreground, background, and bold styling'
},
{
label: '⚡ Powerline Setup',
value: 'powerline',
description:
'Install Powerline fonts for enhanced visual separators and symbols in your status line'
},
'-' as const,
{
label: '💻 Terminal Options',
value: 'terminalConfig',
description: 'Configure terminal-specific settings for optimal display'
},
{
label: '🌐 Global Overrides',
value: 'globalOverrides',
description:
'Set global padding, separators, and color overrides that apply to all widgets'
},
'-' as const,
{
label: isClaudeInstalled
? '🔌 Uninstall from Claude Code'
: '📦 Install to Claude Code',
value: 'install',
description: isClaudeInstalled
? 'Remove ccstatusline from your Claude Code settings'
: 'Add ccstatusline to your Claude Code settings for automatic status line rendering'
}
];
if (hasChanges) {
menuItems.push(
{ label: '💾 Save & Exit', value: 'save', selectable: true },
{ label: '❌ Exit without saving', value: 'exit', selectable: true },
{ label: '', value: '_gap3', selectable: false }, // Visual gap
{ label: '⭐ Like ccstatusline? Star us on GitHub', value: 'starGithub', selectable: true }
{
label: '💾 Save & Exit',
value: 'save',
description: 'Save all changes and exit the configuration tool'
},
{
label: '❌ Exit without saving',
value: 'exit',
description: 'Exit without saving your changes'
},
'-' as const,
{
label: '⭐ Like ccstatusline? Star us on GitHub',
value: 'starGithub',
description: 'Open the ccstatusline GitHub repository in your browser so you can star the project'
}
);
} else {
menuItems.push(
{ label: '🚪 Exit', value: 'exit', selectable: true },
{ label: '', value: '_gap3', selectable: false }, // Visual gap
{ label: '⭐ Like ccstatusline? Star us on GitHub', value: 'starGithub', selectable: true }
{
label: '🚪 Exit',
value: 'exit',
description: 'Exit the configuration tool'
},
'-' as const,
{
label: '⭐ Like ccstatusline? Star us on GitHub',
value: 'starGithub',
description: 'Open the ccstatusline GitHub repository in your browser so you can star the project'
}
);
}
// Get only selectable items for navigation
const selectableItems = menuItems.filter(item => item.selectable);
useInput((input, key) => {
if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(selectableItems.length - 1, selectedIndex + 1));
} else if (key.return) {
const item = selectableItems[selectedIndex];
if (item) {
// Since we filtered by selectable: true, value is guaranteed to be MainMenuOption
onSelect(item.value as MainMenuOption);
}
}
});
// Get description for selected item
const getDescription = (value: string): string => {
const descriptions: Record<string, string> = {
lines: 'Configure any number of status lines with various widgets like model info, git status, and token usage',
colors: 'Customize colors for each widget including foreground, background, and bold styling',
powerline: 'Install Powerline fonts for enhanced visual separators and symbols in your status line',
globalOverrides: 'Set global padding, separators, and color overrides that apply to all widgets',
install: isClaudeInstalled
? 'Remove ccstatusline from your Claude Code settings'
: 'Add ccstatusline to your Claude Code settings for automatic status line rendering',
terminalConfig: 'Configure terminal-specific settings for optimal display',
starGithub: 'Open the ccstatusline GitHub repository in your browser so you can star the project',
save: 'Save all changes and exit the configuration tool',
exit: hasChanges
? 'Exit without saving your changes'
: 'Exit the configuration tool'
};
return descriptions[value] ?? '';
};
const selectedItem = selectableItems[selectedIndex];
const description = selectedItem ? getDescription(selectedItem.value) : '';
// Check if we should show the truncation warning
const showTruncationWarning = previewIsTruncated && settings?.flexMode === 'full-minus-40';
const showTruncationWarning
= previewIsTruncated && settings?.flexMode === 'full-minus-40';
return (
<Box flexDirection='column'>
{showTruncationWarning && (
<Box marginBottom={1}>
<Text color='yellow'> Some lines are truncated, see Terminal Options Terminal Width for info</Text>
<Text color='yellow'>
Some lines are truncated, see Terminal Options Terminal Width
for info
</Text>
</Box>
)}
<Text bold>Main Menu</Text>
<Box marginTop={1} flexDirection='column'>
{menuItems.map((item, idx) => {
if (!item.selectable && item.value.startsWith('_gap')) {
return <Text key={item.value}> </Text>;
}
const selectableIdx = selectableItems.indexOf(item);
const isSelected = selectableIdx === selectedIndex;
return (
<Text
key={item.value}
color={isSelected ? 'green' : undefined}
>
{isSelected ? '▶ ' : ' '}
{item.label}
</Text>
);
})}
</Box>
{description && (
<Box marginTop={1} paddingLeft={2}>
<Text dimColor wrap='wrap'>{description}</Text>
</Box>
)}
<Text bold>Main Menu</Text>
<List
items={menuItems}
marginTop={1}
onSelect={(value, index) => {
if (value === 'back') {
return;
}
onSelect(value, index);
}}
initialSelection={initialSelection}
/>
</Box>
);
};
+163 -157
View File
@@ -6,14 +6,139 @@ import {
import * as os from 'os';
import React, { useState } from 'react';
import type { PowerlineConfig } from '../../types/PowerlineConfig';
import type { Settings } from '../../types/Settings';
import { type PowerlineFontStatus } from '../../utils/powerline';
import { buildEnabledPowerlineSettings } from '../../utils/powerline-settings';
import { ConfirmDialog } from './ConfirmDialog';
import {
List,
type ListEntry
} from './List';
import { PowerlineSeparatorEditor } from './PowerlineSeparatorEditor';
import { PowerlineThemeSelector } from './PowerlineThemeSelector';
type PowerlineMenuValue = 'separator' | 'startCap' | 'endCap' | 'themes';
type Screen = 'menu' | PowerlineMenuValue;
const POWERLINE_MENU_LABEL_WIDTH = 11;
function formatPowerlineMenuLabel(label: string): string {
return label.padEnd(POWERLINE_MENU_LABEL_WIDTH, ' ');
}
export function getSeparatorDisplay(powerlineConfig: PowerlineConfig): string {
const seps = powerlineConfig.separators;
if (seps.length > 1) {
return 'multiple';
}
const sep = seps[0] ?? '\uE0B0';
const presets = [
{ char: '\uE0B0', name: 'Triangle Right' },
{ char: '\uE0B2', name: 'Triangle Left' },
{ char: '\uE0B4', name: 'Round Right' },
{ char: '\uE0B6', name: 'Round Left' }
];
const preset = presets.find(item => item.char === sep);
if (preset) {
return `${preset.char} - ${preset.name}`;
}
return `${sep} - Custom`;
}
export function getCapDisplay(
powerlineConfig: PowerlineConfig,
type: 'start' | 'end'
): string {
const caps = type === 'start'
? powerlineConfig.startCaps
: powerlineConfig.endCaps;
if (caps.length === 0) {
return 'none';
}
if (caps.length > 1) {
return 'multiple';
}
const cap = caps[0];
if (!cap) {
return 'none';
}
const presets = type === 'start' ? [
{ char: '\uE0B2', name: 'Triangle' },
{ char: '\uE0B6', name: 'Round' },
{ char: '\uE0BA', name: 'Lower Triangle' },
{ char: '\uE0BE', name: 'Diagonal' }
] : [
{ char: '\uE0B0', name: 'Triangle' },
{ char: '\uE0B4', name: 'Round' },
{ char: '\uE0B8', name: 'Lower Triangle' },
{ char: '\uE0BC', name: 'Diagonal' }
];
const preset = presets.find(item => item.char === cap);
if (preset) {
return `${preset.char} - ${preset.name}`;
}
return `${cap} - Custom`;
}
export function getThemeDisplay(powerlineConfig: PowerlineConfig): string {
const theme = powerlineConfig.theme;
if (!theme || theme === 'custom') {
return 'Custom';
}
return theme.charAt(0).toUpperCase() + theme.slice(1);
}
export function buildPowerlineSetupMenuItems(
powerlineConfig: PowerlineConfig
): ListEntry<PowerlineMenuValue>[] {
const disabled = !powerlineConfig.enabled;
return [
{
label: formatPowerlineMenuLabel('Separator'),
sublabel: `(${getSeparatorDisplay(powerlineConfig)})`,
value: 'separator',
disabled,
description: 'Choose the glyph used between powerline segments.'
},
{
label: formatPowerlineMenuLabel('Start Cap'),
sublabel: `(${getCapDisplay(powerlineConfig, 'start')})`,
value: 'startCap',
disabled,
description: 'Configure the cap glyph that appears at the start of each powerline line.'
},
{
label: formatPowerlineMenuLabel('End Cap'),
sublabel: `(${getCapDisplay(powerlineConfig, 'end')})`,
value: 'endCap',
disabled,
description: 'Configure the cap glyph that appears at the end of each powerline line.'
},
{
label: formatPowerlineMenuLabel('Themes'),
sublabel: `(${getThemeDisplay(powerlineConfig)})`,
value: 'themes',
disabled,
description: 'Preview built-in powerline themes or copy a theme into custom widget colors.'
}
];
}
export interface PowerlineSetupProps {
settings: Settings;
powerlineFontStatus: PowerlineFontStatus;
@@ -25,8 +150,6 @@ export interface PowerlineSetupProps {
onClearMessage: () => void;
}
type Screen = 'menu' | 'separator' | 'startCap' | 'endCap' | 'themes';
export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
settings,
powerlineFontStatus,
@@ -43,138 +166,55 @@ export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
const [confirmingEnable, setConfirmingEnable] = useState(false);
const [confirmingFontInstall, setConfirmingFontInstall] = useState(false);
// Check if there are any separators or flex-separators in the current configuration
const hasSeparatorItems = settings.lines.some(line => line.some(item => item.type === 'separator' || item.type === 'flex-separator'));
// Menu items for navigation
const menuItems = [
{ label: 'Separator', value: 'separator' },
{ label: 'Start Cap', value: 'startCap' },
{ label: 'End Cap', value: 'endCap' },
{ label: 'Themes', value: 'themes' },
{ label: '← Back', value: 'back' }
];
// Helper functions for display
const getSeparatorDisplay = (): string => {
const seps = powerlineConfig.separators;
if (seps.length > 1) {
return 'multiple';
}
const sep = seps[0] ?? '\uE0B0';
const presets = [
{ char: '\uE0B0', name: 'Triangle Right' },
{ char: '\uE0B2', name: 'Triangle Left' },
{ char: '\uE0B4', name: 'Round Right' },
{ char: '\uE0B6', name: 'Round Left' }
];
const preset = presets.find(p => p.char === sep);
if (preset) {
return `${preset.char} - ${preset.name}`;
}
return `${sep} - Custom`;
};
const getCapDisplay = (type: 'start' | 'end'): string => {
const caps = type === 'start'
? powerlineConfig.startCaps
: powerlineConfig.endCaps;
if (caps.length === 0)
return 'none';
if (caps.length > 1)
return 'multiple';
const cap = caps[0];
if (!cap)
return 'none';
const presets = type === 'start' ? [
{ char: '\uE0B2', name: 'Triangle' },
{ char: '\uE0B6', name: 'Round' },
{ char: '\uE0BA', name: 'Lower Triangle' },
{ char: '\uE0BE', name: 'Diagonal' }
] : [
{ char: '\uE0B0', name: 'Triangle' },
{ char: '\uE0B4', name: 'Round' },
{ char: '\uE0B8', name: 'Lower Triangle' },
{ char: '\uE0BC', name: 'Diagonal' }
];
const preset = presets.find(c => c.char === cap);
if (preset) {
return `${preset.char} - ${preset.name}`;
}
return `${cap} - Custom`;
};
const getThemeDisplay = (): string => {
const theme = powerlineConfig.theme;
if (!theme || theme === 'custom')
return 'Custom';
return theme.charAt(0).toUpperCase() + theme.slice(1);
};
const hasSeparatorItems = settings.lines.some(line => line.some(
item => item.type === 'separator' || item.type === 'flex-separator'
));
useInput((input, key) => {
// Block all input handling when font installation message is shown or installing
if (fontInstallMessage || installingFonts) {
// Only clear message on non-escape keys when message is shown
if (fontInstallMessage && !key.escape) {
onClearMessage();
}
// Always return early to prevent any other input handling
return;
}
// Skip input handling when confirmations are active - let ConfirmDialog handle it
if (confirmingFontInstall || confirmingEnable) {
return;
}
if (screen === 'menu') {
// Menu navigation mode
if (key.escape) {
onBack();
} else if (key.upArrow) {
setSelectedMenuItem(Math.max(0, selectedMenuItem - 1));
} else if (key.downArrow) {
setSelectedMenuItem(Math.min(menuItems.length - 1, selectedMenuItem + 1));
} else if (key.return) {
const selected = menuItems[selectedMenuItem];
if (selected) {
if (selected.value === 'back') {
onBack();
} else if (powerlineConfig.enabled) {
setScreen(selected.value as Screen);
}
}
} else if (input === 't' || input === 'T') {
// Toggle powerline mode
if (!powerlineConfig.enabled) {
// Only show confirmation when enabling if there are separators to remove
if (hasSeparatorItems) {
setConfirmingEnable(true);
} else {
// Enable directly without confirmation since there are no separators.
onUpdate(buildEnabledPowerlineSettings(settings, false));
}
} else {
// Disable without confirmation
const newConfig = { ...powerlineConfig, enabled: false };
onUpdate({ ...settings, powerline: newConfig });
onUpdate({
...settings,
powerline: {
...powerlineConfig,
enabled: false
}
});
}
} else if (input === 'i' || input === 'I') {
// Show font installation consent prompt
setConfirmingFontInstall(true);
} else if ((input === 'a' || input === 'A') && powerlineConfig.enabled) {
// Toggle autoAlign when powerline is enabled
const newConfig = { ...powerlineConfig, autoAlign: !powerlineConfig.autoAlign };
onUpdate({ ...settings, powerline: newConfig });
onUpdate({
...settings,
powerline: {
...powerlineConfig,
autoAlign: !powerlineConfig.autoAlign
}
});
}
}
});
// Render sub-screens
if (screen === 'separator') {
return (
<PowerlineSeparatorEditor
@@ -218,7 +258,6 @@ export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
);
}
// Main menu screen
return (
<Box flexDirection='column'>
{!confirmingFontInstall && !installingFonts && !fontInstallMessage && (
@@ -374,62 +413,29 @@ export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
</>
)}
<Box marginTop={1} flexDirection='column'>
{powerlineConfig.enabled ? (
<>
{menuItems.map((item, index) => {
const isSelected = index === selectedMenuItem;
let displayValue = '';
{!powerlineConfig.enabled && (
<Box marginTop={1}>
<Text dimColor>Enable Powerline mode to configure separators, caps, and themes.</Text>
</Box>
)}
switch (item.value) {
case 'separator':
displayValue = getSeparatorDisplay();
break;
case 'startCap':
displayValue = getCapDisplay('start');
break;
case 'endCap':
displayValue = getCapDisplay('end');
break;
case 'themes':
displayValue = getThemeDisplay();
break;
case 'back':
displayValue = '';
break;
}
<List
marginTop={1}
items={buildPowerlineSetupMenuItems(powerlineConfig)}
onSelect={(value) => {
if (value === 'back') {
onBack();
return;
}
if (item.value === 'back') {
return (
<Box key={item.value} marginTop={1}>
<Text color={isSelected ? 'green' : undefined}>
{isSelected ? '▶ ' : ' '}
{item.label}
</Text>
</Box>
);
}
return (
<Box key={item.value}>
<Text color={isSelected ? 'green' : undefined}>
{isSelected ? '▶ ' : ' '}
{item.label.padEnd(11, ' ')}
<Text dimColor>
{displayValue && `(${displayValue})`}
</Text>
</Text>
</Box>
);
})}
</>
) : (
// When powerline is disabled, show ESC to go back message
<Box marginTop={1}>
<Text dimColor>Press ESC to go back</Text>
</Box>
)}
</Box>
setScreen(value);
}}
onSelectionChange={(_, index) => {
setSelectedMenuItem(index);
}}
initialSelection={selectedMenuItem}
showBackButton={true}
/>
</>
)}
</Box>
+137 -128
View File
@@ -4,6 +4,8 @@ import {
useInput
} from 'ink';
import React, {
useEffect,
useMemo,
useRef,
useState
} from 'react';
@@ -16,6 +18,74 @@ import {
} from '../../utils/colors';
import { ConfirmDialog } from './ConfirmDialog';
import {
List,
type ListEntry
} from './List';
export function buildPowerlineThemeItems(
themes: string[],
originalTheme: string
): ListEntry<string>[] {
return themes.map((themeName) => {
const theme = getPowerlineTheme(themeName);
return {
label: theme?.name ?? themeName,
sublabel: themeName === originalTheme ? '(original)' : undefined,
value: themeName,
description: theme?.description ?? ''
};
});
}
export function applyCustomPowerlineTheme(
settings: Settings,
themeName: string
): Settings | null {
const theme = getPowerlineTheme(themeName);
if (!theme || themeName === 'custom') {
return null;
}
const colorLevel = getColorLevelString(settings.colorLevel);
const colorLevelKey = colorLevel === 'ansi16' ? '1' : colorLevel === 'ansi256' ? '2' : '3';
const themeColors = theme[colorLevelKey];
if (!themeColors) {
return null;
}
const lines = settings.lines.map((line) => {
let widgetColorIndex = 0;
return line.map((widget) => {
if (widget.type === 'separator' || widget.type === 'flex-separator') {
return widget;
}
const fgColor = themeColors.fg[widgetColorIndex % themeColors.fg.length];
const bgColor = themeColors.bg[widgetColorIndex % themeColors.bg.length];
widgetColorIndex++;
return {
...widget,
color: fgColor,
backgroundColor: bgColor
};
});
});
return {
...settings,
powerline: {
...settings.powerline,
theme: 'custom'
},
lines
};
}
export interface PowerlineThemeSelectorProps {
settings: Settings;
@@ -28,120 +98,63 @@ export const PowerlineThemeSelector: React.FC<PowerlineThemeSelectorProps> = ({
onUpdate,
onBack
}) => {
const themes = getPowerlineThemes();
const themes = useMemo(() => getPowerlineThemes(), []);
const currentTheme = settings.powerline.theme ?? 'custom';
const [selectedIndex, setSelectedIndex] = useState(Math.max(0, themes.indexOf(currentTheme)));
const [showCustomizeConfirm, setShowCustomizeConfirm] = useState(false);
const originalThemeRef = useRef(currentTheme);
const originalSettingsRef = useRef(settings);
const latestSettingsRef = useRef(settings);
const latestOnUpdateRef = useRef(onUpdate);
const didHandleInitialSelectionRef = useRef(false);
const applyTheme = (themeName: string) => {
// Simply change the theme setting, don't modify widget colors
const updatedSettings = {
...settings,
useEffect(() => {
latestSettingsRef.current = settings;
latestOnUpdateRef.current = onUpdate;
}, [settings, onUpdate]);
useEffect(() => {
const themeName = themes[selectedIndex];
if (!themeName) {
return;
}
if (!didHandleInitialSelectionRef.current) {
didHandleInitialSelectionRef.current = true;
return;
}
latestOnUpdateRef.current({
...latestSettingsRef.current,
powerline: {
...settings.powerline,
...latestSettingsRef.current.powerline,
theme: themeName
}
};
onUpdate(updatedSettings);
};
const customizeTheme = () => {
// Copy current theme's colors to widgets and switch to custom theme
const currentThemeName = themes[selectedIndex];
if (!currentThemeName) {
return;
}
const theme = getPowerlineTheme(currentThemeName);
if (!theme || currentThemeName === 'custom') {
// If already on custom, just go back
onBack();
return;
}
const colorLevel = getColorLevelString(settings.colorLevel);
const colorLevelKey = colorLevel === 'ansi16' ? '1' : colorLevel === 'ansi256' ? '2' : '3';
const themeColors = theme[colorLevelKey];
if (themeColors) {
// Apply theme colors to widgets
const newLines = settings.lines.map((line) => {
let widgetColorIndex = 0;
return line.map((widget) => {
// Skip separators
if (widget.type === 'separator' || widget.type === 'flex-separator') {
return widget;
}
const fgColor = themeColors.fg[widgetColorIndex % themeColors.fg.length];
const bgColor = themeColors.bg[widgetColorIndex % themeColors.bg.length];
widgetColorIndex++;
return {
...widget,
color: fgColor,
backgroundColor: bgColor
};
});
});
const updatedSettings = {
...settings,
powerline: {
...settings.powerline,
theme: 'custom'
},
lines: newLines
};
onUpdate(updatedSettings);
}
onBack();
};
});
}, [selectedIndex, themes]);
useInput((input, key) => {
// Skip input handling when confirmation is active - let ConfirmDialog handle it
if (showCustomizeConfirm) {
return;
}
{
// Normal input handling
if (key.escape) {
// Restore original settings completely when canceling
onUpdate(originalSettingsRef.current);
onBack();
} else if (key.upArrow) {
const newIndex = Math.max(0, selectedIndex - 1);
setSelectedIndex(newIndex);
const newTheme = themes[newIndex];
if (newTheme) {
applyTheme(newTheme);
}
} else if (key.downArrow) {
const newIndex = Math.min(themes.length - 1, selectedIndex + 1);
setSelectedIndex(newIndex);
const newTheme = themes[newIndex];
if (newTheme) {
applyTheme(newTheme);
}
} else if (key.return) {
// User confirmed their selection, so we keep the current theme
onBack();
} else if (input === 'c' || input === 'C') {
// Customize theme - copy theme colors to widgets
const currentThemeName = themes[selectedIndex];
if (currentThemeName && currentThemeName !== 'custom') {
setShowCustomizeConfirm(true);
}
if (key.escape) {
onUpdate(originalSettingsRef.current);
onBack();
} else if (input === 'c' || input === 'C') {
const currentThemeName = themes[selectedIndex];
if (currentThemeName && currentThemeName !== 'custom') {
setShowCustomizeConfirm(true);
}
}
});
const selectedThemeName = themes[selectedIndex];
const selectedTheme = selectedThemeName ? getPowerlineTheme(selectedThemeName) : undefined;
const themeItems = useMemo(
() => buildPowerlineThemeItems(themes, originalThemeRef.current),
[themes]
);
if (showCustomizeConfirm) {
return (
@@ -159,8 +172,14 @@ export const PowerlineThemeSelector: React.FC<PowerlineThemeSelectorProps> = ({
<ConfirmDialog
inline={true}
onConfirm={() => {
customizeTheme();
if (selectedThemeName) {
const updatedSettings = applyCustomPowerlineTheme(settings, selectedThemeName);
if (updatedSettings) {
onUpdate(updatedSettings);
}
}
setShowCustomizeConfirm(false);
onBack();
}}
onCancel={() => {
setShowCustomizeConfirm(false);
@@ -185,40 +204,30 @@ export const PowerlineThemeSelector: React.FC<PowerlineThemeSelectorProps> = ({
</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
{themes.map((themeName, index) => {
const theme = getPowerlineTheme(themeName);
const isSelected = index === selectedIndex;
const isOriginal = themeName === originalThemeRef.current;
<List
marginTop={1}
items={themeItems}
onSelect={() => {
onBack();
}}
onSelectionChange={(themeName, index) => {
if (themeName === 'back') {
return;
}
return (
<Box key={themeName}>
<Text color={isSelected ? 'green' : undefined}>
{isSelected ? '▶ ' : ' '}
{theme?.name ?? themeName}
{isOriginal && <Text dimColor> (original)</Text>}
</Text>
</Box>
);
})}
</Box>
setSelectedIndex(index);
}}
initialSelection={selectedIndex}
/>
{selectedTheme && (
<Box marginTop={2} flexDirection='column'>
<Text dimColor>Description:</Text>
<Box marginLeft={2}>
<Text>{selectedTheme.description}</Text>
</Box>
{selectedThemeName && selectedThemeName !== 'custom' && (
<Box marginTop={1}>
<Text dimColor>Press (c) to customize this theme - copies colors to widgets</Text>
</Box>
)}
{settings.colorLevel === 1 && (
<Box>
<Text color='yellow'> 16 color mode themes have a very limited palette, we recommend switching color level in Terminal Options</Text>
</Box>
)}
{selectedThemeName && selectedThemeName !== 'custom' && (
<Box marginTop={1}>
<Text dimColor>Press (c) to customize this theme - copies colors to widgets</Text>
</Box>
)}
{settings.colorLevel === 1 && (
<Box marginTop={1}>
<Text color='yellow'> 16 color mode themes have a very limited palette, we recommend switching color level in Terminal Options</Text>
</Box>
)}
</Box>
+86 -81
View File
@@ -13,6 +13,50 @@ import {
} from '../../utils/color-sanitize';
import { ConfirmDialog } from './ConfirmDialog';
import {
List,
type ListEntry
} from './List';
type TerminalOptionsValue = 'width' | 'colorLevel';
export function getNextColorLevel(level: 0 | 1 | 2 | 3): 0 | 1 | 2 | 3 {
return ((level + 1) % 4) as 0 | 1 | 2 | 3;
}
export function shouldWarnOnColorLevelChange(
currentLevel: 0 | 1 | 2 | 3,
nextLevel: 0 | 1 | 2 | 3,
hasCustomColors: boolean
): boolean {
return hasCustomColors
&& ((currentLevel === 2 && nextLevel !== 2)
|| (currentLevel === 3 && nextLevel !== 3));
}
export function buildTerminalOptionsItems(
colorLevel: 0 | 1 | 2 | 3
): ListEntry<TerminalOptionsValue>[] {
return [
{
label: '◱ Terminal Width',
value: 'width',
description: 'Configure how the status line uses available terminal width and when it should compact.'
},
{
label: '▓ Color Level',
sublabel: `(${getColorLevelLabel(colorLevel)})`,
value: 'colorLevel',
description: [
'Color level affects how colors are rendered:',
'• Truecolor: Full 24-bit RGB colors (16.7M colors)',
'• 256 Color: Extended color palette (256 colors)',
'• Basic: Standard 16-color terminal palette',
'• No Color: Disables all color output'
].join('\n')
}
];
}
export interface TerminalOptionsMenuProps {
settings: Settings;
@@ -20,49 +64,47 @@ export interface TerminalOptionsMenuProps {
onBack: (target?: string) => void;
}
export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({ settings, onUpdate, onBack }) => {
export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({
settings,
onUpdate,
onBack
}) => {
const [showColorWarning, setShowColorWarning] = useState(false);
const [pendingColorLevel, setPendingColorLevel] = useState<0 | 1 | 2 | 3 | null>(null);
const [selectedIndex, setSelectedIndex] = useState(0);
const handleSelect = () => {
if (selectedIndex === 2) {
// Back button
const handleSelect = (value: TerminalOptionsValue | 'back') => {
if (value === 'back') {
onBack();
} else if (selectedIndex === 0) {
// Terminal Width Options
onBack('width');
} else if (selectedIndex === 1) {
// Color Level
// Check if there are any custom colors that would be lost
const hasCustomColors = hasCustomWidgetColors(settings.lines);
const currentLevel = settings.colorLevel;
const nextLevel = ((currentLevel + 1) % 4) as 0 | 1 | 2 | 3;
// Warn if switching away from mode that supports custom colors
if (hasCustomColors
&& ((currentLevel === 2 && nextLevel !== 2) // Switching from 256 color mode
|| (currentLevel === 3 && nextLevel !== 3))) { // Switching from truecolor mode
setShowColorWarning(true);
setPendingColorLevel(nextLevel);
} else {
// Update chalk level immediately
chalk.level = nextLevel;
const cleanedLines = sanitizeLinesForColorLevel(settings.lines, nextLevel);
onUpdate({
...settings,
lines: cleanedLines,
colorLevel: nextLevel
});
}
return;
}
if (value === 'width') {
onBack('width');
return;
}
const hasCustomColors = hasCustomWidgetColors(settings.lines);
const currentLevel = settings.colorLevel;
const nextLevel = getNextColorLevel(currentLevel);
if (shouldWarnOnColorLevelChange(currentLevel, nextLevel, hasCustomColors)) {
setShowColorWarning(true);
setPendingColorLevel(nextLevel);
return;
}
chalk.level = nextLevel;
const cleanedLines = sanitizeLinesForColorLevel(settings.lines, nextLevel);
onUpdate({
...settings,
lines: cleanedLines,
colorLevel: nextLevel
});
};
const handleColorConfirm = () => {
// Proceed with color level change and clean up custom colors
if (pendingColorLevel !== null) {
chalk.level = pendingColorLevel;
@@ -83,19 +125,9 @@ export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({ settin
setPendingColorLevel(null);
};
useInput((input, key) => {
if (key.escape) {
if (!showColorWarning) {
onBack();
}
} else if (!showColorWarning) {
if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(2, selectedIndex + 1));
} else if (key.return) {
handleSelect();
}
useInput((_, key) => {
if (key.escape && !showColorWarning) {
onBack();
}
});
@@ -118,39 +150,12 @@ export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({ settin
) : (
<>
<Text color='white'>Configure terminal-specific settings for optimal display</Text>
<Box marginTop={1} flexDirection='column'>
<Box>
<Text color={selectedIndex === 0 ? 'green' : undefined}>
{selectedIndex === 0 ? '▶ ' : ' '}
Terminal Width
</Text>
</Box>
<Box>
<Text color={selectedIndex === 1 ? 'green' : undefined}>
{selectedIndex === 1 ? '▶ ' : ' '}
Color Level:
{' '}
{getColorLevelLabel(settings.colorLevel)}
</Text>
</Box>
<Box marginTop={1}>
<Text color={selectedIndex === 2 ? 'green' : undefined}>
{selectedIndex === 2 ? '▶ ' : ' '}
Back
</Text>
</Box>
</Box>
{selectedIndex === 1 && (
<Box marginTop={1} flexDirection='column'>
<Text dimColor>Color level affects how colors are rendered:</Text>
<Text dimColor> Truecolor: Full 24-bit RGB colors (16.7M colors)</Text>
<Text dimColor> 256 Color: Extended color palette (256 colors)</Text>
<Text dimColor> Basic: Standard 16-color terminal palette</Text>
<Text dimColor> No Color: Disables all color output</Text>
</Box>
)}
<List
marginTop={1}
items={buildTerminalOptionsItems(settings.colorLevel)}
onSelect={handleSelect}
showBackButton={true}
/>
</>
)}
</Box>
+93 -94
View File
@@ -9,38 +9,89 @@ import type { FlexMode } from '../../types/FlexMode';
import type { Settings } from '../../types/Settings';
import { shouldInsertInput } from '../../utils/input-guards';
import {
List,
type ListEntry
} from './List';
export const TERMINAL_WIDTH_OPTIONS: FlexMode[] = ['full', 'full-minus-40', 'full-until-compact'];
export function getTerminalWidthSelectionIndex(selectedOption: FlexMode): number {
const selectedIndex = TERMINAL_WIDTH_OPTIONS.indexOf(selectedOption);
return selectedIndex >= 0 ? selectedIndex : 0;
}
export function validateCompactThresholdInput(value: string): string | null {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
return 'Please enter a valid number';
}
if (parsedValue < 1 || parsedValue > 99) {
return `Value must be between 1 and 99 (you entered ${parsedValue})`;
}
return null;
}
export function buildTerminalWidthItems(
selectedOption: FlexMode,
compactThreshold: number
): ListEntry<FlexMode>[] {
return [
{
value: 'full',
label: 'Full width always',
sublabel: selectedOption === 'full' ? '(active)' : undefined,
description: 'Uses the full terminal width minus 4 characters for terminal padding. If the auto-compact message appears, it may cause the line to wrap.\n\nNOTE: If /ide integration is enabled, it is not recommended to use this mode.'
},
{
value: 'full-minus-40',
label: 'Full width minus 40',
sublabel: selectedOption === 'full-minus-40' ? '(active)' : '(default)',
description: 'Leaves a gap to the right of the status line to accommodate the auto-compact message. This prevents wrapping but may leave unused space. This limitation exists because we cannot detect when the message will appear.'
},
{
value: 'full-until-compact',
label: 'Full width until compact',
sublabel: selectedOption === 'full-until-compact'
? `(threshold ${compactThreshold}%, active)`
: `(threshold ${compactThreshold}%)`,
description: `Dynamically adjusts width based on context usage. When context reaches ${compactThreshold}%, it switches to leaving space for the auto-compact message.\n\nNOTE: If /ide integration is enabled, it is not recommended to use this mode.`
}
];
}
export interface TerminalWidthMenuProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
onBack: () => void;
}
export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({ settings, onUpdate, onBack }) => {
export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({
settings,
onUpdate,
onBack
}) => {
const [selectedOption, setSelectedOption] = useState<FlexMode>(settings.flexMode);
const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold);
const [editingThreshold, setEditingThreshold] = useState(false);
const [thresholdInput, setThresholdInput] = useState(String(settings.compactThreshold));
const [validationError, setValidationError] = useState<string | null>(null);
// For manual navigation: 0-2 for options, 3 for back
const [selectedIndex, setSelectedIndex] = useState(() => {
const options: FlexMode[] = ['full', 'full-minus-40', 'full-until-compact'];
return options.indexOf(settings.flexMode);
});
const options: FlexMode[] = ['full', 'full-minus-40', 'full-until-compact'];
useInput((input, key) => {
if (editingThreshold) {
if (key.return) {
const value = parseInt(thresholdInput, 10);
if (isNaN(value)) {
setValidationError('Please enter a valid number');
} else if (value < 1 || value > 99) {
setValidationError(`Value must be between 1 and 99 (you entered ${value})`);
const error = validateCompactThresholdInput(thresholdInput);
if (error) {
setValidationError(error);
} else {
const value = parseInt(thresholdInput, 10);
setCompactThreshold(value);
// Update settings with both flexMode and the new threshold
const updatedSettings = {
...settings,
flexMode: selectedOption,
@@ -66,59 +117,14 @@ export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({ settings,
setValidationError(null);
}
}
} else {
if (key.escape) {
onBack();
} else if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(3, selectedIndex + 1)); // 0-2 for options, 3 for back
} else if (key.return) {
if (selectedIndex === 3) {
onBack();
} else if (selectedIndex >= 0 && selectedIndex < options.length) {
const mode = options[selectedIndex];
if (mode) {
setSelectedOption(mode);
return;
}
// Update settings
const updatedSettings = {
...settings,
flexMode: mode,
compactThreshold: compactThreshold
};
onUpdate(updatedSettings);
if (mode === 'full-until-compact') {
// Prompt for threshold editing
setEditingThreshold(true);
}
}
}
}
if (key.escape) {
onBack();
}
});
const optionDetails = [
{
value: 'full' as FlexMode,
label: 'Full width always',
description: 'Uses the full terminal width minus 4 characters for terminal padding. If the auto-compact message appears, it may cause the line to wrap.\n\nNOTE: If /ide integration is enabled, it\'s not recommended to use this mode.'
},
{
value: 'full-minus-40' as FlexMode,
label: 'Full width minus 40 (default)',
description: 'Leaves a gap to the right of the status line to accommodate the auto-compact message. This prevents wrapping but may leave unused space. This limitation exists because we cannot detect when the message will appear.'
},
{
value: 'full-until-compact' as FlexMode,
label: 'Full width until compact',
description: `Dynamically adjusts width based on context usage. When context reaches ${compactThreshold}%, it switches to leaving space for the auto-compact message.\n\nNOTE: If /ide integration is enabled, it's not recommended to use this mode.`
}
];
const currentOption = selectedIndex < 3 ? optionDetails[selectedIndex] : null;
return (
<Box flexDirection='column'>
<Text bold>Terminal Width</Text>
@@ -140,38 +146,31 @@ export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({ settings,
)}
</Box>
) : (
<>
<Box marginTop={1} flexDirection='column'>
{optionDetails.map((opt, index) => (
<Box key={opt.value}>
<Text color={selectedIndex === index ? 'green' : undefined}>
{selectedIndex === index ? '▶ ' : ' '}
{opt.label}
{opt.value === selectedOption ? ' ✓' : ''}
</Text>
</Box>
))}
<List
marginTop={1}
items={buildTerminalWidthItems(selectedOption, compactThreshold)}
initialSelection={getTerminalWidthSelectionIndex(selectedOption)}
onSelect={(value) => {
if (value === 'back') {
onBack();
return;
}
<Box marginTop={1}>
<Text color={selectedIndex === 3 ? 'green' : undefined}>
{selectedIndex === 3 ? '▶ ' : ' '}
Back
</Text>
</Box>
</Box>
setSelectedOption(value);
{currentOption && (
<Box marginTop={1} marginBottom={1} borderStyle='round' borderColor='dim' paddingX={1}>
<Box flexDirection='column'>
<Text>
<Text color='yellow'>{currentOption.label}</Text>
{currentOption.value === 'full-until-compact' && ` | Current threshold: ${compactThreshold}%`}
</Text>
<Text dimColor wrap='wrap'>{currentOption.description}</Text>
</Box>
</Box>
)}
</>
const updatedSettings = {
...settings,
flexMode: value,
compactThreshold
};
onUpdate(updatedSettings);
if (value === 'full-until-compact') {
setEditingThreshold(true);
}
}}
showBackButton={true}
/>
)}
</Box>
);
@@ -0,0 +1,135 @@
import { render } from 'ink';
import { PassThrough } from 'node:stream';
import React from 'react';
import stripAnsi from 'strip-ansi';
import {
describe,
expect,
it,
vi
} from 'vitest';
import { InstallMenu } from '../InstallMenu';
class MockTtyStream extends PassThrough {
isTTY = true;
columns = 120;
rows = 40;
setRawMode() {
return this;
}
ref() {
return this;
}
unref() {
return this;
}
}
interface CapturedWriteStream extends NodeJS.WriteStream { getOutput: () => string }
function createMockStdin(): NodeJS.ReadStream {
return new MockTtyStream() as unknown as NodeJS.ReadStream;
}
function createMockStdout(): CapturedWriteStream {
const stream = new MockTtyStream();
const chunks: string[] = [];
stream.on('data', (chunk: Buffer | string) => {
chunks.push(chunk.toString());
});
return Object.assign(stream as unknown as NodeJS.WriteStream, {
getOutput() {
return stripAnsi(chunks.join(''));
}
});
}
function flushInk() {
return new Promise((resolve) => {
setTimeout(resolve, 25);
});
}
describe('InstallMenu', () => {
it('calls onCancel when escape is pressed', async () => {
const stdin = createMockStdin();
const stdout = createMockStdout();
const stderr = createMockStdout();
const onCancel = vi.fn();
const instance = render(
React.createElement(InstallMenu, {
bunxAvailable: true,
existingStatusLine: null,
onSelectNpx: vi.fn(),
onSelectBunx: vi.fn(),
onCancel
}),
{
stdin,
stdout,
stderr,
debug: true,
exitOnCtrlC: false,
patchConsole: false
}
);
try {
await flushInk();
stdin.write('\u001B');
await flushInk();
expect(onCancel).toHaveBeenCalledTimes(1);
} finally {
instance.unmount();
instance.cleanup();
stdin.destroy();
stdout.destroy();
stderr.destroy();
}
});
it('respects the provided initial selection', async () => {
const stdin = createMockStdin();
const stdout = createMockStdout();
const stderr = createMockStdout();
const instance = render(
React.createElement(InstallMenu, {
bunxAvailable: true,
existingStatusLine: null,
onSelectNpx: vi.fn(),
onSelectBunx: vi.fn(),
onCancel: vi.fn(),
initialSelection: 1
}),
{
stdin,
stdout,
stderr,
debug: true,
exitOnCtrlC: false,
patchConsole: false
}
);
try {
await flushInk();
expect(stdout.getOutput()).toContain('▶ bunx - Bun Package Execute');
expect(stdout.getOutput()).not.toContain('▶ npx - Node Package Execute');
} finally {
instance.unmount();
instance.cleanup();
stdin.destroy();
stdout.destroy();
stderr.destroy();
}
});
});
@@ -0,0 +1,67 @@
import {
describe,
expect,
it
} from 'vitest';
import { DEFAULT_SETTINGS } from '../../../types/Settings';
import {
buildPowerlineSetupMenuItems,
getCapDisplay,
getSeparatorDisplay,
getThemeDisplay
} from '../PowerlineSetup';
describe('PowerlineSetup helpers', () => {
it('formats separator, cap, and theme display values', () => {
const config = {
...DEFAULT_SETTINGS.powerline,
enabled: true,
separators: ['\uE0B4'],
startCaps: ['\uE0B2'],
endCaps: ['\uE0B0'],
theme: 'gruvbox'
};
expect(getSeparatorDisplay(config)).toBe('\uE0B4 - Round Right');
expect(getCapDisplay(config, 'start')).toBe('\uE0B2 - Triangle');
expect(getCapDisplay(config, 'end')).toBe('\uE0B0 - Triangle');
expect(getThemeDisplay(config)).toBe('Gruvbox');
});
it('builds powerline setup items with disabled states and sublabels', () => {
const disabledItems = buildPowerlineSetupMenuItems({
...DEFAULT_SETTINGS.powerline,
enabled: false
});
expect(disabledItems.every(item => item.disabled)).toBe(true);
const enabledItems = buildPowerlineSetupMenuItems({
...DEFAULT_SETTINGS.powerline,
enabled: true,
separators: ['\uE0B0', '\uE0B4'],
startCaps: [],
endCaps: ['\uE0BC'],
theme: undefined
});
expect(enabledItems[0]).toMatchObject({
label: 'Separator ',
sublabel: '(multiple)',
disabled: false
});
expect(enabledItems[1]).toMatchObject({
label: 'Start Cap ',
sublabel: '(none)'
});
expect(enabledItems[2]).toMatchObject({
label: 'End Cap ',
sublabel: '(\uE0BC - Diagonal)'
});
expect(enabledItems[3]).toMatchObject({
label: 'Themes ',
sublabel: '(Custom)'
});
});
});
@@ -0,0 +1,160 @@
import { render } from 'ink';
import { PassThrough } from 'node:stream';
import React from 'react';
import {
afterEach,
describe,
expect,
it,
vi
} from 'vitest';
import { DEFAULT_SETTINGS } from '../../../types/Settings';
import { getPowerlineThemes } from '../../../utils/colors';
import {
PowerlineThemeSelector,
applyCustomPowerlineTheme,
buildPowerlineThemeItems,
type PowerlineThemeSelectorProps
} from '../PowerlineThemeSelector';
class MockTtyStream extends PassThrough {
isTTY = true;
columns = 120;
rows = 40;
setRawMode() {
return this;
}
ref() {
return this;
}
unref() {
return this;
}
}
function createMockStdin(): NodeJS.ReadStream {
return new MockTtyStream() as unknown as NodeJS.ReadStream;
}
function createMockStdout(): NodeJS.WriteStream {
return new MockTtyStream() as unknown as NodeJS.WriteStream;
}
function flushInk() {
return new Promise((resolve) => {
setTimeout(resolve, 25);
});
}
describe('PowerlineThemeSelector helpers', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('builds powerline theme list items with original theme sublabels', () => {
const items = buildPowerlineThemeItems(['gruvbox', 'onedark'], 'onedark');
expect(items).toHaveLength(2);
expect(items[0]).toMatchObject({
label: 'Gruvbox',
value: 'gruvbox'
});
expect(items[1]).toMatchObject({
label: 'One Dark',
sublabel: '(original)',
value: 'onedark'
});
});
it('copies a built-in theme into widget colors and switches to custom mode', () => {
const settings = {
...DEFAULT_SETTINGS,
colorLevel: 2 as const,
powerline: {
...DEFAULT_SETTINGS.powerline,
theme: 'gruvbox'
}
};
const updatedSettings = applyCustomPowerlineTheme(settings, 'gruvbox');
expect(updatedSettings).not.toBeNull();
expect(updatedSettings?.powerline.theme).toBe('custom');
expect(updatedSettings?.lines[0]?.[0]).toMatchObject({
color: 'ansi256:16',
backgroundColor: 'ansi256:167'
});
expect(updatedSettings?.lines[0]?.[1]).toEqual(settings.lines[0]?.[1]);
expect(updatedSettings?.lines[0]?.[2]).toMatchObject({
color: 'ansi256:235',
backgroundColor: 'ansi256:214'
});
});
it('returns null when the requested theme cannot be customized', () => {
expect(applyCustomPowerlineTheme(DEFAULT_SETTINGS, 'custom')).toBeNull();
expect(applyCustomPowerlineTheme(DEFAULT_SETTINGS, 'missing-theme')).toBeNull();
});
it('previews the highlighted theme once without triggering update-depth warnings', async () => {
const themes = getPowerlineThemes();
expect(themes.length).toBeGreaterThan(1);
const stdin = createMockStdin();
const stdout = createMockStdout();
const stderr = createMockStdout();
const onUpdate = vi.fn<PowerlineThemeSelectorProps['onUpdate']>();
const onBack = vi.fn();
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
const instance = render(
React.createElement(PowerlineThemeSelector, {
settings: {
...DEFAULT_SETTINGS,
powerline: {
...DEFAULT_SETTINGS.powerline,
enabled: true,
theme: themes[0]
}
},
onUpdate,
onBack
}),
{
stdin,
stdout,
stderr,
debug: true,
exitOnCtrlC: false,
patchConsole: false
}
);
try {
await flushInk();
expect(onUpdate).not.toHaveBeenCalled();
stdin.write('\u001B[B');
await flushInk();
expect(onUpdate).toHaveBeenCalledTimes(1);
expect(onUpdate.mock.calls[0]?.[0]?.powerline.theme).toBe(themes[1]);
const maximumUpdateDepthWarnings = consoleErrorSpy.mock.calls.filter((call) => {
return call.some(arg => typeof arg === 'string' && arg.includes('Maximum update depth exceeded'));
});
expect(maximumUpdateDepthWarnings).toHaveLength(0);
} finally {
instance.unmount();
instance.cleanup();
stdin.destroy();
stdout.destroy();
stderr.destroy();
}
});
});
@@ -0,0 +1,44 @@
import {
describe,
expect,
it
} from 'vitest';
import {
buildTerminalOptionsItems,
getNextColorLevel,
shouldWarnOnColorLevelChange
} from '../TerminalOptionsMenu';
describe('TerminalOptionsMenu helpers', () => {
it('cycles color levels in order', () => {
expect(getNextColorLevel(0)).toBe(1);
expect(getNextColorLevel(1)).toBe(2);
expect(getNextColorLevel(2)).toBe(3);
expect(getNextColorLevel(3)).toBe(0);
});
it('warns only when custom colors would be lost', () => {
expect(shouldWarnOnColorLevelChange(2, 3, true)).toBe(true);
expect(shouldWarnOnColorLevelChange(3, 0, true)).toBe(true);
expect(shouldWarnOnColorLevelChange(2, 2, true)).toBe(false);
expect(shouldWarnOnColorLevelChange(1, 2, true)).toBe(false);
expect(shouldWarnOnColorLevelChange(3, 0, false)).toBe(false);
});
it('builds terminal options list items with the current color level label', () => {
const items = buildTerminalOptionsItems(2);
expect(items).toHaveLength(2);
expect(items[0]).toMatchObject({
label: '◱ Terminal Width',
value: 'width'
});
expect(items[1]).toMatchObject({
label: '▓ Color Level',
sublabel: '(256 Color (default))',
value: 'colorLevel'
});
expect(items[1]?.description).toContain('Truecolor');
});
});
@@ -0,0 +1,169 @@
import { render } from 'ink';
import { PassThrough } from 'node:stream';
import React from 'react';
import {
afterEach,
describe,
expect,
it,
vi
} from 'vitest';
import { DEFAULT_SETTINGS } from '../../../types/Settings';
import {
TerminalWidthMenu,
buildTerminalWidthItems,
getTerminalWidthSelectionIndex,
validateCompactThresholdInput
} from '../TerminalWidthMenu';
class MockTtyStream extends PassThrough {
isTTY = true;
columns = 120;
rows = 40;
setRawMode() {
return this;
}
ref() {
return this;
}
unref() {
return this;
}
}
interface CapturedWriteStream extends NodeJS.WriteStream {
clearOutput: () => void;
getOutput: () => string;
}
function createMockStdin(): NodeJS.ReadStream {
return new MockTtyStream() as unknown as NodeJS.ReadStream;
}
function createMockStdout(): CapturedWriteStream {
const stream = new MockTtyStream();
const chunks: string[] = [];
stream.on('data', (chunk: Buffer | string) => {
chunks.push(chunk.toString());
});
return Object.assign(stream as unknown as NodeJS.WriteStream, {
clearOutput() {
chunks.length = 0;
},
getOutput() {
return chunks.join('');
}
});
}
function flushInk() {
return new Promise((resolve) => {
setTimeout(resolve, 25);
});
}
describe('TerminalWidthMenu helpers', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('validates compact threshold input', () => {
expect(validateCompactThresholdInput('')).toBe('Please enter a valid number');
expect(validateCompactThresholdInput('0')).toBe('Value must be between 1 and 99 (you entered 0)');
expect(validateCompactThresholdInput('100')).toBe('Value must be between 1 and 99 (you entered 100)');
expect(validateCompactThresholdInput('42')).toBeNull();
});
it('builds terminal width menu items with active and threshold sublabels', () => {
const items = buildTerminalWidthItems('full-until-compact', 60);
expect(items).toHaveLength(3);
expect(items[0]).toMatchObject({
label: 'Full width always',
value: 'full'
});
expect(items[1]).toMatchObject({
label: 'Full width minus 40',
sublabel: '(default)',
value: 'full-minus-40'
});
expect(items[2]).toMatchObject({
label: 'Full width until compact',
sublabel: '(threshold 60%, active)',
value: 'full-until-compact'
});
expect(items[2]?.description).toContain('60%');
});
it('returns the current option index for list selection', () => {
expect(getTerminalWidthSelectionIndex('full')).toBe(0);
expect(getTerminalWidthSelectionIndex('full-minus-40')).toBe(1);
expect(getTerminalWidthSelectionIndex('full-until-compact')).toBe(2);
});
it('keeps full-until-compact selected after confirming the threshold prompt', async () => {
const stdin = createMockStdin();
const stdout = createMockStdout();
const stderr = createMockStdout();
const onUpdate = vi.fn();
const onBack = vi.fn();
const instance = render(
React.createElement(TerminalWidthMenu, {
settings: {
...DEFAULT_SETTINGS,
flexMode: 'full',
compactThreshold: 60
},
onUpdate,
onBack
}),
{
stdin,
stdout,
stderr,
debug: true,
exitOnCtrlC: false,
patchConsole: false
}
);
try {
await flushInk();
stdin.write('\u001B[B');
await flushInk();
stdin.write('\u001B[B');
await flushInk();
stdin.write('\r');
await flushInk();
expect(stdout.getOutput()).toContain('Enter compact threshold (1-99):');
stdout.clearOutput();
stdin.write('\r');
await flushInk();
expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({
flexMode: 'full-until-compact',
compactThreshold: 60
}));
const output = stdout.getOutput();
expect(output).toContain('▶ Full width until compact');
expect(output).not.toContain('▶ Full width always');
} finally {
instance.unmount();
instance.cleanup();
stdin.destroy();
stdout.destroy();
stderr.destroy();
}
});
});
@@ -166,7 +166,7 @@ describe('items-editor input handlers', () => {
setMoveMode: vi.fn(),
setShowClearConfirm: vi.fn(),
openWidgetPicker: vi.fn(),
getVisibleCustomKeybinds: widgetImpl => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds() : [],
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
setCustomEditorWidget: vi.fn()
});
@@ -192,7 +192,7 @@ describe('items-editor input handlers', () => {
setMoveMode: vi.fn(),
setShowClearConfirm: vi.fn(),
openWidgetPicker: vi.fn(),
getVisibleCustomKeybinds: widgetImpl => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds() : [],
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
setCustomEditorWidget: vi.fn()
});
@@ -218,11 +218,68 @@ describe('items-editor input handlers', () => {
setMoveMode: vi.fn(),
setShowClearConfirm: vi.fn(),
openWidgetPicker: vi.fn(),
getVisibleCustomKeybinds: widgetImpl => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds() : [],
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
setCustomEditorWidget: vi.fn()
});
const updated = onUpdate.mock.calls[0]?.[0] as WidgetItem[] | undefined;
expect(updated?.[0]?.metadata?.display).toBe('progress');
});
it('uses v to cycle skills widget mode', () => {
const widgets: WidgetItem[] = [
{ id: '1', type: 'skills' }
];
const onUpdate = vi.fn();
handleNormalInputMode({
input: 'v',
key: {},
widgets,
selectedIndex: 0,
separatorChars: ['|', '-'],
onBack: vi.fn(),
onUpdate,
setSelectedIndex: vi.fn(),
setMoveMode: vi.fn(),
setShowClearConfirm: vi.fn(),
openWidgetPicker: vi.fn(),
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
setCustomEditorWidget: vi.fn()
});
const updated = onUpdate.mock.calls[0]?.[0] as WidgetItem[] | undefined;
expect(updated?.[0]?.metadata?.mode).toBe('count');
});
it('opens custom editor for skills list limit action', () => {
const widgets: WidgetItem[] = [
{ id: '1', type: 'skills', metadata: { mode: 'list' } }
];
const onUpdate = vi.fn();
const setCustomEditorWidget = vi.fn();
handleNormalInputMode({
input: 'l',
key: {},
widgets,
selectedIndex: 0,
separatorChars: ['|', '-'],
onBack: vi.fn(),
onUpdate,
setSelectedIndex: vi.fn(),
setMoveMode: vi.fn(),
setShowClearConfirm: vi.fn(),
openWidgetPicker: vi.fn(),
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
setCustomEditorWidget
});
expect(onUpdate).not.toHaveBeenCalled();
const customEditorState = setCustomEditorWidget.mock.calls[0]?.[0] as
| { action?: string; widget?: WidgetItem }
| undefined;
expect(customEditorState?.action).toBe('edit-list-limit');
expect(customEditorState?.widget?.type).toBe('skills');
});
});
@@ -338,7 +338,7 @@ export interface HandleNormalInputModeArgs {
setMoveMode: (moveMode: boolean) => void;
setShowClearConfirm: (show: boolean) => void;
openWidgetPicker: (action: WidgetPickerAction) => void;
getVisibleCustomKeybinds: (widgetImpl: Widget, widget: WidgetItem) => CustomKeybind[];
getCustomKeybindsForWidget: (widgetImpl: Widget, widget: WidgetItem) => CustomKeybind[];
setCustomEditorWidget: (state: CustomEditorWidgetState | null) => void;
}
@@ -354,7 +354,7 @@ export function handleNormalInputMode({
setMoveMode,
setShowClearConfirm,
openWidgetPicker,
getVisibleCustomKeybinds,
getCustomKeybindsForWidget,
setCustomEditorWidget
}: HandleNormalInputModeArgs): void {
if (key.upArrow && widgets.length > 0) {
@@ -436,7 +436,7 @@ export function handleNormalInputMode({
return;
}
const customKeybinds = getVisibleCustomKeybinds(widgetImpl, currentWidget);
const customKeybinds = getCustomKeybindsForWidget(widgetImpl, currentWidget);
const matchedKeybind = customKeybinds.find(kb => kb.key === input);
if (matchedKeybind && !key.ctrl) {
+1
View File
@@ -1,4 +1,5 @@
export interface ClaudeSettings {
effortLevel?: 'low' | 'medium' | 'high' | 'max';
permissions?: {
allow?: string[];
deny?: string[];
+6 -2
View File
@@ -1,4 +1,7 @@
import type { BlockMetrics } from '../types';
import type {
BlockMetrics,
SkillsMetrics
} from '../types';
import type { SpeedMetrics } from './SpeedMetrics';
import type { StatusJSON } from './StatusJSON';
@@ -13,7 +16,7 @@ export interface RenderUsageData {
extraUsageLimit?: number;
extraUsageUsed?: number;
extraUsageUtilization?: number;
error?: 'no-credentials' | 'timeout' | 'api-error' | 'parse-error';
error?: 'no-credentials' | 'timeout' | 'rate-limited' | 'api-error' | 'parse-error';
}
export interface RenderContext {
@@ -24,6 +27,7 @@ export interface RenderContext {
usageData?: RenderUsageData | null;
sessionDuration?: string | null;
blockMetrics?: BlockMetrics | null;
skillsMetrics?: SkillsMetrics | null;
terminalWidth?: number | null;
isPreview?: boolean;
lineIndex?: number; // Index of the current line being rendered (for theme cycling)
+12
View File
@@ -0,0 +1,12 @@
export interface SkillInvocation {
timestamp: string;
session_id: string;
skill: string;
source: string;
}
export interface SkillsMetrics {
totalInvocations: number;
uniqueSkills: string[];
lastSkill: string | null;
}
+2 -1
View File
@@ -54,7 +54,8 @@ export const StatusJSONSchema = z.looseObject({
]).nullable().optional(),
used_percentage: CoercedNumberSchema.nullable().optional(),
remaining_percentage: CoercedNumberSchema.nullable().optional()
}).nullable().optional()
}).nullable().optional(),
vim: z.object({ mode: z.string().optional() }).nullable().optional()
});
export type StatusJSON = z.infer<typeof StatusJSONSchema>;
+1 -1
View File
@@ -37,7 +37,7 @@ export interface Widget {
getCategory(): string;
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay;
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null;
getCustomKeybinds?(): CustomKeybind[];
getCustomKeybinds?(item?: WidgetItem): CustomKeybind[];
renderEditor?(props: WidgetEditorProps): React.ReactElement | null;
supportsRawValue(): boolean;
supportsColors(item: WidgetItem): boolean;
+11
View File
@@ -58,4 +58,15 @@ describe('StatusJSONSchema numeric coercion', () => {
expect(result.success).toBe(false);
});
it('accepts null vim payloads', () => {
const result = StatusJSONSchema.safeParse({ vim: null });
expect(result.success).toBe(true);
if (!result.success) {
return;
}
expect(result.data.vim).toBeNull();
});
});
+2 -1
View File
@@ -18,4 +18,5 @@ export type { PowerlineFontStatus } from './PowerlineFontStatus';
export type { ClaudeSettings } from './ClaudeSettings';
export type { ColorEntry } from './ColorEntry';
export type { BlockMetrics } from './BlockMetrics';
export type { SpeedMetrics } from './SpeedMetrics';
export type { SpeedMetrics } from './SpeedMetrics';
export type { SkillInvocation, SkillsMetrics } from './SkillsMetrics';
@@ -11,6 +11,7 @@ import {
vi
} from 'vitest';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import {
CCSTATUSLINE_COMMANDS,
getClaudeSettingsPath,
@@ -143,6 +144,36 @@ describe('buildCommand via installStatusLine', () => {
await installStatusLine(true);
expect(readInstalledCommand()).toBe(`${CCSTATUSLINE_COMMANDS.BUNX} --config '/my path/settings.json'`);
});
it('should sync hooks on install when settings include hook-enabled widgets', async () => {
const configPath = path.join(testClaudeConfigDir, 'ccstatusline-settings.json');
initConfigPath(configPath);
const settingsWithSkills = {
...DEFAULT_SETTINGS,
lines: [[{ id: 'skills-1', type: 'skills' }], [], []]
};
fs.writeFileSync(configPath, JSON.stringify(settingsWithSkills, null, 2), 'utf-8');
await installStatusLine(false);
const installedCommand = `${CCSTATUSLINE_COMMANDS.NPM} --config ${configPath}`;
const claudeSettings = await loadClaudeSettings();
expect(claudeSettings.statusLine?.command).toBe(installedCommand);
const hooks = (claudeSettings.hooks ?? {}) as Record<string, unknown[]>;
expect(hooks.PreToolUse).toEqual([
{
_tag: 'ccstatusline-managed',
matcher: 'Skill',
hooks: [{ type: 'command', command: `${installedCommand} --hook` }]
}
]);
expect(hooks.UserPromptSubmit).toEqual([
{
_tag: 'ccstatusline-managed',
hooks: [{ type: 'command', command: `${installedCommand} --hook` }]
}
]);
});
});
describe('backup and error handling behavior', () => {
@@ -257,6 +288,52 @@ describe('backup and error handling behavior', () => {
}
});
it('uninstallStatusLine should remove all managed hooks', async () => {
writeRawClaudeSettings(JSON.stringify({
statusLine: {
type: 'command',
command: CCSTATUSLINE_COMMANDS.NPM,
padding: 0
},
hooks: {
PreToolUse: [
{
_tag: 'ccstatusline-managed',
matcher: 'Skill',
hooks: [{ type: 'command', command: `${CCSTATUSLINE_COMMANDS.NPM} --hook` }]
},
{
matcher: 'Other',
hooks: [{ type: 'command', command: 'keep-me' }]
}
],
UserPromptSubmit: [
{
_tag: 'ccstatusline-managed',
hooks: [{ type: 'command', command: `${CCSTATUSLINE_COMMANDS.NPM} --hook` }]
}
]
}
}));
await uninstallStatusLine();
const settingsPath = getClaudeSettingsPath();
const updated = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as {
statusLine?: unknown;
hooks?: Record<string, unknown[]>;
};
expect(updated.statusLine).toBeUndefined();
expect(updated.hooks).toEqual({
PreToolUse: [
{
matcher: 'Other',
hooks: [{ type: 'command', command: 'keep-me' }]
}
]
});
});
it('getExistingStatusLine should return null when settings cannot be loaded', async () => {
writeRawClaudeSettings('{ invalid json');
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
+11
View File
@@ -19,6 +19,7 @@ import {
} from '../../types/Settings';
const MOCK_HOME_DIR = '/tmp/ccstatusline-config-test-home';
const ORIGINAL_CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR;
let loadSettings: () => Promise<Settings>;
let saveSettings: (settings: Settings) => Promise<void>;
@@ -34,6 +35,10 @@ function getSettingsPaths(): { configDir: string; settingsPath: string; backupPa
};
}
function getClaudeConfigDir(): string {
return path.join(MOCK_HOME_DIR, '.claude');
}
describe('config utilities', () => {
beforeAll(async () => {
const configModule = await import('../config');
@@ -44,6 +49,7 @@ describe('config utilities', () => {
beforeEach(() => {
fs.rmSync(MOCK_HOME_DIR, { recursive: true, force: true });
process.env.CLAUDE_CONFIG_DIR = getClaudeConfigDir();
const { settingsPath } = getSettingsPaths();
initConfigPath(settingsPath);
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
@@ -55,6 +61,11 @@ describe('config utilities', () => {
afterAll(() => {
fs.rmSync(MOCK_HOME_DIR, { recursive: true, force: true });
if (ORIGINAL_CLAUDE_CONFIG_DIR === undefined) {
delete process.env.CLAUDE_CONFIG_DIR;
} else {
process.env.CLAUDE_CONFIG_DIR = ORIGINAL_CLAUDE_CONFIG_DIR;
}
initConfigPath();
});
+79
View File
@@ -0,0 +1,79 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
afterAll,
afterEach,
beforeEach,
describe,
expect,
it
} from 'vitest';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import { syncWidgetHooks } from '../hooks';
const ORIGINAL_CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR;
let testClaudeConfigDir = '';
function getClaudeSettingsPath(): string {
return path.join(testClaudeConfigDir, 'settings.json');
}
describe('syncWidgetHooks', () => {
beforeEach(() => {
testClaudeConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-hooks-'));
process.env.CLAUDE_CONFIG_DIR = testClaudeConfigDir;
});
afterEach(() => {
if (testClaudeConfigDir) {
fs.rmSync(testClaudeConfigDir, { recursive: true, force: true });
}
});
afterAll(() => {
if (ORIGINAL_CLAUDE_CONFIG_DIR === undefined) {
delete process.env.CLAUDE_CONFIG_DIR;
} else {
process.env.CLAUDE_CONFIG_DIR = ORIGINAL_CLAUDE_CONFIG_DIR;
}
});
it('removes managed hooks and persists cleanup when status line is unset', async () => {
const settingsPath = getClaudeSettingsPath();
fs.writeFileSync(settingsPath, JSON.stringify({
hooks: {
PreToolUse: [
{
_tag: 'ccstatusline-managed',
matcher: 'Skill',
hooks: [{ type: 'command', command: 'old-command --hook' }]
},
{
matcher: 'Other',
hooks: [{ type: 'command', command: 'keep-command' }]
}
],
UserPromptSubmit: [
{
_tag: 'ccstatusline-managed',
hooks: [{ type: 'command', command: 'old-command --hook' }]
}
]
}
}, null, 2), 'utf-8');
await syncWidgetHooks(DEFAULT_SETTINGS);
const saved = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { hooks?: Record<string, unknown[]> };
expect(saved.hooks).toEqual({
PreToolUse: [
{
matcher: 'Other',
hooks: [{ type: 'command', command: 'keep-command' }]
}
]
});
});
});
+49
View File
@@ -0,0 +1,49 @@
import {
describe,
expect,
it
} from 'vitest';
import {
buildIdeFileUrl,
encodeGitRefForUrlPath,
parseGitHubBaseUrl
} from '../hyperlink';
describe('parseGitHubBaseUrl', () => {
it('supports scp-style SSH remotes', () => {
expect(parseGitHubBaseUrl('git@github.com:owner/repo.git')).toBe('https://github.com/owner/repo');
});
it('supports ssh URL remotes', () => {
expect(parseGitHubBaseUrl('ssh://git@github.com/owner/repo.git')).toBe('https://github.com/owner/repo');
});
it('supports credentialed HTTPS remotes', () => {
expect(parseGitHubBaseUrl('https://token@github.com/owner/repo.git')).toBe('https://github.com/owner/repo');
});
it('rejects non-GitHub remotes', () => {
expect(parseGitHubBaseUrl('https://gitlab.com/owner/repo.git')).toBeNull();
});
});
describe('encodeGitRefForUrlPath', () => {
it('encodes reserved characters while preserving branch separators', () => {
expect(encodeGitRefForUrlPath('feature/issue#1')).toBe('feature/issue%231');
});
});
describe('buildIdeFileUrl', () => {
it('builds encoded IDE links for POSIX paths', () => {
expect(buildIdeFileUrl('/Users/example/my repo#1', 'cursor')).toBe('cursor://file/Users/example/my%20repo%231');
});
it('builds IDE links for Windows drive-letter paths', () => {
expect(buildIdeFileUrl('C:/Work/my repo#1', 'vscode')).toBe('vscode://file/C:/Work/my%20repo%231');
});
it('builds IDE links for UNC paths', () => {
expect(buildIdeFileUrl('\\\\server\\share\\my repo', 'cursor')).toBe('cursor://file//server/share/my%20repo');
});
});
+59
View File
@@ -0,0 +1,59 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi
} from 'vitest';
import {
getSkillsFilePath,
getSkillsMetrics
} from '../skills';
let testHomeDir = '';
function writeSkillsLog(sessionId: string, lines: string[]): void {
const skillsPath = getSkillsFilePath(sessionId);
fs.mkdirSync(path.dirname(skillsPath), { recursive: true });
fs.writeFileSync(skillsPath, lines.join('\n'), 'utf-8');
}
describe('skills metrics', () => {
beforeEach(() => {
testHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-home-'));
vi.spyOn(os, 'homedir').mockReturnValue(testHomeDir);
});
afterEach(() => {
vi.restoreAllMocks();
if (testHomeDir) {
fs.rmSync(testHomeDir, { recursive: true, force: true });
}
});
it('uses ~/.cache/ccstatusline/skills path for skill logs', () => {
expect(getSkillsFilePath('session-1')).toBe(
path.join(testHomeDir, '.cache', 'ccstatusline', 'skills', 'skills-session-1.jsonl')
);
});
it('returns total, unique (most-recent-first), and last skill from a valid log', () => {
writeSkillsLog('session-1', [
JSON.stringify({ skill: 'commit', session_id: 'session-1' }),
JSON.stringify({ skill: 'review-pr', session_id: 'session-1' }),
JSON.stringify({ skill: 'lint', session_id: 'session-1' }),
JSON.stringify({ skill: 'commit', session_id: 'session-1' })
]);
expect(getSkillsMetrics('session-1')).toEqual({
totalInvocations: 4,
uniqueSkills: ['commit', 'lint', 'review-pr'],
lastSkill: 'commit'
});
});
});
+468 -106
View File
@@ -15,52 +15,36 @@ interface UsageProbeResult {
lockExists: boolean;
cacheExists: boolean;
requestCount: number;
proxyAgentConfigured: boolean;
requestHost: string | null;
lockContents: string | null;
}
describe('fetchUsageData error handling', () => {
it('preserves root errors within lock window and avoids locking on no-credentials', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-usage-test-'));
const probeScriptPath = path.join(tempRoot, 'probe-usage.mjs');
const usageModulePath = fileURLToPath(new URL('../usage.ts', import.meta.url));
interface TokenHome {
bin: string;
claudeConfig: string;
home: string;
}
const noCredentialsHome = path.join(tempRoot, 'home-no-credentials');
const apiErrorHome = path.join(tempRoot, 'home-api-error');
const apiErrorBin = path.join(tempRoot, 'bin-api-error');
const apiErrorClaudeConfig = path.join(tempRoot, 'claude-api-error');
const successHome = path.join(tempRoot, 'home-success');
const successBin = path.join(tempRoot, 'bin-success');
const successClaudeConfig = path.join(tempRoot, 'claude-success');
const securityScript = path.join(apiErrorBin, 'security');
const successSecurityScript = path.join(successBin, 'security');
const credentialsFile = path.join(apiErrorClaudeConfig, '.credentials.json');
const successCredentialsFile = path.join(successClaudeConfig, '.credentials.json');
const successResponseBody = JSON.stringify({
five_hour: {
utilization: 42,
resets_at: '2030-01-01T00:00:00.000Z'
},
seven_day: {
utilization: 17,
resets_at: '2030-01-07T00:00:00.000Z'
}
});
interface ProbeOptions {
claudeConfigDir?: string;
home: string;
httpsProxy?: string;
lowercaseHttpsProxy?: string;
mode?: 'error' | 'status' | 'success' | 'unexpected';
nowMs: number;
pathDir?: string;
responseBody?: string;
responseHeaders?: Record<string, string>;
statusCode?: number;
}
fs.mkdirSync(noCredentialsHome, { recursive: true });
fs.mkdirSync(apiErrorHome, { recursive: true });
fs.mkdirSync(apiErrorBin, { recursive: true });
fs.mkdirSync(apiErrorClaudeConfig, { recursive: true });
fs.mkdirSync(successHome, { recursive: true });
fs.mkdirSync(successBin, { recursive: true });
fs.mkdirSync(successClaudeConfig, { recursive: true });
function createProbeHarness() {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-usage-test-'));
const probeScriptPath = path.join(tempRoot, 'probe-usage.mjs');
const usageModulePath = fileURLToPath(new URL('../usage.ts', import.meta.url));
fs.writeFileSync(securityScript, '#!/bin/sh\necho \'{"claudeAiOauth":{"accessToken":"test-token"}}\'\n');
fs.chmodSync(securityScript, 0o755);
fs.writeFileSync(credentialsFile, JSON.stringify({ claudeAiOauth: { accessToken: 'test-token' } }));
fs.writeFileSync(successSecurityScript, '#!/bin/sh\necho \'{"claudeAiOauth":{"accessToken":"test-token"}}\'\n');
fs.chmodSync(successSecurityScript, 0o755);
fs.writeFileSync(successCredentialsFile, JSON.stringify({ claudeAiOauth: { accessToken: 'test-token' } }));
const probeScript = `
const probeScript = `
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
@@ -70,16 +54,24 @@ const require = createRequire(import.meta.url);
const https = require('https');
const mode = process.env.TEST_REQUEST_MODE || 'success';
const responseBody = process.env.TEST_RESPONSE_BODY || '';
const responseHeaders = JSON.parse(process.env.TEST_RESPONSE_HEADERS_JSON || '{}');
const statusCode = Number(process.env.TEST_STATUS_CODE || (mode === 'success' ? '200' : '500'));
let requestCount = 0;
let proxyAgentConfigured = false;
let requestHost = null;
https.request = (...args) => {
requestCount += 1;
const callback = args.find(value => typeof value === 'function');
const options = args.find(value => value && typeof value === 'object' && !Buffer.isBuffer(value));
proxyAgentConfigured = Boolean(options?.agent);
requestHost = options?.hostname ?? null;
const requestHandlers = new Map();
const responseHandlers = new Map();
const response = {
statusCode: mode === 'success' ? 200 : 500,
headers: responseHeaders,
statusCode,
setEncoding() {},
on(event, handler) {
const existing = responseHandlers.get(event) || [];
@@ -106,24 +98,28 @@ https.request = (...args) => {
return;
}
if (mode === 'success') {
if (callback) {
callback(response);
}
const dataHandlers = responseHandlers.get('data') || [];
for (const handler of dataHandlers) {
handler(Buffer.from(responseBody));
}
const endHandlers = responseHandlers.get('end') || [];
for (const handler of endHandlers) {
handler();
if (mode === 'unexpected') {
const handlers = requestHandlers.get('error') || [];
for (const handler of handlers) {
handler(new Error('unexpected request'));
}
return;
}
const handlers = requestHandlers.get('error') || [];
for (const handler of handlers) {
handler(new Error('unexpected request'));
if (callback) {
callback(response);
}
if (responseBody !== '') {
const dataHandlers = responseHandlers.get('data') || [];
for (const handler of dataHandlers) {
handler(responseBody);
}
}
const endHandlers = responseHandlers.get('end') || [];
for (const handler of endHandlers) {
handler();
}
}
};
@@ -145,63 +141,174 @@ process.stdout.write(JSON.stringify({
second,
lockExists: fs.existsSync(lockFile),
cacheExists: fs.existsSync(cacheFile),
requestCount
requestCount,
proxyAgentConfigured,
requestHost,
lockContents: fs.existsSync(lockFile) ? fs.readFileSync(lockFile, 'utf8') : null
}));
`;
try {
fs.writeFileSync(probeScriptPath, probeScript);
fs.writeFileSync(probeScriptPath, probeScript);
const noCredentialsOutput = execFileSync(process.execPath, [probeScriptPath], {
encoding: 'utf8',
env: {
...process.env,
HOME: noCredentialsHome,
PATH: '/nonexistent',
TEST_REQUEST_MODE: 'unexpected',
TEST_NOW_MS: '2200000000000'
}
function createEmptyHome(name: string): { home: string } {
const home = path.join(tempRoot, `home-${name}`);
fs.mkdirSync(home, { recursive: true });
return { home };
}
function createTokenHome(name: string): TokenHome {
const home = path.join(tempRoot, `home-${name}`);
const bin = path.join(tempRoot, `bin-${name}`);
const claudeConfig = path.join(tempRoot, `claude-${name}`);
const securityScript = path.join(bin, 'security');
const credentialsFile = path.join(claudeConfig, '.credentials.json');
fs.mkdirSync(home, { recursive: true });
fs.mkdirSync(bin, { recursive: true });
fs.mkdirSync(claudeConfig, { recursive: true });
fs.writeFileSync(securityScript, '#!/bin/sh\necho \'{"claudeAiOauth":{"accessToken":"test-token"}}\'\n');
fs.chmodSync(securityScript, 0o755);
fs.writeFileSync(credentialsFile, JSON.stringify({ claudeAiOauth: { accessToken: 'test-token' } }));
return {
bin,
claudeConfig,
home
};
}
function runProbe(options: ProbeOptions): UsageProbeResult {
const output = execFileSync(process.execPath, [probeScriptPath], {
encoding: 'utf8',
env: {
...process.env,
HOME: options.home,
PATH: options.pathDir ?? '/nonexistent',
TEST_NOW_MS: String(options.nowMs),
TEST_REQUEST_MODE: options.mode ?? 'success',
TEST_RESPONSE_BODY: options.responseBody ?? '',
TEST_RESPONSE_HEADERS_JSON: JSON.stringify(options.responseHeaders ?? {}),
TEST_STATUS_CODE: String(options.statusCode ?? (options.mode === 'success' ? 200 : 500)),
...(options.claudeConfigDir ? { CLAUDE_CONFIG_DIR: options.claudeConfigDir } : {}),
...(options.httpsProxy !== undefined ? { HTTPS_PROXY: options.httpsProxy } : {}),
...(options.lowercaseHttpsProxy !== undefined ? { https_proxy: options.lowercaseHttpsProxy } : {})
}
});
return JSON.parse(output) as UsageProbeResult;
}
function cleanup(): void {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
return {
cleanup,
createEmptyHome,
createTokenHome,
runProbe
};
}
function parseLockContents(lockContents: string | null): { blockedUntil: number; error?: string } | null {
return lockContents ? JSON.parse(lockContents) as { blockedUntil: number; error?: string } : null;
}
describe('fetchUsageData error handling', () => {
const nowMs = 2200000000000;
const successResponseBody = JSON.stringify({
five_hour: {
utilization: 42,
resets_at: '2030-01-01T00:00:00.000Z'
},
seven_day: {
utilization: 17,
resets_at: '2030-01-07T00:00:00.000Z'
}
});
const updatedSuccessResponseBody = JSON.stringify({
five_hour: {
utilization: 55,
resets_at: '2030-01-02T00:00:00.000Z'
},
seven_day: {
utilization: 21,
resets_at: '2030-01-08T00:00:00.000Z'
}
});
const rateLimitedResponseBody = JSON.stringify({
error: {
message: 'Rate limited. Please try again later.',
type: 'rate_limit_error'
}
});
it('preserves root errors within a process and keeps existing proxy and cache behavior', () => {
const harness = createProbeHarness();
try {
const noCredentialsHome = harness.createEmptyHome('no-credentials');
const apiErrorHome = harness.createTokenHome('api-error');
const successHome = harness.createTokenHome('success');
const invalidProxyHome = harness.createTokenHome('invalid-proxy');
const proxyHome = harness.createTokenHome('proxy');
const blankProxyHome = harness.createTokenHome('blank-proxy');
const lowercaseProxyHome = harness.createTokenHome('lowercase-proxy');
const noCredentialsResult = harness.runProbe({
home: noCredentialsHome.home,
mode: 'unexpected',
nowMs
});
const noCredentialsResult = JSON.parse(noCredentialsOutput) as UsageProbeResult;
expect(noCredentialsResult.first).toEqual({ error: 'no-credentials' });
expect(noCredentialsResult.second).toEqual({ error: 'no-credentials' });
expect(noCredentialsResult.lockExists).toBe(false);
expect(noCredentialsResult.cacheExists).toBe(false);
expect(noCredentialsResult.requestCount).toBe(0);
expect(noCredentialsResult.proxyAgentConfigured).toBe(false);
const apiErrorOutput = execFileSync(process.execPath, [probeScriptPath], {
encoding: 'utf8',
env: {
...process.env,
HOME: apiErrorHome,
PATH: apiErrorBin,
CLAUDE_CONFIG_DIR: apiErrorClaudeConfig,
TEST_REQUEST_MODE: 'error',
TEST_NOW_MS: '2200000000000'
}
const apiErrorResult = harness.runProbe({
claudeConfigDir: apiErrorHome.claudeConfig,
home: apiErrorHome.home,
mode: 'error',
nowMs,
pathDir: apiErrorHome.bin
});
const apiErrorResult = JSON.parse(apiErrorOutput) as UsageProbeResult;
expect(apiErrorResult.first).toEqual({ error: 'api-error' });
expect(apiErrorResult.second).toEqual({ error: 'api-error' });
expect(apiErrorResult.cacheExists).toBe(false);
expect(apiErrorResult.requestCount).toBe(1);
const successOutput = execFileSync(process.execPath, [probeScriptPath], {
encoding: 'utf8',
env: {
...process.env,
HOME: successHome,
PATH: successBin,
CLAUDE_CONFIG_DIR: successClaudeConfig,
TEST_REQUEST_MODE: 'success',
TEST_RESPONSE_BODY: successResponseBody,
TEST_NOW_MS: '2200000000000'
}
expect(apiErrorResult.proxyAgentConfigured).toBe(false);
expect(apiErrorResult.requestHost).toBe('api.anthropic.com');
expect(parseLockContents(apiErrorResult.lockContents)).toEqual({
blockedUntil: Math.floor(nowMs / 1000) + 30,
error: 'timeout'
});
const genericLockResult = harness.runProbe({
claudeConfigDir: apiErrorHome.claudeConfig,
home: apiErrorHome.home,
mode: 'unexpected',
nowMs,
pathDir: apiErrorHome.bin
});
expect(genericLockResult.first).toEqual({ error: 'timeout' });
expect(genericLockResult.second).toEqual({ error: 'timeout' });
expect(genericLockResult.requestCount).toBe(0);
const successResult = harness.runProbe({
claudeConfigDir: successHome.claudeConfig,
home: successHome.home,
mode: 'success',
nowMs,
pathDir: successHome.bin,
responseBody: successResponseBody
});
const successResult = JSON.parse(successOutput) as UsageProbeResult;
expect(successResult.first).toEqual({
sessionUsage: 42,
sessionResetAt: '2030-01-01T00:00:00.000Z',
@@ -211,26 +318,281 @@ process.stdout.write(JSON.stringify({
expect(successResult.second).toEqual(successResult.first);
expect(successResult.cacheExists).toBe(true);
expect(successResult.requestCount).toBe(1);
expect(successResult.proxyAgentConfigured).toBe(false);
expect(successResult.requestHost).toBe('api.anthropic.com');
const cachedSuccessOutput = execFileSync(process.execPath, [probeScriptPath], {
encoding: 'utf8',
env: {
...process.env,
HOME: successHome,
PATH: successBin,
CLAUDE_CONFIG_DIR: successClaudeConfig,
TEST_REQUEST_MODE: 'unexpected',
TEST_NOW_MS: '2200000000000'
}
const httpsProxyResult = harness.runProbe({
claudeConfigDir: proxyHome.claudeConfig,
home: proxyHome.home,
httpsProxy: 'http://proxy.local:8080',
mode: 'success',
nowMs,
pathDir: proxyHome.bin,
responseBody: successResponseBody
});
expect(httpsProxyResult.first).toEqual(successResult.first);
expect(httpsProxyResult.second).toEqual(successResult.first);
expect(httpsProxyResult.requestCount).toBe(1);
expect(httpsProxyResult.proxyAgentConfigured).toBe(true);
expect(httpsProxyResult.requestHost).toBe('api.anthropic.com');
const lowercaseProxyResult = harness.runProbe({
claudeConfigDir: lowercaseProxyHome.claudeConfig,
home: lowercaseProxyHome.home,
lowercaseHttpsProxy: 'http://proxy.local:8080',
mode: 'success',
nowMs,
pathDir: lowercaseProxyHome.bin,
responseBody: successResponseBody
});
expect(lowercaseProxyResult.first).toEqual(successResult.first);
expect(lowercaseProxyResult.second).toEqual(successResult.first);
expect(lowercaseProxyResult.requestCount).toBe(1);
expect(lowercaseProxyResult.proxyAgentConfigured).toBe(false);
const blankProxyResult = harness.runProbe({
claudeConfigDir: blankProxyHome.claudeConfig,
home: blankProxyHome.home,
httpsProxy: ' ',
mode: 'success',
nowMs,
pathDir: blankProxyHome.bin,
responseBody: successResponseBody
});
expect(blankProxyResult.first).toEqual(successResult.first);
expect(blankProxyResult.second).toEqual(successResult.first);
expect(blankProxyResult.requestCount).toBe(1);
expect(blankProxyResult.proxyAgentConfigured).toBe(false);
const invalidProxyResult = harness.runProbe({
claudeConfigDir: invalidProxyHome.claudeConfig,
home: invalidProxyHome.home,
httpsProxy: '://bad-proxy',
mode: 'success',
nowMs,
pathDir: invalidProxyHome.bin,
responseBody: successResponseBody
});
expect(invalidProxyResult.first).toEqual({ error: 'api-error' });
expect(invalidProxyResult.second).toEqual({ error: 'api-error' });
expect(invalidProxyResult.requestCount).toBe(0);
expect(invalidProxyResult.proxyAgentConfigured).toBe(false);
const staleProxyResult = harness.runProbe({
claudeConfigDir: successHome.claudeConfig,
home: successHome.home,
httpsProxy: '://bad-proxy',
mode: 'success',
nowMs: nowMs + 181000,
pathDir: successHome.bin,
responseBody: successResponseBody
});
expect(staleProxyResult.first).toEqual(successResult.first);
expect(staleProxyResult.second).toEqual(successResult.first);
expect(staleProxyResult.requestCount).toBe(0);
expect(staleProxyResult.proxyAgentConfigured).toBe(false);
const cachedSuccessResult = harness.runProbe({
claudeConfigDir: successHome.claudeConfig,
home: successHome.home,
mode: 'unexpected',
nowMs,
pathDir: successHome.bin
});
const cachedSuccessResult = JSON.parse(cachedSuccessOutput) as UsageProbeResult;
expect(cachedSuccessResult.first).toEqual(successResult.first);
expect(cachedSuccessResult.second).toEqual(successResult.first);
expect(cachedSuccessResult.cacheExists).toBe(true);
expect(cachedSuccessResult.requestCount).toBe(1);
expect(cachedSuccessResult.requestCount).toBe(0);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
harness.cleanup();
}
});
it('reuses stale cached data during a numeric Retry-After backoff and retries after expiry', () => {
const harness = createProbeHarness();
try {
const home = harness.createTokenHome('rate-limited-with-cache');
const rateLimitNowMs = nowMs + 31000;
const successResult = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'success',
nowMs,
pathDir: home.bin,
responseBody: successResponseBody
});
const rateLimitedResult = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'status',
nowMs: rateLimitNowMs,
pathDir: home.bin,
responseBody: rateLimitedResponseBody,
responseHeaders: { 'retry-after': '3600' },
statusCode: 429
});
expect(rateLimitedResult.first).toEqual(successResult.first);
expect(rateLimitedResult.second).toEqual(successResult.first);
expect(rateLimitedResult.requestCount).toBe(1);
expect(parseLockContents(rateLimitedResult.lockContents)).toEqual({
blockedUntil: Math.floor(rateLimitNowMs / 1000) + 3600,
error: 'rate-limited'
});
const activeBackoffResult = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'unexpected',
nowMs: rateLimitNowMs + 600000,
pathDir: home.bin
});
expect(activeBackoffResult.first).toEqual(successResult.first);
expect(activeBackoffResult.second).toEqual(successResult.first);
expect(activeBackoffResult.requestCount).toBe(0);
const postBackoffResult = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'success',
nowMs: rateLimitNowMs + 3601000,
pathDir: home.bin,
responseBody: updatedSuccessResponseBody
});
expect(postBackoffResult.first).toEqual({
sessionUsage: 55,
sessionResetAt: '2030-01-02T00:00:00.000Z',
weeklyUsage: 21,
weeklyResetAt: '2030-01-08T00:00:00.000Z'
});
expect(postBackoffResult.second).toEqual(postBackoffResult.first);
expect(postBackoffResult.requestCount).toBe(1);
} finally {
harness.cleanup();
}
});
it('returns rate-limited without stale cache and falls back to the default backoff when Retry-After is invalid', () => {
const harness = createProbeHarness();
try {
const home = harness.createTokenHome('rate-limited-no-cache');
const firstRateLimitedResult = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'status',
nowMs,
pathDir: home.bin,
responseBody: rateLimitedResponseBody,
responseHeaders: { 'retry-after': 'not-a-number' },
statusCode: 429
});
expect(firstRateLimitedResult.first).toEqual({ error: 'rate-limited' });
expect(firstRateLimitedResult.second).toEqual({ error: 'rate-limited' });
expect(firstRateLimitedResult.requestCount).toBe(1);
expect(parseLockContents(firstRateLimitedResult.lockContents)).toEqual({
blockedUntil: Math.floor(nowMs / 1000) + 300,
error: 'rate-limited'
});
const activeBackoffResult = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'unexpected',
nowMs: nowMs + 299000,
pathDir: home.bin
});
expect(activeBackoffResult.first).toEqual({ error: 'rate-limited' });
expect(activeBackoffResult.second).toEqual({ error: 'rate-limited' });
expect(activeBackoffResult.requestCount).toBe(0);
const postBackoffResult = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'success',
nowMs: nowMs + 301000,
pathDir: home.bin,
responseBody: successResponseBody
});
expect(postBackoffResult.first).toEqual({
sessionUsage: 42,
sessionResetAt: '2030-01-01T00:00:00.000Z',
weeklyUsage: 17,
weeklyResetAt: '2030-01-07T00:00:00.000Z'
});
expect(postBackoffResult.second).toEqual(postBackoffResult.first);
expect(postBackoffResult.requestCount).toBe(1);
} finally {
harness.cleanup();
}
});
it('parses HTTP-date Retry-After headers', () => {
const harness = createProbeHarness();
try {
const home = harness.createTokenHome('rate-limited-http-date');
const retryAt = new Date(nowMs + 900000).toUTCString();
const result = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'status',
nowMs,
pathDir: home.bin,
responseBody: rateLimitedResponseBody,
responseHeaders: { 'retry-after': retryAt },
statusCode: 429
});
expect(result.first).toEqual({ error: 'rate-limited' });
expect(result.second).toEqual({ error: 'rate-limited' });
expect(parseLockContents(result.lockContents)).toEqual({
blockedUntil: Math.floor((nowMs + 900000) / 1000),
error: 'rate-limited'
});
} finally {
harness.cleanup();
}
});
it('supports the legacy empty lock file fallback', () => {
const harness = createProbeHarness();
try {
const home = harness.createTokenHome('legacy-lock');
const lockDir = path.join(home.home, '.cache', 'ccstatusline');
const lockFile = path.join(lockDir, 'usage.lock');
fs.mkdirSync(lockDir, { recursive: true });
fs.writeFileSync(lockFile, '');
fs.utimesSync(lockFile, new Date(nowMs), new Date(nowMs));
const result = harness.runProbe({
claudeConfigDir: home.claudeConfig,
home: home.home,
mode: 'unexpected',
nowMs,
pathDir: home.bin
});
expect(result.first).toEqual({ error: 'timeout' });
expect(result.second).toEqual({ error: 'timeout' });
expect(result.requestCount).toBe(0);
} finally {
harness.cleanup();
}
});
});
@@ -0,0 +1,72 @@
import * as childProcess from 'child_process';
import { createRequire } from 'module';
import type { Mock } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi
} from 'vitest';
import { getUsageToken } from '../usage-fetch';
vi.mock('child_process', () => ({ execFileSync: vi.fn() }));
const require = createRequire(import.meta.url);
const { execFileSync: realExecFileSync } = require('node:child_process') as { execFileSync: typeof childProcess.execFileSync };
const mockedExecFileSync = childProcess.execFileSync as Mock;
describe('getUsageToken dump-keychain behavior', () => {
beforeEach(() => {
mockedExecFileSync.mockReset();
mockedExecFileSync.mockImplementation(realExecFileSync);
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin');
});
afterEach(() => {
vi.restoreAllMocks();
mockedExecFileSync.mockReset();
mockedExecFileSync.mockImplementation(realExecFileSync);
});
it('uses an expanded maxBuffer when dumping keychains for hashed fallbacks', () => {
let dumpMaxBuffer: number | undefined;
mockedExecFileSync.mockImplementation((command: string, args?: string[], options?: { maxBuffer?: number }) => {
if (command !== 'security') {
return realExecFileSync(command, args, options);
}
if (!args) {
throw new Error('Expected security arguments');
}
if (args[0] === 'find-generic-password' && args[2] === 'Claude Code-credentials') {
throw new Error('missing exact credential');
}
if (args[0] === 'dump-keychain') {
dumpMaxBuffer = options?.maxBuffer;
return [
'keychain: "/Users/example/Library/Keychains/login.keychain-db"',
'version: 512',
'class: "genp"',
'attributes:',
' "svce"<blob>="Claude Code-credentials-hashed"',
' "mdat"<timedate>="20240301010101Z"'
].join('\n');
}
if (args[0] === 'find-generic-password' && args[2] === 'Claude Code-credentials-hashed') {
return JSON.stringify({ claudeAiOauth: { accessToken: 'hashed-token' } });
}
throw new Error(`Unexpected security args: ${args.join(' ')}`);
});
expect(getUsageToken()).toBe('hashed-token');
expect(dumpMaxBuffer).toBe(8 * 1024 * 1024);
});
});
+330
View File
@@ -0,0 +1,330 @@
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { fileURLToPath } from 'url';
import {
describe,
expect,
it
} from 'vitest';
import { parseMacKeychainCredentialCandidates } from '../usage-fetch';
interface TokenHome {
bin: string;
claudeConfig: string;
home: string;
logFile: string;
}
interface TokenProbeOptions {
candidatePayloads?: Record<string, string>;
dump?: string;
dumpPaddingLines?: number;
exactPayload?: string;
platform: NodeJS.Platform;
tokenHome: TokenHome;
}
interface TokenProbeResult {
first: string | null;
second: string | null;
securityLog: string[];
}
function makeTokenPayload(token: string): string {
return JSON.stringify({ claudeAiOauth: { accessToken: token } });
}
function encodeAsciiAsHex(value: string): string {
return Buffer.from(value, 'utf8').toString('hex');
}
function makeKeychainBlock(service: string, modifiedAt?: { raw?: string; quoted?: string }): string {
const lines = [
'keychain: "/Users/example/Library/Keychains/login.keychain-db"',
'version: 512',
'class: "genp"',
'attributes:',
` "svce"<blob>="${service}"`
];
if (modifiedAt?.raw && modifiedAt.quoted) {
lines.push(` "mdat"<timedate>=0x${modifiedAt.raw} "${modifiedAt.quoted}"`);
} else if (modifiedAt?.raw) {
lines.push(` "mdat"<timedate>=0x${modifiedAt.raw}`);
} else if (modifiedAt?.quoted) {
lines.push(` "mdat"<timedate>="${modifiedAt.quoted}"`);
}
return lines.join('\n');
}
function createTokenHarness() {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-token-test-'));
const probeScriptPath = path.join(tempRoot, 'probe-token.mjs');
const usageFetchModulePath = fileURLToPath(new URL('../usage-fetch.ts', import.meta.url));
const probeScript = `
import * as fs from 'fs';
if (process.env.TEST_PLATFORM) {
Object.defineProperty(process, 'platform', {
configurable: true,
value: process.env.TEST_PLATFORM
});
}
const { getUsageToken } = await import(${JSON.stringify(usageFetchModulePath)});
const first = getUsageToken();
const second = getUsageToken();
const logFile = process.env.TEST_SECURITY_LOG_FILE;
process.stdout.write(JSON.stringify({
first,
second,
securityLog: logFile && fs.existsSync(logFile)
? fs.readFileSync(logFile, 'utf8').split(/\\r?\\n/).filter(Boolean)
: []
}));
`;
const securityScript = `#!/usr/bin/env node
const fs = require('fs');
const args = process.argv.slice(2);
const logFile = process.env.TEST_SECURITY_LOG_FILE;
if (logFile) {
fs.appendFileSync(logFile, args.join(' ') + '\\n');
}
if (args[0] === 'dump-keychain') {
const paddingLines = Number.parseInt(process.env.TEST_SECURITY_DUMP_PADDING_LINES || '0', 10);
let remainingPaddingLines = paddingLines;
while (remainingPaddingLines > 0) {
const chunkSize = Math.min(remainingPaddingLines, 1024);
fs.writeSync(process.stdout.fd, 'ignored\\n'.repeat(chunkSize));
remainingPaddingLines -= chunkSize;
}
fs.writeSync(process.stdout.fd, process.env.TEST_SECURITY_DUMP || '');
process.exit(0);
}
if (args[0] !== 'find-generic-password') {
process.exit(44);
}
const serviceIndex = args.indexOf('-s');
const service = serviceIndex >= 0 ? args[serviceIndex + 1] : '';
const exactPayload = process.env.TEST_SECURITY_EXACT_PAYLOAD;
const candidatePayloads = JSON.parse(process.env.TEST_SECURITY_CANDIDATE_PAYLOADS_JSON || '{}');
if (service === 'Claude Code-credentials') {
if (exactPayload === undefined || exactPayload === '__MISSING__') {
process.exit(44);
}
fs.writeSync(process.stdout.fd, exactPayload);
process.exit(0);
}
if (Object.prototype.hasOwnProperty.call(candidatePayloads, service)) {
fs.writeSync(process.stdout.fd, candidatePayloads[service]);
process.exit(0);
}
process.exit(44);
`;
fs.writeFileSync(probeScriptPath, probeScript);
function createTokenHome(name: string, fileToken?: string): TokenHome {
const home = path.join(tempRoot, `home-${name}`);
const bin = path.join(tempRoot, `bin-${name}`);
const claudeConfig = path.join(tempRoot, `claude-${name}`);
const logFile = path.join(tempRoot, `security-${name}.log`);
const securityPath = path.join(bin, 'security');
fs.mkdirSync(home, { recursive: true });
fs.mkdirSync(bin, { recursive: true });
fs.mkdirSync(claudeConfig, { recursive: true });
fs.writeFileSync(securityPath, securityScript);
fs.chmodSync(securityPath, 0o755);
if (fileToken) {
fs.writeFileSync(
path.join(claudeConfig, '.credentials.json'),
makeTokenPayload(fileToken)
);
}
return {
bin,
claudeConfig,
home,
logFile
};
}
function runTokenProbe(options: TokenProbeOptions): TokenProbeResult {
const output = execFileSync(process.execPath, [probeScriptPath], {
encoding: 'utf8',
env: {
...process.env,
CLAUDE_CONFIG_DIR: options.tokenHome.claudeConfig,
HOME: options.tokenHome.home,
PATH: `${options.tokenHome.bin}${path.delimiter}${process.env.PATH ?? ''}`,
TEST_PLATFORM: options.platform,
TEST_SECURITY_CANDIDATE_PAYLOADS_JSON: JSON.stringify(options.candidatePayloads ?? {}),
TEST_SECURITY_DUMP: options.dump ?? '',
TEST_SECURITY_DUMP_PADDING_LINES: String(options.dumpPaddingLines ?? 0),
TEST_SECURITY_EXACT_PAYLOAD: options.exactPayload ?? '__MISSING__',
TEST_SECURITY_LOG_FILE: options.tokenHome.logFile
}
});
return JSON.parse(output) as TokenProbeResult;
}
function cleanup(): void {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
return {
cleanup,
createTokenHome,
runTokenProbe
};
}
describe('parseMacKeychainCredentialCandidates', () => {
it('returns hashed macOS credential candidates sorted newest-first and excludes the exact service', () => {
const dump = [
makeKeychainBlock('Claude Code-credentials', { quoted: '20240101010101Z' }),
makeKeychainBlock('Claude Code-credentials-old', { quoted: '20240201010101Z' }),
makeKeychainBlock('Claude Code-credentials-new', { quoted: '20240301010101Z' })
].join('\n');
expect(parseMacKeychainCredentialCandidates(dump)).toEqual([
'Claude Code-credentials-new',
'Claude Code-credentials-old'
]);
});
it('uses discovered order when modified times are unavailable and parses hex-only timestamps when present', () => {
const dump = [
makeKeychainBlock('Claude Code-credentials-first'),
makeKeychainBlock('Claude Code-credentials-second', { raw: encodeAsciiAsHex('20240401010101Z\0') }),
makeKeychainBlock('Claude Code-credentials-third')
].join('\n');
expect(parseMacKeychainCredentialCandidates(dump)).toEqual([
'Claude Code-credentials-second',
'Claude Code-credentials-first',
'Claude Code-credentials-third'
]);
});
});
describe('getUsageToken', () => {
it('prefers the exact macOS keychain service over hashed fallbacks and files', () => {
const harness = createTokenHarness();
try {
const tokenHome = harness.createTokenHome('exact', 'file-token');
const result = harness.runTokenProbe({
exactPayload: makeTokenPayload('exact-token'),
platform: 'darwin',
tokenHome
});
expect(result.first).toBe('exact-token');
expect(result.second).toBe('exact-token');
expect(result.securityLog).toEqual([
'find-generic-password -s Claude Code-credentials -w',
'find-generic-password -s Claude Code-credentials -w'
]);
} finally {
harness.cleanup();
}
});
it('tries the newest hashed macOS keychain candidate after an exact miss', () => {
const harness = createTokenHarness();
try {
const tokenHome = harness.createTokenHome('hashed');
const dump = [
makeKeychainBlock('Claude Code-credentials-old', { quoted: '20240201010101Z' }),
makeKeychainBlock('Claude Code-credentials-new', { quoted: '20240301010101Z' })
].join('\n');
const result = harness.runTokenProbe({
candidatePayloads: { 'Claude Code-credentials-new': makeTokenPayload('hashed-token') },
dump,
platform: 'darwin',
tokenHome
});
expect(result.first).toBe('hashed-token');
expect(result.second).toBe('hashed-token');
expect(result.securityLog).toEqual([
'find-generic-password -s Claude Code-credentials -w',
'dump-keychain',
'find-generic-password -s Claude Code-credentials-new -w',
'find-generic-password -s Claude Code-credentials -w',
'dump-keychain',
'find-generic-password -s Claude Code-credentials-new -w'
]);
} finally {
harness.cleanup();
}
});
it('falls back to ~/.claude/.credentials.json on macOS when keychain lookups miss or parse invalid data', () => {
const harness = createTokenHarness();
try {
const tokenHome = harness.createTokenHome('file-fallback', 'file-token');
const dump = makeKeychainBlock('Claude Code-credentials-hashed', { quoted: '20240301010101Z' });
const result = harness.runTokenProbe({
candidatePayloads: { 'Claude Code-credentials-hashed': 'not-json' },
dump,
platform: 'darwin',
tokenHome
});
expect(result.first).toBe('file-token');
expect(result.second).toBe('file-token');
expect(result.securityLog).toEqual([
'find-generic-password -s Claude Code-credentials -w',
'dump-keychain',
'find-generic-password -s Claude Code-credentials-hashed -w',
'find-generic-password -s Claude Code-credentials -w',
'dump-keychain',
'find-generic-password -s Claude Code-credentials-hashed -w'
]);
} finally {
harness.cleanup();
}
});
it('uses the credentials file on non-macOS', () => {
const harness = createTokenHarness();
try {
const tokenHome = harness.createTokenHome('linux', 'linux-file-token');
const result = harness.runTokenProbe({
platform: 'linux',
tokenHome
});
expect(result.first).toBe('linux-file-token');
expect(result.second).toBe('linux-file-token');
expect(result.securityLog).toEqual([]);
} finally {
harness.cleanup();
}
});
});
+13
View File
@@ -0,0 +1,13 @@
import {
describe,
expect,
it
} from 'vitest';
import { getUsageErrorMessage } from '../usage-windows';
describe('getUsageErrorMessage', () => {
it('returns the rate-limited label', () => {
expect(getUsageErrorMessage('rate-limited')).toBe('[Rate limited]');
});
});
+32 -1
View File
@@ -134,9 +134,40 @@ describe('usage window helpers', () => {
});
it('formats duration in block timer style', () => {
expect(formatUsageDuration(0)).toBe('0hr');
expect(formatUsageDuration(0)).toBe('0m');
expect(formatUsageDuration(3 * 60 * 60 * 1000)).toBe('3hr');
expect(formatUsageDuration(3.5 * 60 * 60 * 1000)).toBe('3hr 30m');
expect(formatUsageDuration(4 * 60 * 60 * 1000 + 5 * 60 * 1000)).toBe('4hr 5m');
});
it('formats duration with days when >= 24h', () => {
expect(formatUsageDuration(25 * 60 * 60 * 1000)).toBe('1d 1hr');
expect(formatUsageDuration(36.5 * 60 * 60 * 1000)).toBe('1d 12hr 30m');
expect(formatUsageDuration(168 * 60 * 60 * 1000)).toBe('7d');
});
it('formats duration in compact style', () => {
expect(formatUsageDuration(0, true)).toBe('0m');
expect(formatUsageDuration(3 * 60 * 60 * 1000, true)).toBe('3h');
expect(formatUsageDuration(3.5 * 60 * 60 * 1000, true)).toBe('3h30m');
expect(formatUsageDuration(4 * 60 * 60 * 1000 + 5 * 60 * 1000, true)).toBe('4h5m');
});
it('formats duration with days in compact style when >= 24h', () => {
expect(formatUsageDuration(25 * 60 * 60 * 1000, true)).toBe('1d1h');
expect(formatUsageDuration(36.5 * 60 * 60 * 1000, true)).toBe('1d12h30m');
expect(formatUsageDuration(168 * 60 * 60 * 1000, true)).toBe('7d');
});
it('formats duration without days when requested', () => {
expect(formatUsageDuration(25 * 60 * 60 * 1000, false, false)).toBe('25hr');
expect(formatUsageDuration(36.5 * 60 * 60 * 1000, false, false)).toBe('36hr 30m');
expect(formatUsageDuration(168 * 60 * 60 * 1000, false, false)).toBe('168hr');
});
it('formats duration without days in compact style when requested', () => {
expect(formatUsageDuration(25 * 60 * 60 * 1000, true, false)).toBe('25h');
expect(formatUsageDuration(36.5 * 60 * 60 * 1000, true, false)).toBe('36h30m');
expect(formatUsageDuration(168 * 60 * 60 * 1000, true, false)).toBe('168h');
});
});
+56
View File
@@ -4,6 +4,10 @@ import * as os from 'os';
import * as path from 'path';
import type { ClaudeSettings } from '../types/ClaudeSettings';
import {
SettingsSchema,
type Settings
} from '../types/Settings';
import {
getConfigPath,
@@ -110,6 +114,26 @@ async function backupClaudeSettings(suffix = '.bak'): Promise<string | null> {
interface LoadClaudeSettingsOptions { logErrors?: boolean }
export function loadClaudeSettingsSync(options: LoadClaudeSettingsOptions = {}): ClaudeSettings {
const { logErrors = true } = options;
const settingsPath = getClaudeSettingsPath();
// File doesn't exist - return empty object
if (!fs.existsSync(settingsPath)) {
return {};
}
try {
const content = fs.readFileSync(settingsPath, 'utf-8');
return JSON.parse(content) as ClaudeSettings;
} catch (error) {
if (logErrors) {
console.error('Failed to load Claude settings:', error);
}
throw error;
}
}
export async function loadClaudeSettings(options: LoadClaudeSettingsOptions = {}): Promise<ClaudeSettings> {
const { logErrors = true } = options;
const settingsPath = getClaudeSettingsPath();
@@ -178,6 +202,25 @@ function buildCommand(baseCommand: string): string {
return baseCommand;
}
async function loadSavedSettingsForHookSync(): Promise<Settings | null> {
const configPath = getConfigPath();
if (!fs.existsSync(configPath)) {
return null;
}
try {
const content = await readFile(configPath, 'utf-8');
const parsed = JSON.parse(content) as unknown;
const result = SettingsSchema.safeParse(parsed);
if (!result.success) {
return null;
}
return result.data;
} catch {
return null;
}
}
export async function installStatusLine(useBunx = false): Promise<void> {
let settings: ClaudeSettings;
@@ -202,6 +245,12 @@ export async function installStatusLine(useBunx = false): Promise<void> {
};
await saveClaudeSettings(settings);
const savedSettings = await loadSavedSettingsForHookSync();
if (savedSettings) {
const { syncWidgetHooks } = await import('./hooks');
await syncWidgetHooks(savedSettings);
}
}
export async function uninstallStatusLine(): Promise<void> {
@@ -218,6 +267,13 @@ export async function uninstallStatusLine(): Promise<void> {
delete settings.statusLine;
await saveClaudeSettings(settings);
}
try {
const { removeManagedHooks } = await import('./hooks');
await removeManagedHooks();
} catch {
// Ignore hook cleanup failures during uninstall
}
}
export async function getExistingStatusLine(): Promise<string | null> {
+6
View File
@@ -158,4 +158,10 @@ export async function saveSettings(settings: Settings): Promise<void> {
};
await writeSettingsJson(settingsWithVersion, paths);
// Sync widget hooks to Claude settings
try {
const { syncWidgetHooks } = await import('./hooks');
await syncWidgetHooks(settings);
} catch { /* ignore hook sync failures */ }
}
+96
View File
@@ -0,0 +1,96 @@
import type { Settings } from '../types/Settings';
import type { Widget } from '../types/Widget';
import {
getExistingStatusLine,
loadClaudeSettings,
saveClaudeSettings
} from './claude-settings';
import { getWidget } from './widgets';
export interface WidgetHookDef {
event: string;
matcher?: string;
}
const HOOK_TAG = 'ccstatusline-managed';
interface HookEntry {
_tag?: string;
matcher?: string;
hooks?: { type: string; command: string }[];
}
function stripManagedHooks(hooks: Record<string, HookEntry[]>): void {
for (const event of Object.keys(hooks)) {
hooks[event] = (hooks[event] ?? []).filter(entry => entry._tag !== HOOK_TAG);
if (hooks[event].length === 0) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete hooks[event];
}
}
}
function getActiveHookDefs(settings: Settings): WidgetHookDef[] {
const seen = new Set<string>();
const defs: WidgetHookDef[] = [];
for (const line of settings.lines) {
for (const item of line) {
const widget = getWidget(item.type) as (Widget & { getHooks?: () => WidgetHookDef[] }) | null;
if (!widget?.getHooks) {
continue;
}
for (const hook of widget.getHooks()) {
const key = `${hook.event}:${hook.matcher ?? ''}`;
if (!seen.has(key)) {
seen.add(key);
defs.push(hook);
}
}
}
}
return defs;
}
export async function syncWidgetHooks(settings: Settings): Promise<void> {
const needed = getActiveHookDefs(settings);
const claudeSettings = await loadClaudeSettings({ logErrors: false });
const hooks = (claudeSettings.hooks ?? {}) as Record<string, HookEntry[]>;
// Remove all ccstatusline-managed hooks
stripManagedHooks(hooks);
const statusCommand = await getExistingStatusLine();
if (!statusCommand) {
claudeSettings.hooks = Object.keys(hooks).length > 0 ? hooks : undefined;
await saveClaudeSettings(claudeSettings);
return;
}
const hookCommand = `${statusCommand} --hook`;
// Add needed hooks
for (const def of needed) {
const entry: HookEntry = {
_tag: HOOK_TAG,
hooks: [{ type: 'command', command: hookCommand }]
};
if (def.matcher) {
entry.matcher = def.matcher;
}
const list = hooks[def.event] ??= [];
list.push(entry);
}
claudeSettings.hooks = Object.keys(hooks).length > 0 ? hooks : undefined;
await saveClaudeSettings(claudeSettings);
}
export async function removeManagedHooks(): Promise<void> {
const claudeSettings = await loadClaudeSettings({ logErrors: false });
const hooks = (claudeSettings.hooks ?? {}) as Record<string, HookEntry[]>;
stripManagedHooks(hooks);
claudeSettings.hooks = Object.keys(hooks).length > 0 ? hooks : undefined;
await saveClaudeSettings(claudeSettings);
}
+93
View File
@@ -0,0 +1,93 @@
export const IDE_LINK_MODES = [
'vscode',
'cursor'
] as const;
export type IdeLinkMode = (typeof IDE_LINK_MODES)[number];
export function renderOsc8Link(url: string, text: string): string {
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
}
function parseGitHubRepositoryPath(pathname: string): string | null {
const trimmedPath = pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '');
const segments = trimmedPath.split('/').filter(Boolean);
if (segments.length !== 2) {
return null;
}
return `${segments[0]}/${segments[1]}`;
}
/**
* Converts a git remote URL to a GitHub HTTPS base URL.
* Handles SSH, HTTPS, and ssh:// URL formats.
* Returns null if the remote is not a GitHub URL.
*/
export function parseGitHubBaseUrl(remoteUrl: string): string | null {
const trimmed = remoteUrl.trim();
if (trimmed.length === 0) {
return null;
}
const sshMatch = /^(?:[^@]+@)?github\.com:([^/]+\/[^/]+?)(?:\.git)?\/?$/.exec(trimmed);
if (sshMatch?.[1]) {
return `https://github.com/${sshMatch[1]}`;
}
try {
const parsedUrl = new URL(trimmed);
const supportedProtocols = new Set([
'http:',
'https:',
'ssh:',
'git:'
]);
if (parsedUrl.hostname.toLowerCase() !== 'github.com' || !supportedProtocols.has(parsedUrl.protocol)) {
return null;
}
const repoPath = parseGitHubRepositoryPath(parsedUrl.pathname);
if (!repoPath) {
return null;
}
return `https://github.com/${repoPath}`;
} catch {
return null;
}
}
export function encodeGitRefForUrlPath(ref: string): string {
return ref
.split('/')
.map(segment => encodeURIComponent(segment))
.join('/');
}
function encodeFilePathForUri(path: string): string {
return path
.replace(/\\/g, '/')
.split('/')
.map(segment => encodeURIComponent(segment))
.join('/');
}
export function buildIdeFileUrl(filePath: string, ideLinkMode: IdeLinkMode): string {
const normalizedPath = filePath.replace(/\\/g, '/');
const uncMatch = /^\/\/([^/]+)(\/.*)?$/.exec(normalizedPath);
if (uncMatch?.[1]) {
const encodedPath = encodeFilePathForUri(uncMatch[2] ?? '/');
return `${ideLinkMode}://file//${uncMatch[1]}${encodedPath}`;
}
const driveMatch = /^([A-Za-z]:)(\/.*)?$/.exec(normalizedPath);
if (driveMatch?.[1]) {
const encodedPath = encodeFilePathForUri(driveMatch[2] ?? '/');
return `${ideLinkMode}://file/${driveMatch[1]}${encodedPath}`;
}
return `${ideLinkMode}://file${encodeFilePathForUri(normalizedPath)}`;
}
+59
View File
@@ -0,0 +1,59 @@
import { getVisibleText } from './ansi';
import {
parseJsonlLine,
readJsonlLinesSync
} from './jsonl-lines';
export type TranscriptThinkingEffort = 'low' | 'medium' | 'high' | 'max';
const MODEL_STDOUT_PREFIX = '<local-command-stdout>Set model to ';
const MODEL_STDOUT_EFFORT_REGEX = /^<local-command-stdout>Set model to[\s\S]*? with (low|medium|high|max) effort<\/local-command-stdout>$/i;
interface TranscriptEntry { message?: { content?: string } }
function normalizeThinkingEffort(value: string | undefined): TranscriptThinkingEffort | undefined {
if (!value) {
return undefined;
}
const normalized = value.toLowerCase();
if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'max') {
return normalized;
}
return undefined;
}
export function getTranscriptThinkingEffort(transcriptPath: string | undefined): TranscriptThinkingEffort | undefined {
if (!transcriptPath) {
return undefined;
}
try {
const lines = readJsonlLinesSync(transcriptPath);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (!line) {
continue;
}
const entry = parseJsonlLine(line) as TranscriptEntry | null;
if (typeof entry?.message?.content !== 'string') {
continue;
}
const visibleContent = getVisibleText(entry.message.content).trim();
if (!visibleContent.startsWith(MODEL_STDOUT_PREFIX)) {
continue;
}
const match = MODEL_STDOUT_EFFORT_REGEX.exec(visibleContent);
return normalizeThinkingEffort(match?.[1]);
}
} catch {
return undefined;
}
return undefined;
}
+2 -1
View File
@@ -10,4 +10,5 @@ export {
getSpeedMetrics,
getSpeedMetricsCollection,
getTokenMetrics
} from './jsonl-metrics';
} from './jsonl-metrics';
export { getTranscriptThinkingEffort } from './jsonl-metadata';
+3 -3
View File
@@ -155,10 +155,10 @@ function renderPowerlineStatusLine(
// Use pre-rendered content - use the correct index from the mapping
const actualPreRenderedIndex = preRenderedIndices[i];
const preRendered = actualPreRenderedIndex !== undefined ? preRenderedWidgets[actualPreRenderedIndex] : undefined;
const widgetImpl = getWidget(widget.type);
if (preRendered?.content) {
widgetText = preRendered.content;
// Get default color from widget impl for consistency
const widgetImpl = getWidget(widget.type);
if (widgetImpl) {
defaultColor = widgetImpl.getDefaultColor();
}
@@ -352,7 +352,7 @@ function renderPowerlineStatusLine(
// - Background: previous widget's background color
// Build separator with raw ANSI codes to avoid reset issues
let separatorOutput = '';
let separatorOutput: string;
// Check if adjacent widgets have the same background color
const sameBackground = widget.bgColor && nextWidget.bgColor && widget.bgColor === nextWidget.bgColor;
@@ -832,7 +832,7 @@ export function renderStatusLine(
});
// Build the final status line
let statusLine = '';
let statusLine: string;
if (hasFlexSeparator && terminalWidth) {
// Split elements by flex separators
+58
View File
@@ -0,0 +1,58 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type {
SkillInvocation,
SkillsMetrics
} from '../types/SkillsMetrics';
const EMPTY: SkillsMetrics = { totalInvocations: 0, uniqueSkills: [], lastSkill: null };
function getSkillsDir(): string {
return path.join(os.homedir(), '.cache', 'ccstatusline', 'skills');
}
export function getSkillsFilePath(sessionId: string): string {
return path.join(getSkillsDir(), `skills-${sessionId}.jsonl`);
}
export function getSkillsMetrics(sessionId: string): SkillsMetrics {
const filePath = getSkillsFilePath(sessionId);
if (!fs.existsSync(filePath)) {
return EMPTY;
}
try {
const invocations: SkillInvocation[] = fs.readFileSync(filePath, 'utf-8')
.trim().split('\n')
.filter(line => line.trim())
.map((line) => {
try { return JSON.parse(line) as SkillInvocation; } catch {
return null;
}
})
.filter((e): e is SkillInvocation => e !== null && typeof e.skill === 'string' && typeof e.session_id === 'string');
if (invocations.length === 0) {
return EMPTY;
}
const uniqueSkills: string[] = [];
const seenSkills = new Set<string>();
for (let i = invocations.length - 1; i >= 0; i--) {
const skill = invocations[i]?.skill;
if (skill && !seenSkills.has(skill)) {
seenSkills.add(skill);
uniqueSkills.push(skill);
}
}
return {
totalInvocations: invocations.length,
uniqueSkills,
lastSkill: invocations[invocations.length - 1]?.skill ?? null
};
} catch {
return EMPTY;
}
}
+328 -91
View File
@@ -1,6 +1,7 @@
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as https from 'https';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as os from 'os';
import * as path from 'path';
import { z } from 'zod';
@@ -18,9 +19,16 @@ const CACHE_FILE = path.join(CACHE_DIR, 'usage.json');
const LOCK_FILE = path.join(CACHE_DIR, 'usage.lock');
const CACHE_MAX_AGE = 180; // seconds
const LOCK_MAX_AGE = 30; // rate limit: only try API once per 30 seconds
const TOKEN_CACHE_MAX_AGE = 3600; // 1 hour
const DEFAULT_RATE_LIMIT_BACKOFF = 300; // seconds
const MACOS_USAGE_CREDENTIALS_SERVICE = 'Claude Code-credentials';
const MACOS_SECURITY_DUMP_MAX_BUFFER = 8 * 1024 * 1024;
const UsageCredentialsSchema = z.object({ claudeAiOauth: z.object({ accessToken: z.string().nullable().optional() }).optional() });
const UsageLockErrorSchema = z.enum(['timeout', 'rate-limited']);
const UsageLockSchema = z.object({
blockedUntil: z.number(),
error: UsageLockErrorSchema.optional()
});
const CachedUsageDataSchema = z.object({
sessionUsage: z.number().nullable().optional(),
@@ -107,63 +115,202 @@ function parseUsageApiResponse(rawJson: string): UsageData | null {
// Memory caches
let cachedUsageData: UsageData | null = null;
let usageCacheTime = 0;
let cachedUsageToken: string | null = null;
let usageTokenCacheTime = 0;
let usageErrorCacheMaxAge = LOCK_MAX_AGE;
function setCachedUsageError(error: UsageError, now: number): UsageData {
type UsageLockError = z.infer<typeof UsageLockErrorSchema>;
type UsageApiFetchResult = { kind: 'success'; body: string } | { kind: 'rate-limited'; retryAfterSeconds: number } | { kind: 'error' };
interface MacKeychainCredentialCandidate {
modifiedAt: string | null;
order: number;
service: string;
}
function ensureCacheDirExists(): void {
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
}
function setCachedUsageError(error: UsageError, now: number, maxAge = LOCK_MAX_AGE): UsageData {
const errorData: UsageData = { error };
cachedUsageData = errorData;
usageCacheTime = now;
usageErrorCacheMaxAge = maxAge;
return errorData;
}
function getStaleUsageOrError(error: UsageError, now: number): UsageData {
const stale = readStaleUsageCache();
if (stale && !stale.error) {
cachedUsageData = stale;
usageCacheTime = now;
return stale;
}
return setCachedUsageError(error, now);
function cacheUsageData(data: UsageData, now: number): UsageData {
cachedUsageData = data;
usageCacheTime = now;
usageErrorCacheMaxAge = LOCK_MAX_AGE;
return data;
}
function getUsageToken(): string | null {
const now = Math.floor(Date.now() / 1000);
function getStaleUsageOrError(error: UsageError, now: number, errorCacheMaxAge = LOCK_MAX_AGE): UsageData {
const stale = readStaleUsageCache();
if (stale && !stale.error) {
return cacheUsageData(stale, now);
}
return setCachedUsageError(error, now, errorCacheMaxAge);
}
// Return cached token if still valid
if (cachedUsageToken && (now - usageTokenCacheTime) < TOKEN_CACHE_MAX_AGE) {
return cachedUsageToken;
function normalizeSecurityTimedateValue(rawValue: string): string | null {
const cleaned = rawValue.replace(/\\000/g, '').replace(/\0/g, '').trim();
return /^\d{14}Z$/.test(cleaned) ? cleaned : null;
}
function decodeHexAscii(rawHex: string): string | null {
if (rawHex.length === 0 || rawHex.length % 2 !== 0) {
return null;
}
try {
const isMac = process.platform === 'darwin';
if (isMac) {
// macOS: read from keychain
const result = execSync(
'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
).trim();
const token = parseUsageAccessToken(result);
if (token) {
cachedUsageToken = token;
usageTokenCacheTime = now;
}
return token;
let decoded = '';
for (let i = 0; i < rawHex.length; i += 2) {
const byte = Number.parseInt(rawHex.slice(i, i + 2), 16);
if (Number.isNaN(byte)) {
return null;
}
// Non-macOS: read from credentials file, honoring CLAUDE_CONFIG_DIR
const credFile = path.join(getClaudeConfigDir(), '.credentials.json');
const token = parseUsageAccessToken(fs.readFileSync(credFile, 'utf8'));
if (token) {
cachedUsageToken = token;
usageTokenCacheTime = now;
decoded += String.fromCharCode(byte);
}
return decoded;
}
function parseModifiedTimeFromKeychainBlock(block: string): string | null {
const quotedMatch = /"mdat"<timedate>=(?:0x[0-9A-Fa-f]+\s+)?"([^"]+)"/.exec(block);
if (quotedMatch?.[1]) {
const parsed = normalizeSecurityTimedateValue(quotedMatch[1]);
if (parsed !== null) {
return parsed;
}
return token;
}
const hexMatch = /"mdat"<timedate>=0x([0-9A-Fa-f]+)/.exec(block);
if (!hexMatch?.[1]) {
return null;
}
const decoded = decodeHexAscii(hexMatch[1]);
return decoded ? normalizeSecurityTimedateValue(decoded) : null;
}
function sortMacKeychainCredentialCandidates(a: MacKeychainCredentialCandidate, b: MacKeychainCredentialCandidate): number {
if (a.modifiedAt !== null && b.modifiedAt !== null && a.modifiedAt !== b.modifiedAt) {
return b.modifiedAt.localeCompare(a.modifiedAt);
}
if (a.modifiedAt !== null && b.modifiedAt === null) {
return -1;
}
if (a.modifiedAt === null && b.modifiedAt !== null) {
return 1;
}
return a.order - b.order;
}
export function parseMacKeychainCredentialCandidates(rawDump: string, servicePrefix = MACOS_USAGE_CREDENTIALS_SERVICE): string[] {
const blocks = rawDump.split(/(?=^keychain:\s)/m).filter(block => block.trim().length > 0);
const dedupedCandidates = new Map<string, MacKeychainCredentialCandidate>();
let order = 0;
for (const block of blocks) {
const serviceMatch = /"svce"<blob>="([^"]+)"/.exec(block);
const service = serviceMatch?.[1];
if (!service || !service.startsWith(servicePrefix) || service === MACOS_USAGE_CREDENTIALS_SERVICE) {
continue;
}
const candidate: MacKeychainCredentialCandidate = {
modifiedAt: parseModifiedTimeFromKeychainBlock(block),
order,
service
};
order += 1;
const existing = dedupedCandidates.get(service);
if (!existing || sortMacKeychainCredentialCandidates(candidate, existing) < 0) {
dedupedCandidates.set(service, candidate);
}
}
return [...dedupedCandidates.values()]
.sort(sortMacKeychainCredentialCandidates)
.map(candidate => candidate.service);
}
function readMacKeychainSecret(service: string): string | null {
try {
return execFileSync(
'security',
['find-generic-password', '-s', service, '-w'],
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
).trim();
} catch {
return null;
}
}
function readUsageTokenFromMacKeychainService(service: string): string | null {
const secret = readMacKeychainSecret(service);
return secret ? parseUsageAccessToken(secret) : null;
}
function listMacKeychainCredentialCandidates(): string[] {
try {
const rawDump = execFileSync(
'security',
['dump-keychain'],
{
encoding: 'utf8',
maxBuffer: MACOS_SECURITY_DUMP_MAX_BUFFER,
stdio: ['pipe', 'pipe', 'ignore']
}
);
return parseMacKeychainCredentialCandidates(rawDump);
} catch {
return [];
}
}
function readUsageTokenFromMacKeychainCandidates(): string | null {
const candidates = listMacKeychainCredentialCandidates();
for (const service of candidates) {
const token = readUsageTokenFromMacKeychainService(service);
if (token) {
return token;
}
}
return null;
}
function readUsageTokenFromCredentialsFile(): string | null {
try {
const credFile = path.join(getClaudeConfigDir(), '.credentials.json');
return parseUsageAccessToken(fs.readFileSync(credFile, 'utf8'));
} catch {
return null;
}
}
export function getUsageToken(): string | null {
if (process.platform !== 'darwin') {
return readUsageTokenFromCredentialsFile();
}
return readUsageTokenFromMacKeychainService(MACOS_USAGE_CREDENTIALS_SERVICE)
?? readUsageTokenFromMacKeychainCandidates()
?? readUsageTokenFromCredentialsFile();
}
function readStaleUsageCache(): UsageData | null {
try {
return parseCachedUsageData(fs.readFileSync(CACHE_FILE, 'utf8'));
@@ -172,22 +319,94 @@ function readStaleUsageCache(): UsageData | null {
}
}
function writeUsageLock(blockedUntil: number, error: UsageLockError): void {
try {
ensureCacheDirExists();
fs.writeFileSync(LOCK_FILE, JSON.stringify({ blockedUntil, error }));
} catch {
// Ignore lock file errors
}
}
function readActiveUsageLock(now: number): { blockedUntil: number; error: UsageLockError } | null {
let hasValidJsonLock = false;
try {
const parsed = parseJsonWithSchema(fs.readFileSync(LOCK_FILE, 'utf8'), UsageLockSchema);
if (parsed) {
hasValidJsonLock = true;
if (parsed.blockedUntil > now) {
return {
blockedUntil: parsed.blockedUntil,
error: parsed.error ?? 'timeout'
};
}
return null;
}
} catch {
// Fall back to the legacy mtime-based lock behavior below.
}
if (hasValidJsonLock) {
return null;
}
try {
const lockStat = fs.statSync(LOCK_FILE);
const lockMtime = Math.floor(lockStat.mtimeMs / 1000);
const blockedUntil = lockMtime + LOCK_MAX_AGE;
if (blockedUntil > now) {
return {
blockedUntil,
error: 'timeout'
};
}
} catch {
// Lock file doesn't exist - OK to proceed
}
return null;
}
function parseRetryAfterSeconds(headerValue: string | string[] | undefined, nowMs = Date.now()): number | null {
const rawValue = Array.isArray(headerValue) ? headerValue[0] : headerValue;
const trimmedValue = rawValue?.trim();
if (!trimmedValue) {
return null;
}
if (/^\d+$/.test(trimmedValue)) {
const seconds = Number.parseInt(trimmedValue, 10);
return seconds > 0 ? seconds : null;
}
const retryAtMs = Date.parse(trimmedValue);
if (Number.isNaN(retryAtMs)) {
return null;
}
const retryAfterSeconds = Math.ceil((retryAtMs - nowMs) / 1000);
return retryAfterSeconds > 0 ? retryAfterSeconds : null;
}
const USAGE_API_HOST = 'api.anthropic.com';
const USAGE_API_PATH = '/api/oauth/usage';
const USAGE_API_TIMEOUT_MS = 5000;
async function fetchFromUsageApi(token: string): Promise<string | null> {
return new Promise((resolve) => {
let settled = false;
function getUsageApiProxyUrl(): string | null {
const proxyUrl = process.env.HTTPS_PROXY?.trim();
if (proxyUrl === '') {
return null;
}
const finish = (value: string | null) => {
if (settled) {
return;
}
settled = true;
resolve(value);
};
return proxyUrl ?? null;
}
const request = https.request({
function getUsageApiRequestOptions(token: string): https.RequestOptions | null {
const proxyUrl = getUsageApiProxyUrl();
try {
return {
hostname: USAGE_API_HOST,
path: USAGE_API_PATH,
method: 'GET',
@@ -195,8 +414,33 @@ async function fetchFromUsageApi(token: string): Promise<string | null> {
'Authorization': `Bearer ${token}`,
'anthropic-beta': 'oauth-2025-04-20'
},
timeout: 5000
}, (response) => {
timeout: USAGE_API_TIMEOUT_MS,
...(proxyUrl ? { agent: new HttpsProxyAgent(proxyUrl) } : {})
};
} catch {
return null;
}
}
async function fetchFromUsageApi(token: string): Promise<UsageApiFetchResult> {
return new Promise((resolve) => {
let settled = false;
const finish = (value: UsageApiFetchResult) => {
if (settled) {
return;
}
settled = true;
resolve(value);
};
const requestOptions = getUsageApiRequestOptions(token);
if (!requestOptions) {
finish({ kind: 'error' });
return;
}
const request = https.request(requestOptions, (response) => {
let data = '';
response.setEncoding('utf8');
@@ -206,17 +450,26 @@ async function fetchFromUsageApi(token: string): Promise<string | null> {
response.on('end', () => {
if (response.statusCode === 200 && data) {
finish(data);
finish({ kind: 'success', body: data });
return;
}
finish(null);
if (response.statusCode === 429) {
finish({
kind: 'rate-limited',
retryAfterSeconds: parseRetryAfterSeconds(response.headers['retry-after']) ?? DEFAULT_RATE_LIMIT_BACKOFF
});
return;
}
finish({ kind: 'error' });
});
});
request.on('error', () => { finish(null); });
request.on('error', () => { finish({ kind: 'error' }); });
request.on('timeout', () => {
request.destroy();
finish(null);
finish({ kind: 'error' });
});
request.end();
});
@@ -231,7 +484,7 @@ export async function fetchUsageData(): Promise<UsageData> {
if (!cachedUsageData.error && cacheAge < CACHE_MAX_AGE) {
return cachedUsageData;
}
if (cachedUsageData.error && cacheAge < LOCK_MAX_AGE) {
if (cachedUsageData.error && cacheAge < usageErrorCacheMaxAge) {
return cachedUsageData;
}
}
@@ -243,9 +496,7 @@ export async function fetchUsageData(): Promise<UsageData> {
if (fileAge < CACHE_MAX_AGE) {
const fileData = parseCachedUsageData(fs.readFileSync(CACHE_FILE, 'utf8'));
if (fileData && !fileData.error) {
cachedUsageData = fileData;
usageCacheTime = now;
return fileData;
return cacheUsageData(fileData, now);
}
}
} catch {
@@ -258,41 +509,31 @@ export async function fetchUsageData(): Promise<UsageData> {
return getStaleUsageOrError('no-credentials', now);
}
// Rate limit: only try API once per 30 seconds
try {
const lockStat = fs.statSync(LOCK_FILE);
const lockAge = now - Math.floor(lockStat.mtimeMs / 1000);
if (lockAge < LOCK_MAX_AGE) {
// Rate limited - return stale cache or timeout error
const stale = readStaleUsageCache();
if (stale && !stale.error)
return stale;
return { error: 'timeout' };
}
} catch {
// Lock file doesn't exist - OK to proceed
const activeLock = readActiveUsageLock(now);
if (activeLock) {
return getStaleUsageOrError(
activeLock.error,
now,
Math.max(1, activeLock.blockedUntil - now)
);
}
// Touch lock file
try {
const lockDir = path.dirname(LOCK_FILE);
if (!fs.existsSync(lockDir)) {
fs.mkdirSync(lockDir, { recursive: true });
}
fs.writeFileSync(LOCK_FILE, '');
} catch {
// Ignore lock file errors
}
writeUsageLock(now + LOCK_MAX_AGE, 'timeout');
// Fetch from API using Node's https module
try {
const response = await fetchFromUsageApi(token);
if (!response) {
if (response.kind === 'rate-limited') {
writeUsageLock(now + response.retryAfterSeconds, 'rate-limited');
return getStaleUsageOrError('rate-limited', now, response.retryAfterSeconds);
}
if (response.kind === 'error') {
return getStaleUsageOrError('api-error', now);
}
const usageData = parseUsageApiResponse(response);
const usageData = parseUsageApiResponse(response.body);
if (!usageData) {
return getStaleUsageOrError('parse-error', now);
}
@@ -304,17 +545,13 @@ export async function fetchUsageData(): Promise<UsageData> {
// Save to cache
try {
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
ensureCacheDirExists();
fs.writeFileSync(CACHE_FILE, JSON.stringify(usageData));
} catch {
// Ignore cache write errors
}
cachedUsageData = usageData;
usageCacheTime = now;
return usageData;
return cacheUsageData(usageData, now);
} catch {
return getStaleUsageOrError('parse-error', now);
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { z } from 'zod';
export const FIVE_HOUR_BLOCK_MS = 5 * 60 * 60 * 1000;
export const SEVEN_DAY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
export const UsageErrorSchema = z.enum(['no-credentials', 'timeout', 'api-error', 'parse-error']);
export const UsageErrorSchema = z.enum(['no-credentials', 'timeout', 'rate-limited', 'api-error', 'parse-error']);
export type UsageError = z.infer<typeof UsageErrorSchema>;
export interface UsageData {
+10 -8
View File
@@ -89,22 +89,24 @@ export function resolveWeeklyUsageWindow(usageData: UsageData, nowMs = Date.now(
return getWeeklyUsageWindowFromResetAt(usageData.weeklyResetAt, nowMs);
}
export function formatUsageDuration(durationMs: number): string {
export function formatUsageDuration(durationMs: number, compact = false, useDays = true): string {
const clampedMs = Math.max(0, durationMs);
const elapsedHours = Math.floor(clampedMs / (1000 * 60 * 60));
const elapsedMinutes = Math.floor((clampedMs % (1000 * 60 * 60)) / (1000 * 60));
const totalHours = Math.floor(clampedMs / (1000 * 60 * 60));
const m = Math.floor((clampedMs % (1000 * 60 * 60)) / (1000 * 60));
if (elapsedMinutes === 0) {
return `${elapsedHours}hr`;
}
return `${elapsedHours}hr ${elapsedMinutes}m`;
const hLabel = compact ? 'h' : 'hr';
const sep = compact ? '' : ' ';
const d = useDays ? Math.floor(totalHours / 24) : 0;
const h = useDays ? totalHours % 24 : totalHours;
const parts = [d > 0 && `${d}d`, h > 0 && `${h}${hLabel}`, m > 0 && `${m}m`].filter(Boolean);
return parts.length > 0 ? parts.join(sep) : '0m';
}
export function getUsageErrorMessage(error: UsageError): string {
switch (error) {
case 'no-credentials': return '[No credentials]';
case 'timeout': return '[Timeout]';
case 'rate-limited': return '[Rate limited]';
case 'api-error': return '[API Error]';
case 'parse-error': return '[Parse Error]';
}
+4 -1
View File
@@ -51,7 +51,10 @@ export const WIDGET_MANIFEST: WidgetManifestEntry[] = [
{ type: 'weekly-usage', create: () => new widgets.WeeklyUsageWidget() },
{ type: 'reset-timer', create: () => new widgets.BlockResetTimerWidget() },
{ type: 'weekly-reset-timer', create: () => new widgets.WeeklyResetTimerWidget() },
{ type: 'context-bar', create: () => new widgets.ContextBarWidget() }
{ type: 'context-bar', create: () => new widgets.ContextBarWidget() },
{ type: 'skills', create: () => new widgets.SkillsWidget() },
{ type: 'thinking-effort', create: () => new widgets.ThinkingEffortWidget() },
{ type: 'vim-mode', create: () => new widgets.VimModeWidget() }
];
export const LAYOUT_WIDGET_MANIFEST: LayoutWidgetManifestEntry[] = [
+14 -9
View File
@@ -18,8 +18,11 @@ import {
getUsageDisplayMode,
getUsageDisplayModifierText,
getUsageProgressBarWidth,
getUsageTimerCustomKeybinds,
isUsageCompact,
isUsageInverted,
isUsageProgressMode,
toggleUsageCompact,
toggleUsageInverted
} from './shared/usage-display';
@@ -39,25 +42,30 @@ export class BlockResetTimerWidget implements Widget {
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return {
displayText: this.getDisplayName(),
modifierText: getUsageDisplayModifierText(item)
modifierText: getUsageDisplayModifierText(item, { includeCompact: true })
};
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'toggle-progress') {
return cycleUsageDisplayMode(item);
return cycleUsageDisplayMode(item, ['compact']);
}
if (action === 'toggle-invert') {
return toggleUsageInverted(item);
}
if (action === 'toggle-compact') {
return toggleUsageCompact(item);
}
return null;
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const displayMode = getUsageDisplayMode(item);
const inverted = isUsageInverted(item);
const compact = isUsageCompact(item);
if (context.isPreview) {
const previewPercent = inverted ? 90.0 : 10.0;
@@ -68,7 +76,7 @@ export class BlockResetTimerWidget implements Widget {
return formatRawOrLabeledValue(item, 'Reset ', `[${progressBar}] ${previewPercent.toFixed(1)}%`);
}
return formatRawOrLabeledValue(item, 'Reset: ', '4hr 30m');
return formatRawOrLabeledValue(item, 'Reset: ', compact ? '4h30m' : '4hr 30m');
}
const usageData = context.usageData ?? {};
@@ -90,15 +98,12 @@ export class BlockResetTimerWidget implements Widget {
return formatRawOrLabeledValue(item, 'Reset ', `[${progressBar}] ${percentage}%`);
}
const remainingTime = formatUsageDuration(window.remainingMs);
const remainingTime = formatUsageDuration(window.remainingMs, compact);
return formatRawOrLabeledValue(item, 'Reset: ', remainingTime);
}
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
];
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
return getUsageTimerCustomKeybinds(item);
}
supportsRawValue(): boolean { return true; }
+15 -10
View File
@@ -17,8 +17,11 @@ import {
getUsageDisplayMode,
getUsageDisplayModifierText,
getUsageProgressBarWidth,
getUsageTimerCustomKeybinds,
isUsageCompact,
isUsageInverted,
isUsageProgressMode,
toggleUsageCompact,
toggleUsageInverted
} from './shared/usage-display';
@@ -38,25 +41,30 @@ export class BlockTimerWidget implements Widget {
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return {
displayText: this.getDisplayName(),
modifierText: getUsageDisplayModifierText(item)
modifierText: getUsageDisplayModifierText(item, { includeCompact: true })
};
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'toggle-progress') {
return cycleUsageDisplayMode(item);
return cycleUsageDisplayMode(item, ['compact']);
}
if (action === 'toggle-invert') {
return toggleUsageInverted(item);
}
if (action === 'toggle-compact') {
return toggleUsageCompact(item);
}
return null;
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const displayMode = getUsageDisplayMode(item);
const inverted = isUsageInverted(item);
const compact = isUsageCompact(item);
if (context.isPreview) {
const previewPercent = inverted ? 26.1 : 73.9;
@@ -67,7 +75,7 @@ export class BlockTimerWidget implements Widget {
return formatRawOrLabeledValue(item, 'Block ', `[${progressBar}] ${previewPercent.toFixed(1)}%`);
}
return formatRawOrLabeledValue(item, 'Block: ', '3hr 45m');
return formatRawOrLabeledValue(item, 'Block: ', compact ? '3h45m' : '3hr 45m');
}
const usageData = context.usageData ?? {};
@@ -80,7 +88,7 @@ export class BlockTimerWidget implements Widget {
return formatRawOrLabeledValue(item, 'Block ', `[${emptyBar}] 0.0%`);
}
return formatRawOrLabeledValue(item, 'Block: ', '0hr 0m');
return formatRawOrLabeledValue(item, 'Block: ', compact ? '0h' : '0hr 0m');
}
if (isUsageProgressMode(displayMode)) {
@@ -91,15 +99,12 @@ export class BlockTimerWidget implements Widget {
return formatRawOrLabeledValue(item, 'Block ', `[${progressBar}] ${percentage}%`);
}
const elapsedTime = formatUsageDuration(window.elapsedMs);
const elapsedTime = formatUsageDuration(window.elapsedMs, compact);
return formatRawOrLabeledValue(item, 'Block: ', elapsedTime);
}
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
];
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
return getUsageTimerCustomKeybinds(item);
}
supportsRawValue(): boolean { return true; }
+46 -6
View File
@@ -10,13 +10,26 @@ import {
isInsideGitWorkTree,
runGit
} from '../utils/git';
import {
encodeGitRefForUrlPath,
parseGitHubBaseUrl,
renderOsc8Link
} from '../utils/hyperlink';
import { makeModifierText } from './shared/editor-display';
import {
getHideNoGitKeybinds,
getHideNoGitModifierText,
handleToggleNoGitAction,
isHideNoGitEnabled
} from './shared/git-no-git';
import {
isMetadataFlagEnabled,
toggleMetadataFlag
} from './shared/metadata';
const LINK_KEY = 'linkToGitHub';
const TOGGLE_LINK_ACTION = 'toggle-link';
export class GitBranchWidget implements Widget {
getDefaultColor(): string { return 'magenta'; }
@@ -24,21 +37,34 @@ export class GitBranchWidget implements Widget {
getDisplayName(): string { return 'Git Branch'; }
getCategory(): string { return 'Git'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const isLink = isMetadataFlagEnabled(item, LINK_KEY);
const modifiers: string[] = [];
const noGitText = getHideNoGitModifierText(item);
if (noGitText)
modifiers.push('hide \'no git\'');
if (isLink)
modifiers.push('GitHub link');
return {
displayText: this.getDisplayName(),
modifierText: getHideNoGitModifierText(item)
modifierText: makeModifierText(modifiers)
};
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === TOGGLE_LINK_ACTION) {
return toggleMetadataFlag(item, LINK_KEY);
}
return handleToggleNoGitAction(action, item);
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
void settings;
const hideNoGit = isHideNoGitEnabled(item);
const isLink = isMetadataFlagEnabled(item, LINK_KEY);
if (context.isPreview) {
return item.rawValue ? 'main' : '⎇ main';
const text = item.rawValue ? 'main' : '⎇ main';
return isLink ? renderOsc8Link('https://github.com/owner/repo/tree/main', text) : text;
}
if (!isInsideGitWorkTree(context)) {
@@ -46,10 +72,21 @@ export class GitBranchWidget implements Widget {
}
const branch = this.getGitBranch(context);
if (branch)
return item.rawValue ? branch : `${branch}`;
if (!branch) {
return hideNoGit ? null : '⎇ no git';
}
return hideNoGit ? null : '⎇ no git';
const displayText = item.rawValue ? branch : `${branch}`;
if (isLink) {
const remoteUrl = runGit('remote get-url origin', context);
const baseUrl = remoteUrl ? parseGitHubBaseUrl(remoteUrl) : null;
if (baseUrl) {
return renderOsc8Link(`${baseUrl}/tree/${encodeGitRefForUrlPath(branch)}`, displayText);
}
}
return displayText;
}
private getGitBranch(context: RenderContext): string | null {
@@ -57,7 +94,10 @@ export class GitBranchWidget implements Widget {
}
getCustomKeybinds(): CustomKeybind[] {
return getHideNoGitKeybinds();
return [
...getHideNoGitKeybinds(),
{ key: 'l', label: '(l)ink to GitHub', action: TOGGLE_LINK_ACTION }
];
}
supportsRawValue(): boolean { return true; }
+78 -6
View File
@@ -10,13 +10,29 @@ import {
isInsideGitWorkTree,
runGit
} from '../utils/git';
import type { IdeLinkMode } from '../utils/hyperlink';
import {
IDE_LINK_MODES,
buildIdeFileUrl,
renderOsc8Link
} from '../utils/hyperlink';
import { makeModifierText } from './shared/editor-display';
import {
getHideNoGitKeybinds,
getHideNoGitModifierText,
handleToggleNoGitAction,
isHideNoGitEnabled
} from './shared/git-no-git';
import { isMetadataFlagEnabled } from './shared/metadata';
const IDE_LINK_KEY = 'linkToIDE';
const LEGACY_CURSOR_LINK_KEY = 'linkToCursor';
const TOGGLE_LINK_ACTION = 'toggle-link';
const IDE_LINK_LABELS: Record<IdeLinkMode, string> = {
vscode: 'link-vscode',
cursor: 'link-cursor'
};
export class GitRootDirWidget implements Widget {
getDefaultColor(): string { return 'cyan'; }
@@ -24,21 +40,33 @@ export class GitRootDirWidget implements Widget {
getDisplayName(): string { return 'Git Root Dir'; }
getCategory(): string { return 'Git'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const ideLinkMode = this.getIdeLinkMode(item);
const modifiers: string[] = [];
const noGitText = getHideNoGitModifierText(item);
if (noGitText)
modifiers.push('hide \'no git\'');
if (ideLinkMode)
modifiers.push(IDE_LINK_LABELS[ideLinkMode]);
return {
displayText: this.getDisplayName(),
modifierText: getHideNoGitModifierText(item)
modifierText: makeModifierText(modifiers)
};
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === TOGGLE_LINK_ACTION) {
return this.cycleIdeLinkMode(item);
}
return handleToggleNoGitAction(action, item);
}
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
const hideNoGit = isHideNoGitEnabled(item);
const ideLinkMode = this.getIdeLinkMode(item);
if (context.isPreview) {
return 'my-repo';
const name = 'my-repo';
return ideLinkMode ? renderOsc8Link(buildIdeFileUrl('/Users/example/my-repo', ideLinkMode), name) : name;
}
if (!isInsideGitWorkTree(context)) {
@@ -46,11 +74,17 @@ export class GitRootDirWidget implements Widget {
}
const rootDir = this.getGitRootDir(context);
if (rootDir) {
return this.getRootDirName(rootDir);
if (!rootDir) {
return hideNoGit ? null : 'no git';
}
return hideNoGit ? null : 'no git';
const name = this.getRootDirName(rootDir);
if (ideLinkMode) {
return renderOsc8Link(buildIdeFileUrl(rootDir, ideLinkMode), name);
}
return name;
}
private getGitRootDir(context: RenderContext): string | null {
@@ -66,9 +100,47 @@ export class GitRootDirWidget implements Widget {
}
getCustomKeybinds(): CustomKeybind[] {
return getHideNoGitKeybinds();
return [
...getHideNoGitKeybinds(),
{ key: 'l', label: '(l)ink to IDE', action: TOGGLE_LINK_ACTION }
];
}
supportsRawValue(): boolean { return false; }
supportsColors(item: WidgetItem): boolean { return true; }
private getIdeLinkMode(item: WidgetItem): IdeLinkMode | null {
const configuredMode = item.metadata?.[IDE_LINK_KEY];
if (configuredMode && IDE_LINK_MODES.includes(configuredMode as IdeLinkMode)) {
return configuredMode as IdeLinkMode;
}
if (isMetadataFlagEnabled(item, LEGACY_CURSOR_LINK_KEY)) {
return 'cursor';
}
return null;
}
private cycleIdeLinkMode(item: WidgetItem): WidgetItem {
const currentMode = this.getIdeLinkMode(item);
const currentIndex = currentMode ? IDE_LINK_MODES.indexOf(currentMode) : -1;
const nextMode = currentIndex === IDE_LINK_MODES.length - 1 ? null : (IDE_LINK_MODES[currentIndex + 1] ?? null);
const {
[IDE_LINK_KEY]: removedIdeLink,
[LEGACY_CURSOR_LINK_KEY]: removedLegacyLink,
...restMetadata
} = item.metadata ?? {};
void removedIdeLink;
void removedLegacyLink;
return {
...item,
metadata: nextMode ? {
...restMetadata,
[IDE_LINK_KEY]: nextMode
} : (Object.keys(restMetadata).length > 0 ? restMetadata : undefined)
};
}
}
+1 -4
View File
@@ -14,6 +14,7 @@ import type {
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
import { renderOsc8Link } from '../utils/hyperlink';
import { shouldInsertInput } from '../utils/input-guards';
function isValidHttpUrl(url: string): boolean {
@@ -60,10 +61,6 @@ function buildMetadata(widget: WidgetItem, urlValue: string, textValue: string):
};
}
function renderOsc8Link(url: string, text: string): string {
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
}
function getLinkLabel(item: WidgetItem): { url: string; label: string } {
const url = item.metadata?.url?.trim() ?? '';
const metadataText = item.metadata?.text?.trim();
+3 -5
View File
@@ -16,6 +16,7 @@ import {
cycleUsageDisplayMode,
getUsageDisplayMode,
getUsageDisplayModifierText,
getUsagePercentCustomKeybinds,
getUsageProgressBarWidth,
isUsageInverted,
isUsageProgressMode,
@@ -81,11 +82,8 @@ export class SessionUsageWidget implements Widget {
return formatRawOrLabeledValue(item, 'Session: ', `${percent.toFixed(1)}%`);
}
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
];
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
return getUsagePercentCustomKeybinds(item);
}
supportsRawValue(): boolean { return true; }
+212
View File
@@ -0,0 +1,212 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
Widget,
WidgetEditorDisplay,
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
import type { WidgetHookDef } from '../utils/hooks';
import { shouldInsertInput } from '../utils/input-guards';
import { makeModifierText } from './shared/editor-display';
import {
isMetadataFlagEnabled,
removeMetadataKeys,
toggleMetadataFlag
} from './shared/metadata';
type Mode = 'current' | 'count' | 'list';
const MODES: Mode[] = ['current', 'count', 'list'];
const MODE_LABELS: Record<Mode, string> = { current: 'last used', count: 'total count', list: 'unique list' };
const HIDE_WHEN_EMPTY_KEY = 'hideWhenEmpty';
const LIST_LIMIT_KEY = 'listLimit';
const TOGGLE_HIDE_EMPTY_ACTION = 'toggle-hide-empty';
const EDIT_LIST_LIMIT_ACTION = 'edit-list-limit';
function parseListLimit(item: WidgetItem): number {
const parsed = parseInt(item.metadata?.[LIST_LIMIT_KEY] ?? '0', 10);
if (Number.isNaN(parsed) || parsed < 0) {
return 0;
}
return parsed;
}
function setListLimit(item: WidgetItem, limit: number): WidgetItem {
if (limit <= 0) {
const { [LIST_LIMIT_KEY]: removedLimit, ...restMetadata } = item.metadata ?? {};
void removedLimit;
return {
...item,
metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
};
}
return {
...item,
metadata: {
...item.metadata,
[LIST_LIMIT_KEY]: limit.toString()
}
};
}
export class SkillsWidget implements Widget {
getDefaultColor(): string { return 'magenta'; }
getDescription(): string { return 'Shows Claude Code skill invocations from hook data'; }
getDisplayName(): string { return 'Skills'; }
getCategory(): string { return 'Session'; }
supportsRawValue(): boolean { return true; }
supportsColors(): boolean { return true; }
getHooks(): WidgetHookDef[] {
return [
{ event: 'PreToolUse', matcher: 'Skill' },
{ event: 'UserPromptSubmit' }
];
}
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
const keybinds: CustomKeybind[] = [
{ key: 'v', label: '(v)iew: last/count/list', action: 'cycle-mode' },
{ key: 'h', label: '(h)ide when empty', action: TOGGLE_HIDE_EMPTY_ACTION }
];
if (item && this.getMode(item) === 'list') {
keybinds.push({ key: 'l', label: '(l)imit', action: EDIT_LIST_LIMIT_ACTION });
}
return keybinds;
}
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const modifiers = [MODE_LABELS[this.getMode(item)]];
if (this.getMode(item) === 'list') {
const limit = parseListLimit(item);
if (limit > 0) {
modifiers.push(`limit: ${limit}`);
}
}
if (this.isHideWhenEmptyEnabled(item)) {
modifiers.push('hide when empty');
}
return { displayText: 'Skills', modifierText: makeModifierText(modifiers) };
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'cycle-mode') {
const next = MODES[(MODES.indexOf(this.getMode(item)) + 1) % MODES.length] ?? 'current';
const nextItem = next === 'list' ? item : removeMetadataKeys(item, [LIST_LIMIT_KEY]);
return { ...nextItem, metadata: { ...nextItem.metadata, mode: next } };
}
if (action === TOGGLE_HIDE_EMPTY_ACTION) {
return toggleMetadataFlag(item, HIDE_WHEN_EMPTY_KEY);
}
return null;
}
renderEditor(props: WidgetEditorProps): React.ReactElement {
return <SkillsEditor {...props} />;
}
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
const mode = this.getMode(item);
const raw = item.rawValue;
const hideWhenEmpty = this.isHideWhenEmptyEnabled(item);
if (context.isPreview) {
if (mode === 'current') {
return raw ? 'commit' : 'Skill: commit';
}
if (mode === 'count') {
return raw ? '5' : 'Skills: 5';
}
return raw ? 'commit, review-pr' : 'Skills: commit, review-pr';
}
if (mode === 'current') {
const currentSkill = context.skillsMetrics?.lastSkill;
if (!currentSkill) {
if (hideWhenEmpty) {
return null;
}
return raw ? 'none' : 'Skill: none';
}
return raw ? currentSkill : `Skill: ${currentSkill}`;
}
if (mode === 'count') {
const total = context.skillsMetrics?.totalInvocations ?? 0;
if (hideWhenEmpty && total === 0) {
return null;
}
return raw ? String(total) : `Skills: ${total}`;
}
const uniqueSkills = context.skillsMetrics?.uniqueSkills ?? [];
if (uniqueSkills.length === 0) {
if (hideWhenEmpty) {
return null;
}
return raw ? 'none' : 'Skills: none';
}
const limit = parseListLimit(item);
const visibleSkills = limit > 0 ? uniqueSkills.slice(0, limit) : uniqueSkills;
const list = visibleSkills.join(', ');
return raw ? list : `Skills: ${list}`;
}
private getMode(item: WidgetItem): Mode {
const mode = item.metadata?.mode;
return mode && MODES.includes(mode as Mode) ? mode as Mode : 'current';
}
private isHideWhenEmptyEnabled(item: WidgetItem): boolean {
return isMetadataFlagEnabled(item, HIDE_WHEN_EMPTY_KEY);
}
}
const SkillsEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete, onCancel, action }) => {
const [limitInput, setLimitInput] = useState(() => parseListLimit(widget).toString());
useInput((input, key) => {
if (action !== EDIT_LIST_LIMIT_ACTION) {
return;
}
if (key.return) {
const parsed = parseInt(limitInput, 10);
const limit = Number.isNaN(parsed) || parsed < 0 ? 0 : parsed;
onComplete(setListLimit(widget, limit));
} else if (key.escape) {
onCancel();
} else if (key.backspace) {
setLimitInput(limitInput.slice(0, -1));
} else if (shouldInsertInput(input, key) && /\d/.test(input)) {
setLimitInput(limitInput + input);
}
});
if (action === EDIT_LIST_LIMIT_ACTION) {
return (
<Box flexDirection='column'>
<Box>
<Text>Enter max skills to show (0 for unlimited): </Text>
<Text>{limitInput}</Text>
<Text backgroundColor='gray' color='black'>{' '}</Text>
</Box>
<Text dimColor>Press Enter to save, ESC to cancel</Text>
</Box>
);
}
return <Text>Unknown editor mode</Text>;
};
+66
View File
@@ -0,0 +1,66 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { loadClaudeSettingsSync } from '../utils/claude-settings';
import { getTranscriptThinkingEffort } from '../utils/jsonl';
export type ThinkingEffortLevel = 'low' | 'medium' | 'high' | 'max';
/**
* Resolve thinking effort from transcript and settings.
*/
function normalizeThinkingEffort(value: string | undefined): ThinkingEffortLevel | undefined {
if (!value) {
return undefined;
}
const normalized = value.toLowerCase();
if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'max') {
return normalized;
}
return undefined;
}
function resolveThinkingEffortFromSettings(): ThinkingEffortLevel | undefined {
try {
const settings = loadClaudeSettingsSync({ logErrors: false });
return normalizeThinkingEffort(settings.effortLevel);
} catch {
// Settings unavailable, return undefined
}
return undefined;
}
function resolveThinkingEffort(context: RenderContext): ThinkingEffortLevel {
return getTranscriptThinkingEffort(context.data?.transcript_path)
?? resolveThinkingEffortFromSettings()
?? 'medium';
}
export class ThinkingEffortWidget implements Widget {
getDefaultColor(): string { return 'magenta'; }
getDescription(): string { return 'Displays the current thinking effort level (low, medium, high, max).\nMay be incorrect when multiple Claude Code sessions are running due to current Claude Code limitations.'; }
getDisplayName(): string { return 'Thinking Effort'; }
getCategory(): string { return 'Core'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? 'high' : 'Thinking: high';
}
const effort = resolveThinkingEffort(context);
return item.rawValue ? effort : `Thinking: ${effort}`;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+137
View File
@@ -0,0 +1,137 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
const VIM_ICON = 'v';
const VIM_NERD_FONT_ICON = '\uE62B';
const FORMATS = ['icon-dash-letter', 'icon-letter', 'icon', 'letter', 'word'] as const;
type VimFormat = typeof FORMATS[number];
const DEFAULT_FORMAT: VimFormat = 'icon-dash-letter';
const CYCLE_FORMAT_ACTION = 'cycle-format';
const TOGGLE_NERD_FONT_ACTION = 'toggle-nerd-font';
const NERD_FONT_METADATA_KEY = 'nerdFont';
function getFormat(item: WidgetItem): VimFormat {
const f = item.metadata?.format;
return (FORMATS as readonly string[]).includes(f ?? '') ? (f as VimFormat) : DEFAULT_FORMAT;
}
function setFormat(item: WidgetItem, format: VimFormat): WidgetItem {
if (format === DEFAULT_FORMAT) {
const { format: removedFormat, ...restMetadata } = item.metadata ?? {};
void removedFormat;
return {
...item,
metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
};
}
return {
...item,
metadata: {
...(item.metadata ?? {}),
format
}
};
}
function isNerdFontEnabled(item: WidgetItem): boolean {
return item.metadata?.[NERD_FONT_METADATA_KEY] === 'true';
}
function toggleNerdFont(item: WidgetItem): WidgetItem {
if (!isNerdFontEnabled(item)) {
return {
...item,
metadata: {
...(item.metadata ?? {}),
[NERD_FONT_METADATA_KEY]: 'true'
}
};
}
const { [NERD_FONT_METADATA_KEY]: removedNerdFont, ...restMetadata } = item.metadata ?? {};
void removedNerdFont;
return {
...item,
metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
};
}
function formatMode(mode: string, format: VimFormat, icon: string): string {
const letter = mode === 'NORMAL' ? 'N' : mode === 'INSERT' ? 'I' : (mode[0] ?? mode);
switch (format) {
case 'icon-dash-letter': return `${icon}-${letter}`;
case 'icon-letter': return `${icon} ${letter}`;
case 'icon': return icon;
case 'letter': return letter;
case 'word': return mode;
}
}
export class VimModeWidget implements Widget {
getDefaultColor(): string { return 'green'; }
getDescription(): string { return 'Displays current vim editor mode'; }
getDisplayName(): string { return 'Vim Mode'; }
getCategory(): string { return 'Core'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const modifiers: string[] = [getFormat(item)];
if (isNerdFontEnabled(item)) {
modifiers.push('nerd font');
}
return {
displayText: this.getDisplayName(),
modifierText: `(${modifiers.join(', ')})`
};
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === CYCLE_FORMAT_ACTION) {
const currentFormat = getFormat(item);
const nextFormat = FORMATS[(FORMATS.indexOf(currentFormat) + 1) % FORMATS.length] ?? DEFAULT_FORMAT;
return setFormat(item, nextFormat);
}
if (action === TOGGLE_NERD_FONT_ACTION) {
return toggleNerdFont(item);
}
return null;
}
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
const format = getFormat(item);
const icon = isNerdFontEnabled(item) ? VIM_NERD_FONT_ICON : VIM_ICON;
if (context.isPreview)
return formatMode('NORMAL', format, icon);
const mode = context.data?.vim?.mode;
if (mode === undefined)
return null;
return formatMode(mode, format, icon);
}
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'f', label: '(f)ormat', action: CYCLE_FORMAT_ACTION },
{ key: 'n', label: '(n)erd font', action: TOGGLE_NERD_FONT_ACTION }
];
}
supportsRawValue(): boolean { return false; }
supportsColors(_item: WidgetItem): boolean { return true; }
}
+67 -10
View File
@@ -12,14 +12,21 @@ import {
resolveWeeklyUsageWindow
} from '../utils/usage';
import { makeModifierText } from './shared/editor-display';
import {
isMetadataFlagEnabled,
toggleMetadataFlag
} from './shared/metadata';
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
import {
cycleUsageDisplayMode,
getUsageDisplayMode,
getUsageDisplayModifierText,
getUsageProgressBarWidth,
getUsageTimerCustomKeybinds,
isUsageCompact,
isUsageInverted,
isUsageProgressMode,
toggleUsageCompact,
toggleUsageInverted
} from './shared/usage-display';
@@ -30,6 +37,43 @@ function makeTimerProgressBar(percent: number, width: number): string {
return '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
}
const WEEKLY_PREVIEW_DURATION_MS = 36.5 * 60 * 60 * 1000;
function isWeeklyResetHoursOnly(item: WidgetItem): boolean {
return isMetadataFlagEnabled(item, 'hours');
}
function toggleWeeklyResetHoursOnly(item: WidgetItem): WidgetItem {
return toggleMetadataFlag(item, 'hours');
}
function getWeeklyResetModifierText(item: WidgetItem): string | undefined {
const displayMode = getUsageDisplayMode(item);
const modifiers: string[] = [];
if (displayMode === 'progress') {
modifiers.push('progress bar');
} else if (displayMode === 'progress-short') {
modifiers.push('short bar');
}
if (isUsageInverted(item)) {
modifiers.push('inverted');
}
if (!isUsageProgressMode(displayMode)) {
if (isUsageCompact(item)) {
modifiers.push('compact');
}
if (isWeeklyResetHoursOnly(item)) {
modifiers.push('hours only');
}
}
return makeModifierText(modifiers);
}
export class WeeklyResetTimerWidget implements Widget {
getDefaultColor(): string { return 'brightBlue'; }
getDescription(): string { return 'Shows time remaining until weekly usage reset'; }
@@ -39,25 +83,35 @@ export class WeeklyResetTimerWidget implements Widget {
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return {
displayText: this.getDisplayName(),
modifierText: getUsageDisplayModifierText(item)
modifierText: getWeeklyResetModifierText(item)
};
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'toggle-progress') {
return cycleUsageDisplayMode(item);
return cycleUsageDisplayMode(item, ['compact', 'hours']);
}
if (action === 'toggle-invert') {
return toggleUsageInverted(item);
}
if (action === 'toggle-compact') {
return toggleUsageCompact(item);
}
if (action === 'toggle-hours') {
return toggleWeeklyResetHoursOnly(item);
}
return null;
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const displayMode = getUsageDisplayMode(item);
const inverted = isUsageInverted(item);
const compact = isUsageCompact(item);
const useDays = !isWeeklyResetHoursOnly(item);
if (context.isPreview) {
const previewPercent = inverted ? 90.0 : 10.0;
@@ -68,7 +122,7 @@ export class WeeklyResetTimerWidget implements Widget {
return formatRawOrLabeledValue(item, 'Weekly Reset ', `[${progressBar}] ${previewPercent.toFixed(1)}%`);
}
return formatRawOrLabeledValue(item, 'Weekly Reset: ', '36hr 30m');
return formatRawOrLabeledValue(item, 'Weekly Reset: ', formatUsageDuration(WEEKLY_PREVIEW_DURATION_MS, compact, useDays));
}
const usageData = context.usageData ?? {};
@@ -90,15 +144,18 @@ export class WeeklyResetTimerWidget implements Widget {
return formatRawOrLabeledValue(item, 'Weekly Reset ', `[${progressBar}] ${percentage}%`);
}
const remainingTime = formatUsageDuration(window.remainingMs);
const remainingTime = formatUsageDuration(window.remainingMs, compact, useDays);
return formatRawOrLabeledValue(item, 'Weekly Reset: ', remainingTime);
}
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
];
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
const keybinds = getUsageTimerCustomKeybinds(item);
if (!item || !isUsageProgressMode(getUsageDisplayMode(item))) {
keybinds.push({ key: 'h', label: '(h)ours only', action: 'toggle-hours' });
}
return keybinds;
}
supportsRawValue(): boolean { return true; }
+3 -5
View File
@@ -16,6 +16,7 @@ import {
cycleUsageDisplayMode,
getUsageDisplayMode,
getUsageDisplayModifierText,
getUsagePercentCustomKeybinds,
getUsageProgressBarWidth,
isUsageInverted,
isUsageProgressMode,
@@ -81,11 +82,8 @@ export class WeeklyUsageWidget implements Widget {
return formatRawOrLabeledValue(item, 'Weekly: ', `${percent.toFixed(1)}%`);
}
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
];
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
return getUsagePercentCustomKeybinds(item);
}
supportsRawValue(): boolean { return true; }
+28 -1
View File
@@ -10,6 +10,7 @@ import {
import type { RenderContext } from '../../types/RenderContext';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import { renderOsc8Link } from '../../utils/hyperlink';
import { GitBranchWidget } from '../GitBranch';
vi.mock('child_process', () => ({ execSync: vi.fn() }));
@@ -25,6 +26,8 @@ function render(options: {
cwd?: string;
hideNoGit?: boolean;
isPreview?: boolean;
linkToGitHub?: boolean;
metadata?: Record<string, string>;
rawValue?: boolean;
} = {}) {
const widget = new GitBranchWidget();
@@ -32,11 +35,16 @@ function render(options: {
isPreview: options.isPreview,
data: options.cwd ? { cwd: options.cwd } : undefined
};
const metadata = {
...options.metadata,
...(options.hideNoGit ? { hideNoGit: 'true' } : {}),
...(options.linkToGitHub ? { linkToGitHub: 'true' } : {})
};
const item: WidgetItem = {
id: 'git-branch',
type: 'git-branch',
rawValue: options.rawValue,
metadata: options.hideNoGit ? { hideNoGit: 'true' } : undefined
metadata: Object.keys(metadata).length > 0 ? metadata : undefined
};
return widget.render(item, context, DEFAULT_SETTINGS);
@@ -79,6 +87,17 @@ describe('GitBranchWidget', () => {
expect(render({ rawValue: true })).toBe('feature/worktree');
});
it('should render encoded GitHub branch links', () => {
mockExecSync.mockReturnValueOnce('true\n');
mockExecSync.mockReturnValueOnce('feature/issue#1');
mockExecSync.mockReturnValueOnce('ssh://git@github.com/owner/repo.git');
expect(render({ linkToGitHub: true })).toBe(renderOsc8Link(
'https://github.com/owner/repo/tree/feature/issue%231',
'⎇ feature/issue#1'
));
});
it('should render no git when probe returns false', () => {
mockExecSync.mockReturnValue('false\n');
@@ -103,4 +122,12 @@ describe('GitBranchWidget', () => {
expect(render()).toBe('⎇ no git');
});
it('should keep plain text when GitHub remote cannot be parsed', () => {
mockExecSync.mockReturnValueOnce('true\n');
mockExecSync.mockReturnValueOnce('feature/worktree');
mockExecSync.mockReturnValueOnce('https://gitlab.com/owner/repo.git');
expect(render({ linkToGitHub: true })).toBe('⎇ feature/worktree');
});
});
+93
View File
@@ -10,6 +10,10 @@ import {
import type { RenderContext } from '../../types/RenderContext';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import {
buildIdeFileUrl,
renderOsc8Link
} from '../../utils/hyperlink';
import { GitRootDirWidget } from '../GitRootDir';
vi.mock('child_process', () => ({ execSync: vi.fn() }));
@@ -45,6 +49,19 @@ describe('GitRootDirWidget', () => {
expect(render({ isPreview: true })).toBe('my-repo');
});
it('should render preview for vscode IDE links', () => {
const widget = new GitRootDirWidget();
expect(widget.render({
id: 'git-root-dir',
type: 'git-root-dir',
metadata: { linkToIDE: 'vscode' }
}, { isPreview: true }, DEFAULT_SETTINGS)).toBe(renderOsc8Link(
buildIdeFileUrl('/Users/example/my-repo', 'vscode'),
'my-repo'
));
});
it('should render root directory name', () => {
mockExecSync.mockReturnValueOnce('true\n');
mockExecSync.mockReturnValueOnce('/some/path/my-repo');
@@ -83,6 +100,36 @@ describe('GitRootDirWidget', () => {
expect(render()).toBe('C:');
});
it('should render encoded vscode IDE links for repository roots', () => {
const widget = new GitRootDirWidget();
mockExecSync.mockReturnValueOnce('true\n');
mockExecSync.mockReturnValueOnce('C:/Work/my repo#1');
expect(widget.render({
id: 'git-root-dir',
type: 'git-root-dir',
metadata: { linkToIDE: 'vscode' }
}, {}, DEFAULT_SETTINGS)).toBe(renderOsc8Link(
buildIdeFileUrl('C:/Work/my repo#1', 'vscode'),
'my repo#1'
));
});
it('should continue honoring legacy cursor link metadata', () => {
const widget = new GitRootDirWidget();
mockExecSync.mockReturnValueOnce('true\n');
mockExecSync.mockReturnValueOnce('/some/path/my repo#1');
expect(widget.render({
id: 'git-root-dir',
type: 'git-root-dir',
metadata: { linkToCursor: 'true' }
}, {}, DEFAULT_SETTINGS)).toBe(renderOsc8Link(
buildIdeFileUrl('/some/path/my repo#1', 'cursor'),
'my repo#1'
));
});
it('should render no git when probe returns false', () => {
mockExecSync.mockReturnValue('false\n');
@@ -101,6 +148,52 @@ describe('GitRootDirWidget', () => {
expect(render({ hideNoGit: true })).toBeNull();
});
it('should cycle IDE link modes in the editor', () => {
const widget = new GitRootDirWidget();
const base: WidgetItem = { id: 'git-root-dir', type: 'git-root-dir' };
const vscode = widget.handleEditorAction('toggle-link', base);
const cursor = widget.handleEditorAction('toggle-link', vscode ?? base);
const cleared = widget.handleEditorAction('toggle-link', cursor ?? base);
expect(vscode?.metadata?.linkToIDE).toBe('vscode');
expect(cursor?.metadata?.linkToIDE).toBe('cursor');
expect(cleared?.metadata?.linkToIDE).toBeUndefined();
});
it('should migrate legacy cursor metadata when cycling IDE link modes', () => {
const widget = new GitRootDirWidget();
const updated = widget.handleEditorAction('toggle-link', {
id: 'git-root-dir',
type: 'git-root-dir',
metadata: { linkToCursor: 'true' }
});
expect(updated?.metadata?.linkToCursor).toBeUndefined();
expect(updated?.metadata?.linkToIDE).toBeUndefined();
});
it('should show IDE link modifier text in the editor display', () => {
const widget = new GitRootDirWidget();
const display = widget.getEditorDisplay({
id: 'git-root-dir',
type: 'git-root-dir',
metadata: { linkToIDE: 'cursor' }
});
expect(display.modifierText).toBe('(link-cursor)');
});
it('should expose IDE link keybind', () => {
const widget = new GitRootDirWidget();
expect(widget.getCustomKeybinds()).toContainEqual({
key: 'l',
label: '(l)ink to IDE',
action: 'toggle-link'
});
});
it('should disable raw value support', () => {
const widget = new GitRootDirWidget();
@@ -32,9 +32,9 @@ const cases: { name: string; itemType: string; widget: GitWidget }[] = [
describe('Git widget shared behavior', () => {
it.each(cases)('$name should expose hide-no-git keybind', ({ widget }) => {
expect(widget.getCustomKeybinds()).toEqual([
expect(widget.getCustomKeybinds()).toContainEqual(
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
]);
);
});
it.each(cases)('$name should toggle hideNoGit metadata', ({ widget, itemType }) => {
+150
View File
@@ -0,0 +1,150 @@
import {
describe,
expect,
it
} from 'vitest';
import type {
RenderContext,
WidgetItem
} from '../../types';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import { SkillsWidget } from '../Skills';
function render(item: WidgetItem, context: RenderContext): string | null {
return new SkillsWidget().render(item, context, DEFAULT_SETTINGS);
}
describe('SkillsWidget', () => {
it('uses v as the mode toggle keybind', () => {
const widget = new SkillsWidget();
expect(widget.getCustomKeybinds({ id: 'skills', type: 'skills' })).toEqual([
{ key: 'v', label: '(v)iew: last/count/list', action: 'cycle-mode' },
{ key: 'h', label: '(h)ide when empty', action: 'toggle-hide-empty' }
]);
expect(widget.getCustomKeybinds({
id: 'skills',
type: 'skills',
metadata: { mode: 'list' }
})).toEqual([
{ key: 'v', label: '(v)iew: last/count/list', action: 'cycle-mode' },
{ key: 'h', label: '(h)ide when empty', action: 'toggle-hide-empty' },
{ key: 'l', label: '(l)imit', action: 'edit-list-limit' }
]);
});
it('cycles mode current -> count -> list -> current', () => {
const widget = new SkillsWidget();
const base: WidgetItem = { id: 'skills', type: 'skills' };
const count = widget.handleEditorAction('cycle-mode', base);
const list = widget.handleEditorAction('cycle-mode', count ?? base);
const current = widget.handleEditorAction('cycle-mode', list ?? base);
expect(count?.metadata?.mode).toBe('count');
expect(list?.metadata?.mode).toBe('list');
expect(current?.metadata?.mode).toBe('current');
});
it('clears list limit metadata when leaving list mode', () => {
const widget = new SkillsWidget();
const updated = widget.handleEditorAction('cycle-mode', {
id: 'skills',
type: 'skills',
metadata: {
mode: 'list',
listLimit: '2'
}
});
expect(updated?.metadata?.mode).toBe('current');
expect(updated?.metadata?.listLimit).toBeUndefined();
});
it('toggles hide-when-empty metadata', () => {
const widget = new SkillsWidget();
const base: WidgetItem = { id: 'skills', type: 'skills' };
const hidden = widget.handleEditorAction('toggle-hide-empty', base);
const shown = widget.handleEditorAction('toggle-hide-empty', hidden ?? base);
expect(hidden?.metadata?.hideWhenEmpty).toBe('true');
expect(shown?.metadata?.hideWhenEmpty).toBe('false');
});
it('shows hide-when-empty in editor modifier text when enabled', () => {
const widget = new SkillsWidget();
const display = widget.getEditorDisplay({
id: 'skills',
type: 'skills',
metadata: { hideWhenEmpty: 'true' }
});
expect(display.modifierText).toBe('(last used, hide when empty)');
});
it('shows list limit in editor modifier text when configured', () => {
const widget = new SkillsWidget();
const display = widget.getEditorDisplay({
id: 'skills',
type: 'skills',
metadata: { mode: 'list', listLimit: '2' }
});
expect(display.modifierText).toBe('(unique list, limit: 2)');
});
it('renders current, count, and list modes from skills metrics', () => {
const context: RenderContext = {
skillsMetrics: {
totalInvocations: 3,
uniqueSkills: ['commit', 'review-pr'],
lastSkill: 'review-pr'
}
};
expect(render({ id: 'skills', type: 'skills' }, context)).toBe('Skill: review-pr');
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'count' } }, context)).toBe('Skills: 3');
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'list' } }, context)).toBe('Skills: commit, review-pr');
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'list', listLimit: '1' } }, context)).toBe('Skills: commit');
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'list', listLimit: '0' } }, context)).toBe('Skills: commit, review-pr');
});
it('shows non-hidden empty outputs by default', () => {
const context: RenderContext = {
skillsMetrics: {
totalInvocations: 0,
uniqueSkills: [],
lastSkill: null
}
};
expect(render({ id: 'skills', type: 'skills' }, context)).toBe('Skill: none');
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'count' } }, context)).toBe('Skills: 0');
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'list' } }, context)).toBe('Skills: none');
});
it('hides empty outputs when hide-when-empty is enabled', () => {
const context: RenderContext = {
skillsMetrics: {
totalInvocations: 0,
uniqueSkills: [],
lastSkill: null
}
};
expect(render({
id: 'skills',
type: 'skills',
metadata: { hideWhenEmpty: 'true' }
}, context)).toBeNull();
expect(render({
id: 'skills',
type: 'skills',
metadata: { mode: 'count', hideWhenEmpty: 'true' }
}, context)).toBeNull();
expect(render({
id: 'skills',
type: 'skills',
metadata: { mode: 'list', hideWhenEmpty: 'true' }
}, context)).toBeNull();
});
});
@@ -0,0 +1,209 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Mock } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi
} from 'vitest';
import type {
RenderContext,
WidgetItem
} from '../../types';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import { loadClaudeSettingsSync } from '../../utils/claude-settings';
import { ThinkingEffortWidget } from '../ThinkingEffort';
// Mock claude-settings to avoid filesystem reads in tests
vi.mock('../../utils/claude-settings', () => ({ loadClaudeSettingsSync: vi.fn() }));
const mockedLoadSettings = loadClaudeSettingsSync as Mock;
const MODEL_WITH_HIGH_EFFORT = '<local-command-stdout>Set model to \u001b[1mopus (claude-opus-4-6)\u001b[22m with \u001b[1mhigh\u001b[22m effort</local-command-stdout>';
const MODEL_WITH_LOW_EFFORT = '<local-command-stdout>Set model to \u001b[1msonnet (claude-sonnet-4-5)\u001b[22m with \u001b[1mlow\u001b[22m effort</local-command-stdout>';
const MODEL_WITH_MAX_EFFORT = '<local-command-stdout>Set model to \u001b[1mopus (claude-opus-4-6)\u001b[22m with \u001b[1mmax\u001b[22m effort</local-command-stdout>';
const MODEL_WITHOUT_EFFORT = '<local-command-stdout>Set model to \u001b[1msonnet (claude-sonnet-4-5)\u001b[22m</local-command-stdout>';
let tempDir: string;
function makeTranscriptEntry(content: string): string {
return JSON.stringify({
type: 'user',
message: {
role: 'user',
content
}
});
}
function render(options: {
transcriptPath?: string;
fileContent?: string | null | undefined;
rawValue?: boolean;
isPreview?: boolean;
settingsValue?: unknown;
} = {}): string | null {
const {
transcriptPath = options.fileContent !== undefined ? path.join(tempDir, 'session.jsonl') : undefined,
fileContent,
rawValue = false,
isPreview = false,
settingsValue = {}
} = options;
const widget = new ThinkingEffortWidget();
const context: RenderContext = {
data: transcriptPath ? { transcript_path: transcriptPath } : undefined,
isPreview
};
const item: WidgetItem = {
id: 'thinking-effort',
type: 'thinking-effort',
rawValue
};
mockedLoadSettings.mockReturnValue(settingsValue as never);
if (transcriptPath && fileContent !== undefined && fileContent !== null) {
fs.writeFileSync(transcriptPath, fileContent, 'utf-8');
}
return widget.render(item, context, DEFAULT_SETTINGS);
}
describe('ThinkingEffortWidget', () => {
beforeEach(() => {
vi.clearAllMocks();
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-thinking-effort-'));
mockedLoadSettings.mockReturnValue({} as never);
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('metadata', () => {
it('has correct display name', () => {
const widget = new ThinkingEffortWidget();
expect(widget.getDisplayName()).toBe('Thinking Effort');
});
it('has correct category', () => {
const widget = new ThinkingEffortWidget();
expect(widget.getCategory()).toBe('Core');
});
it('supports raw value', () => {
const widget = new ThinkingEffortWidget();
expect(widget.supportsRawValue()).toBe(true);
});
it('supports colors', () => {
const widget = new ThinkingEffortWidget();
expect(widget.supportsColors({ type: 'thinking-effort' } as never)).toBe(true);
});
});
describe('preview mode', () => {
it('returns labelled preview', () => {
const result = render({ isPreview: true });
expect(result).toBe('Thinking: high');
});
it('returns raw preview', () => {
const result = render({ isPreview: true, rawValue: true });
expect(result).toBe('high');
});
});
describe('transcript source', () => {
it('reads effort from the latest /model transcript stdout', () => {
const result = render({
fileContent: makeTranscriptEntry(MODEL_WITH_HIGH_EFFORT),
settingsValue: { effortLevel: 'low' }
});
expect(result).toBe('Thinking: high');
});
it('returns raw transcript effort when requested', () => {
const result = render({
fileContent: makeTranscriptEntry(MODEL_WITH_LOW_EFFORT),
rawValue: true
});
expect(result).toBe('low');
});
it('supports max effort from transcript output', () => {
const result = render({ fileContent: makeTranscriptEntry(MODEL_WITH_MAX_EFFORT) });
expect(result).toBe('Thinking: max');
});
it('does not keep stale transcript effort when a newer /model output has no effort', () => {
const result = render({
fileContent: [
makeTranscriptEntry(MODEL_WITH_HIGH_EFFORT),
makeTranscriptEntry('<local-command-stdout>Bye!</local-command-stdout>'),
makeTranscriptEntry(MODEL_WITHOUT_EFFORT)
].join('\n'),
settingsValue: { effortLevel: 'medium' }
});
expect(result).toBe('Thinking: medium');
});
});
describe('Claude settings fallback', () => {
it('falls back to effortLevel when the latest /model output has no effort', () => {
const result = render({
fileContent: makeTranscriptEntry(MODEL_WITHOUT_EFFORT),
settingsValue: { effortLevel: 'high' }
});
expect(result).toBe('Thinking: high');
});
it('falls back to effortLevel when the transcript is unavailable', () => {
const result = render({
transcriptPath: path.join(tempDir, 'missing.jsonl'),
fileContent: null,
settingsValue: { effortLevel: 'high' }
});
expect(result).toBe('Thinking: high');
});
it('handles case-insensitive effortLevel', () => {
const result = render({ settingsValue: { effortLevel: 'HIGH' } });
expect(result).toBe('Thinking: high');
});
it('supports max effortLevel', () => {
const result = render({ settingsValue: { effortLevel: 'max' } });
expect(result).toBe('Thinking: max');
});
it('defaults to medium when effortLevel is not set', () => {
const result = render();
expect(result).toBe('Thinking: medium');
});
it('defaults to medium when effortLevel is invalid', () => {
const result = render({ settingsValue: { effortLevel: 'ultra' } });
expect(result).toBe('Thinking: medium');
});
it('defaults to medium when settings read fails', () => {
mockedLoadSettings.mockImplementation(() => {
throw new Error('settings unavailable');
});
const result = render();
expect(result).toBe('Thinking: medium');
});
it('defaults to medium when the latest /model output has no effort and settings are missing', () => {
const result = render({ fileContent: makeTranscriptEntry(MODEL_WITHOUT_EFFORT) });
expect(result).toBe('Thinking: medium');
});
});
});
+147
View File
@@ -0,0 +1,147 @@
import {
describe,
expect,
it
} from 'vitest';
import type {
RenderContext,
WidgetItem
} from '../../types';
import { DEFAULT_SETTINGS } from '../../types/Settings';
import { VimModeWidget } from '../VimMode';
const ITEM: WidgetItem = { id: 'vim-mode', type: 'vim-mode' };
function makeContext(overrides: Partial<RenderContext> = {}): RenderContext {
return { ...overrides };
}
describe('VimModeWidget', () => {
describe('editor configuration', () => {
it('uses f as the format toggle keybind', () => {
expect(new VimModeWidget().getCustomKeybinds()).toEqual([
{ key: 'f', label: '(f)ormat', action: 'cycle-format' },
{ key: 'n', label: '(n)erd font', action: 'toggle-nerd-font' }
]);
});
it('defaults to icon-dash-letter in the editor display', () => {
expect(new VimModeWidget().getEditorDisplay(ITEM)).toEqual({
displayText: 'Vim Mode',
modifierText: '(icon-dash-letter)'
});
});
it('shows nerd font in the editor display when enabled', () => {
expect(new VimModeWidget().getEditorDisplay({
...ITEM,
metadata: { nerdFont: 'true' }
})).toEqual({
displayText: 'Vim Mode',
modifierText: '(icon-dash-letter, nerd font)'
});
});
it('cycles icon-dash-letter -> icon-letter -> icon -> letter -> word -> icon-dash-letter', () => {
const widget = new VimModeWidget();
const iconLetter = widget.handleEditorAction('cycle-format', ITEM);
const icon = widget.handleEditorAction('cycle-format', iconLetter ?? ITEM);
const letter = widget.handleEditorAction('cycle-format', icon ?? ITEM);
const word = widget.handleEditorAction('cycle-format', letter ?? ITEM);
const iconDashLetter = widget.handleEditorAction('cycle-format', word ?? ITEM);
expect(iconLetter?.metadata?.format).toBe('icon-letter');
expect(icon?.metadata?.format).toBe('icon');
expect(letter?.metadata?.format).toBe('letter');
expect(word?.metadata?.format).toBe('word');
expect(iconDashLetter?.metadata?.format).toBeUndefined();
});
it('toggles nerd font metadata on and off', () => {
const widget = new VimModeWidget();
const enabled = widget.handleEditorAction('toggle-nerd-font', ITEM);
const disabled = widget.handleEditorAction('toggle-nerd-font', enabled ?? ITEM);
expect(enabled?.metadata?.nerdFont).toBe('true');
expect(disabled?.metadata?.nerdFont).toBeUndefined();
});
});
describe('metadata', () => {
it('has correct display name', () => {
expect(new VimModeWidget().getDisplayName()).toBe('Vim Mode');
});
it('has correct category', () => {
expect(new VimModeWidget().getCategory()).toBe('Core');
});
it('does not support raw value', () => {
expect(new VimModeWidget().supportsRawValue()).toBe(false);
});
it('supports colors', () => {
expect(new VimModeWidget().supportsColors(ITEM)).toBe(true);
});
});
describe('render()', () => {
it('returns v-N by default', () => {
const ctx = makeContext({ data: { vim: { mode: 'NORMAL' } } });
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBe('v-N');
});
it('returns v-I by default for INSERT', () => {
const ctx = makeContext({ data: { vim: { mode: 'INSERT' } } });
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBe('v-I');
});
it('renders alternate configured formats', () => {
const ctx = makeContext({ data: { vim: { mode: 'NORMAL' } } });
expect(new VimModeWidget().render({
...ITEM,
metadata: { format: 'letter' }
}, ctx, DEFAULT_SETTINGS)).toBe('N');
expect(new VimModeWidget().render({
...ITEM,
metadata: { format: 'word' }
}, ctx, DEFAULT_SETTINGS)).toBe('NORMAL');
});
it('renders the Nerd Font glyph when enabled', () => {
const ctx = makeContext({ data: { vim: { mode: 'NORMAL' } } });
expect(new VimModeWidget().render({
...ITEM,
metadata: { nerdFont: 'true' }
}, ctx, DEFAULT_SETTINGS)).toBe('\uE62B-N');
});
it('returns null when vim field is absent (vim disabled)', () => {
const ctx = makeContext({ data: { model: { id: 'claude-sonnet-4-5' } } });
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBeNull();
});
it('returns null when vim field is null', () => {
const ctx = makeContext({ data: { vim: null } });
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBeNull();
});
it('returns null when context.data is absent', () => {
const ctx = makeContext();
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBeNull();
});
it('returns v-N in preview mode regardless of data', () => {
const ctx = makeContext({ isPreview: true });
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBe('v-N');
});
it('returns v-C for unknown modes by default', () => {
const ctx = makeContext({ data: { vim: { mode: 'COMMAND' } } });
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBe('v-C');
});
});
});
+82 -1
View File
@@ -39,7 +39,17 @@ describe('WeeklyResetTimerWidget', () => {
it('renders preview using weekly reset format', () => {
const widget = new WeeklyResetTimerWidget();
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { isPreview: true })).toBe('Weekly Reset: 36hr 30m');
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { isPreview: true })).toBe('Weekly Reset: 1d 12hr 30m');
});
it('renders preview in hours-only mode when toggled', () => {
const widget = new WeeklyResetTimerWidget();
expect(render(widget, {
id: 'weekly-reset',
type: 'weekly-reset-timer',
metadata: { hours: 'true' }
}, { isPreview: true })).toBe('Weekly Reset: 36hr 30m');
});
it('renders remaining time in time mode', () => {
@@ -55,6 +65,27 @@ describe('WeeklyResetTimerWidget', () => {
mockFormatUsageDuration.mockReturnValue('134hr 40m');
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { usageData: {} })).toBe('Weekly Reset: 134hr 40m');
expect(mockFormatUsageDuration).toHaveBeenCalledWith(484800000, false, true);
});
it('renders remaining time in hours-only mode', () => {
const widget = new WeeklyResetTimerWidget();
mockResolveWeeklyUsageWindow.mockReturnValue({
sessionDurationMs: 604800000,
elapsedMs: 120000000,
remainingMs: 484800000,
elapsedPercent: 19.8412698413,
remainingPercent: 80.1587301587
});
mockFormatUsageDuration.mockReturnValue('134hr 40m');
expect(render(widget, {
id: 'weekly-reset',
type: 'weekly-reset-timer',
metadata: { hours: 'true' }
}, { usageData: {} })).toBe('Weekly Reset: 134hr 40m');
expect(mockFormatUsageDuration).toHaveBeenCalledWith(484800000, false, false);
});
it('renders short progress bar with inverted fill', () => {
@@ -111,10 +142,60 @@ describe('WeeklyResetTimerWidget', () => {
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer', rawValue: true }, { usageData: {} })).toBe('120hr 15m');
});
it('toggles hours-only metadata and shows hours-only modifier text', () => {
const widget = new WeeklyResetTimerWidget();
const baseItem: WidgetItem = { id: 'weekly-reset', type: 'weekly-reset-timer' };
const hoursOnly = widget.handleEditorAction('toggle-hours', baseItem);
const cleared = widget.handleEditorAction('toggle-hours', hoursOnly ?? baseItem);
expect(hoursOnly?.metadata?.hours).toBe('true');
expect(cleared?.metadata?.hours).toBe('false');
expect(widget.getEditorDisplay(baseItem).modifierText).toBeUndefined();
expect(widget.getEditorDisplay({
...baseItem,
metadata: { hours: 'true' }
}).modifierText).toBe('(hours only)');
});
it('clears compact and hours-only metadata when cycling into progress mode', () => {
const widget = new WeeklyResetTimerWidget();
const updated = widget.handleEditorAction('toggle-progress', {
id: 'weekly-reset',
type: 'weekly-reset-timer',
metadata: {
compact: 'true',
hours: 'true'
}
});
expect(updated?.metadata?.display).toBe('progress');
expect(updated?.metadata?.compact).toBeUndefined();
expect(updated?.metadata?.hours).toBeUndefined();
});
it('ignores stale hours-only metadata in progress mode editor modifiers', () => {
const widget = new WeeklyResetTimerWidget();
expect(widget.getEditorDisplay({
id: 'weekly-reset',
type: 'weekly-reset-timer',
metadata: {
display: 'progress',
hours: 'true'
}
}).modifierText).toBe('(progress bar)');
});
runUsageTimerEditorSuite({
baseItem: { id: 'weekly-reset', type: 'weekly-reset-timer' },
createWidget: () => new WeeklyResetTimerWidget(),
expectedDisplayName: 'Weekly Reset Timer',
expectedTimeKeybinds: [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 's', label: '(s)hort time', action: 'toggle-compact' },
{ key: 'h', label: '(h)ours only', action: 'toggle-hours' }
],
expectedModifierText: '(short bar, inverted)',
modifierItem: {
id: 'weekly-reset',
@@ -13,7 +13,7 @@ import type {
} from '../../../types/Widget';
interface UsageWidgetLike {
getCustomKeybinds(): CustomKeybind[];
getCustomKeybinds(item?: WidgetItem): CustomKeybind[];
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay;
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null;
supportsRawValue(): boolean;
@@ -41,11 +41,27 @@ interface UsageTimerEditorSuiteConfig<TWidget extends UsageWidgetLike & { getDis
baseItem: WidgetItem;
createWidget: () => TWidget;
expectedDisplayName: string;
expectedProgressKeybinds?: CustomKeybind[];
expectedModifierText: string;
modifierItem: WidgetItem;
expectedTimeKeybinds?: CustomKeybind[];
}
const EXPECTED_USAGE_KEYBINDS: CustomKeybind[] = [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' }
];
const EXPECTED_USAGE_PROGRESS_KEYBINDS: CustomKeybind[] = [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
];
const EXPECTED_TIMER_TIME_KEYBINDS: CustomKeybind[] = [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 's', label: '(s)hort time', action: 'toggle-compact' }
];
const EXPECTED_TIMER_PROGRESS_KEYBINDS: CustomKeybind[] = [
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
];
@@ -61,11 +77,12 @@ export function runUsagePercentWidgetSuite<TWidget extends UsageWidgetLike>(conf
vi.clearAllMocks();
});
it('exposes progress and invert keybinds', () => {
it('exposes widget-managed keybinds for time and progress modes', () => {
const widget = config.createWidget();
expect(widget.supportsRawValue()).toBe(true);
expect(widget.getCustomKeybinds()).toEqual(EXPECTED_USAGE_KEYBINDS);
expect(widget.getCustomKeybinds(config.baseItem)).toEqual(EXPECTED_USAGE_KEYBINDS);
expect(widget.getCustomKeybinds(config.progressItem)).toEqual(EXPECTED_USAGE_PROGRESS_KEYBINDS);
});
it.each([
@@ -140,6 +157,23 @@ export function runUsagePercentWidgetSuite<TWidget extends UsageWidgetLike>(conf
expect(widget.getEditorDisplay(config.baseItem).modifierText).toBeUndefined();
expect(widget.getEditorDisplay(config.modifierItem).modifierText).toBe(config.expectedModifierText);
});
it('ignores stale compact metadata in editor modifiers', () => {
const widget = config.createWidget();
const modifierItemWithCompact: WidgetItem = {
...config.modifierItem,
metadata: {
...(config.modifierItem.metadata ?? {}),
compact: 'true'
}
};
expect(widget.getEditorDisplay({
...config.baseItem,
metadata: { compact: 'true' }
}).modifierText).toBeUndefined();
expect(widget.getEditorDisplay(modifierItemWithCompact).modifierText).toBe(config.expectedModifierText);
});
}
export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { getDisplayName(): string }>(config: UsageTimerEditorSuiteConfig<TWidget>): void {
@@ -147,12 +181,13 @@ export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { get
vi.clearAllMocks();
});
it('supports raw value and exposes progress/invert keybinds', () => {
it('supports raw value and exposes widget-managed keybinds for time and progress modes', () => {
const widget = config.createWidget();
expect(widget.getDisplayName()).toBe(config.expectedDisplayName);
expect(widget.supportsRawValue()).toBe(true);
expect(widget.getCustomKeybinds()).toEqual(EXPECTED_USAGE_KEYBINDS);
expect(widget.getCustomKeybinds(config.baseItem)).toEqual(config.expectedTimeKeybinds ?? EXPECTED_TIMER_TIME_KEYBINDS);
expect(widget.getCustomKeybinds(config.modifierItem)).toEqual(config.expectedProgressKeybinds ?? EXPECTED_TIMER_PROGRESS_KEYBINDS);
});
it('clears invert metadata when cycling back to time mode', () => {
@@ -181,6 +216,17 @@ export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { get
expect(third?.metadata?.display).toBe('time');
});
it('clears compact metadata when cycling into progress mode', () => {
const widget = config.createWidget();
const updated = widget.handleEditorAction('toggle-progress', {
...config.baseItem,
metadata: { compact: 'true' }
});
expect(updated?.metadata?.display).toBe('progress');
expect(updated?.metadata?.compact).toBeUndefined();
});
it('toggles invert metadata and shows editor modifiers', () => {
const widget = config.createWidget();
@@ -192,4 +238,15 @@ export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { get
expect(widget.getEditorDisplay(config.baseItem).modifierText).toBeUndefined();
expect(widget.getEditorDisplay(config.modifierItem).modifierText).toBe(config.expectedModifierText);
});
it('toggles compact metadata and shows compact modifier text', () => {
const widget = config.createWidget();
const compact = widget.handleEditorAction('toggle-compact', config.baseItem);
const cleared = widget.handleEditorAction('toggle-compact', compact ?? config.baseItem);
expect(compact?.metadata?.compact).toBe('true');
expect(cleared?.metadata?.compact).toBe('false');
expect(widget.getEditorDisplay({ ...config.baseItem, metadata: { compact: 'true' } }).modifierText).toBe('(compact)');
});
}
+4 -1
View File
@@ -32,4 +32,7 @@ export { WeeklyUsageWidget } from './WeeklyUsage';
export { BlockResetTimerWidget } from './BlockResetTimer';
export { WeeklyResetTimerWidget } from './WeeklyResetTimer';
export { ContextBarWidget } from './ContextBar';
export { LinkWidget } from './Link';
export { LinkWidget } from './Link';
export { SkillsWidget } from './Skills';
export { ThinkingEffortWidget } from './ThinkingEffort';
export { VimModeWidget } from './VimMode';
+11
View File
@@ -12,4 +12,15 @@ export function toggleMetadataFlag(item: WidgetItem, key: string): WidgetItem {
[key]: (!isMetadataFlagEnabled(item, key)).toString()
}
};
}
export function removeMetadataKeys(item: WidgetItem, keys: string[]): WidgetItem {
const nextMetadata = Object.fromEntries(
Object.entries(item.metadata ?? {}).filter(([key]) => !keys.includes(key))
);
return {
...item,
metadata: Object.keys(nextMetadata).length > 0 ? nextMetadata : undefined
};
}
+55 -9
View File
@@ -1,13 +1,21 @@
import type { WidgetItem } from '../../types/Widget';
import type {
CustomKeybind,
WidgetItem
} from '../../types/Widget';
import { makeModifierText } from './editor-display';
import {
isMetadataFlagEnabled,
removeMetadataKeys,
toggleMetadataFlag
} from './metadata';
export type UsageDisplayMode = 'time' | 'progress' | 'progress-short';
const PROGRESS_TOGGLE_KEYBIND: CustomKeybind = { key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' };
const INVERT_TOGGLE_KEYBIND: CustomKeybind = { key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' };
const COMPACT_TOGGLE_KEYBIND: CustomKeybind = { key: 's', label: '(s)hort time', action: 'toggle-compact' };
export function getUsageDisplayMode(item: WidgetItem): UsageDisplayMode {
const mode = item.metadata?.display;
if (mode === 'progress' || mode === 'progress-short') {
@@ -28,7 +36,20 @@ export function isUsageInverted(item: WidgetItem): boolean {
return isMetadataFlagEnabled(item, 'invert');
}
export function getUsageDisplayModifierText(item: WidgetItem): string | undefined {
export function isUsageCompact(item: WidgetItem): boolean {
return isMetadataFlagEnabled(item, 'compact');
}
export function toggleUsageCompact(item: WidgetItem): WidgetItem {
return toggleMetadataFlag(item, 'compact');
}
interface UsageDisplayModifierOptions { includeCompact?: boolean }
export function getUsageDisplayModifierText(
item: WidgetItem,
options: UsageDisplayModifierOptions = {}
): string | undefined {
const mode = getUsageDisplayMode(item);
const modifiers: string[] = [];
@@ -42,10 +63,14 @@ export function getUsageDisplayModifierText(item: WidgetItem): string | undefine
modifiers.push('inverted');
}
if (options.includeCompact && !isUsageProgressMode(mode) && isUsageCompact(item)) {
modifiers.push('compact');
}
return makeModifierText(modifiers);
}
export function cycleUsageDisplayMode(item: WidgetItem): WidgetItem {
export function cycleUsageDisplayMode(item: WidgetItem, disabledInProgressKeys: string[] = []): WidgetItem {
const currentMode = getUsageDisplayMode(item);
const nextMode: UsageDisplayMode = currentMode === 'time'
? 'progress'
@@ -53,21 +78,42 @@ export function cycleUsageDisplayMode(item: WidgetItem): WidgetItem {
? 'progress-short'
: 'time';
const nextItem = removeMetadataKeys(item, nextMode === 'time'
? ['invert']
: disabledInProgressKeys);
const nextMetadata: Record<string, string> = {
...(item.metadata ?? {}),
...(nextItem.metadata ?? {}),
display: nextMode
};
if (nextMode === 'time') {
delete nextMetadata.invert;
}
return {
...item,
...nextItem,
metadata: nextMetadata
};
}
export function toggleUsageInverted(item: WidgetItem): WidgetItem {
return toggleMetadataFlag(item, 'invert');
}
export function getUsagePercentCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
const keybinds = [PROGRESS_TOGGLE_KEYBIND];
if (item && isUsageProgressMode(getUsageDisplayMode(item))) {
keybinds.push(INVERT_TOGGLE_KEYBIND);
}
return keybinds;
}
export function getUsageTimerCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
const keybinds = [PROGRESS_TOGGLE_KEYBIND];
if (item && isUsageProgressMode(getUsageDisplayMode(item))) {
keybinds.push(INVERT_TOGGLE_KEYBIND);
} else {
keybinds.push(COMPACT_TOGGLE_KEYBIND);
}
return keybinds;
}