From 329ddc9ced85ca39acb4b645bc6bc3fb4f20a61a Mon Sep 17 00:00:00 2001 From: Meghdad Date: Sun, 17 May 2026 03:53:15 +0330 Subject: [PATCH] init --- .gitignore | 26 + README.md | 27 + badge.html | 12 + index.html | 12 + package-lock.json | 2472 +++++++++++++++++++++++++++ package.json | 27 + resume | 1 + src-tauri/Cargo.toml | 35 + src-tauri/build.rs | 3 + src-tauri/capabilities/default.json | 13 + src-tauri/icons/128x128.png | Bin 0 -> 5977 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 12136 bytes src-tauri/icons/32x32.png | Bin 0 -> 1646 bytes src-tauri/icons/icon-128.png | Bin 0 -> 5977 bytes src-tauri/icons/icon-16.png | Bin 0 -> 745 bytes src-tauri/icons/icon-24.png | Bin 0 -> 1211 bytes src-tauri/icons/icon-256.png | Bin 0 -> 12136 bytes src-tauri/icons/icon-32.png | Bin 0 -> 1646 bytes src-tauri/icons/icon-48.png | Bin 0 -> 2377 bytes src-tauri/icons/icon-64.png | Bin 0 -> 3060 bytes src-tauri/icons/icon.ico | Bin 0 -> 27270 bytes src-tauri/icons/icon.png | Bin 0 -> 12136 bytes src-tauri/src/codex_rpc.rs | 248 +++ src-tauri/src/lib.rs | 201 +++ src-tauri/src/local_usage.rs | 275 +++ src-tauri/src/main.rs | 5 + src-tauri/src/models.rs | 125 ++ src-tauri/src/monitor.rs | 185 ++ src-tauri/src/settings.rs | 173 ++ src-tauri/src/tray.rs | 120 ++ src-tauri/tauri.conf.json | 54 + src/App.tsx | 355 ++++ src/api.ts | 43 + src/badge.tsx | 144 ++ src/main.tsx | 10 + src/styles.css | 695 ++++++++ src/types.ts | 96 ++ src/utils.test.ts | 65 + src/utils.ts | 92 + tsconfig.json | 21 + vite.config.ts | 23 + 41 files changed, 5558 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 badge.html create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 resume create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/default.json create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/icon-128.png create mode 100644 src-tauri/icons/icon-16.png create mode 100644 src-tauri/icons/icon-24.png create mode 100644 src-tauri/icons/icon-256.png create mode 100644 src-tauri/icons/icon-32.png create mode 100644 src-tauri/icons/icon-48.png create mode 100644 src-tauri/icons/icon-64.png create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/src/codex_rpc.rs create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/local_usage.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/src/models.rs create mode 100644 src-tauri/src/monitor.rs create mode 100644 src-tauri/src/settings.rs create mode 100644 src-tauri/src/tray.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src/App.tsx create mode 100644 src/api.ts create mode 100644 src/badge.tsx create mode 100644 src/main.tsx create mode 100644 src/styles.css create mode 100644 src/types.ts create mode 100644 src/utils.test.ts create mode 100644 src/utils.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b627bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# End of https://www.toptal.com/developers/gitignore/api/rust + +node_modules/ +dist/ +src-tauri/target/ +src-tauri/gen/ +*.log +.DS_Store +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8b2c18 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Codex Limit Monitor + +Windows-first desktop monitor for Codex usage and ChatGPT Codex rate-limit status. + +## What It Does + +- Reads live Codex limit percentages from `codex app-server` using `account/rateLimits/read`. +- Falls back to read-only local usage summaries from `%USERPROFILE%\.codex\state_5.sqlite` and `logs_2.sqlite`. +- Shows status in a Tauri dashboard, system tray menu, native notifications, and an always-on-top badge. + +## Development + +The current PowerShell session has Rust/Cargo available, but plain `node`, `npm`, and `codex` are not on PATH. If needed, use the configured Node/NPM path from Herd/NVM or set the Codex executable path inside the app settings. + +```powershell +npm install +npm run tauri dev +``` + +If `codex` is not on PATH, set the Codex executable in Settings. The app also tries common Windows candidates such as `%APPDATA%\npm\codex.cmd`. + +## Verification + +```powershell +cargo test --manifest-path src-tauri/Cargo.toml +npm run build +``` diff --git a/badge.html b/badge.html new file mode 100644 index 0000000..4e241e0 --- /dev/null +++ b/badge.html @@ -0,0 +1,12 @@ + + + + + + Codex Badge + + +
+ + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..1bd8617 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Codex Limit Monitor + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b849236 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2472 @@ +{ + "name": "codex-limit-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-limit-monitor", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.1.0", + "typescript": "^5.9.0", + "vite": "^7.2.0", + "vitest": "^4.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz", + "integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.1", + "@tauri-apps/cli-darwin-x64": "2.11.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.1", + "@tauri-apps/cli-linux-arm64-musl": "2.11.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.1", + "@tauri-apps/cli-linux-x64-gnu": "2.11.1", + "@tauri-apps/cli-linux-x64-musl": "2.11.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.1", + "@tauri-apps/cli-win32-x64-msvc": "2.11.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz", + "integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz", + "integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz", + "integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz", + "integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz", + "integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz", + "integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz", + "integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz", + "integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz", + "integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz", + "integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz", + "integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.356", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz", + "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "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.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "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.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a0d4c20 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "codex-limit-monitor", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 1420", + "build": "tsc && vite build", + "preview": "vite preview --host 127.0.0.1 --port 1420", + "tauri": "tauri", + "test": "vitest run" + }, + "dependencies": { + "@tauri-apps/api": "^2.11.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.1.0", + "typescript": "^5.9.0", + "vite": "^7.2.0", + "vitest": "^4.0.0" + } +} diff --git a/resume b/resume new file mode 100644 index 0000000..1bd8c1f --- /dev/null +++ b/resume @@ -0,0 +1 @@ +codex resume 019e2e41-c278-7722-9f72-500a7a88a338 \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..ab708fa --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "codex-limit-monitor" +version = "0.1.0" +description = "Windows-first desktop monitor for Codex limits and usage" +authors = ["meghdad"] +edition = "2021" + +[lib] +name = "codex_limit_monitor" +crate-type = ["rlib"] + +[[bin]] +name = "codex-limit-monitor" +path = "src/main.rs" +test = false + +[build-dependencies] +tauri-build = { version = "2.6", features = [] } + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +dirs = "6" +regex = "1" +rusqlite = { version = "0.37", features = ["bundled"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { version = "2.11", features = ["tray-icon", "image-png"] } +tauri-plugin-notification = "2.3" +thiserror = "2" +tokio = { version = "1", features = ["io-util", "macros", "process", "sync", "time"] } +tracing = "0.1" + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..261851f --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..b8a5b2f --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default permissions for Codex Limit Monitor", + "windows": ["main", "badge"], + "permissions": [ + "core:default", + "notification:default", + "notification:allow-is-permission-granted", + "notification:allow-request-permission", + "notification:allow-show" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9326e9421606eae5b037352fa262622e1dd611 GIT binary patch literal 5977 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 Yng+u0^Blu4QaK#ZRMS>1Q!xwvKeEnzzW@LL literal 0 HcmV?d00001 diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8c100d21ab78f1495a19e1fc1d4044dee5e544f1 GIT binary patch literal 12136 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≫{md+GcC#;qZE{Q2=f z@A})NuM!!U%duM`6C)j<2`Gm{_b&8t%$G@LD?csNRM)NB_5{XNy#Y0JDgh5ReLsB8 zvGG+B1mE5P!>X0Q0f2a|K5Lb*3a&@K@Kp`V{92;*H!DNzj+YKJLb+i4*#c2+LM8jZ z)aj27UTe4-UR%3&L$9ol!)b0?@pJg% zUxp+>A4!^u*X-zuP4R1uI&>y(*4HG7xMXR+_?J1+gJxexYwB96?6R{~m3Uf+6GmfI z9E~bEC9VKUas%d?lT@KM+JN*c)tdNCeb0)v=hIih0o~J2z)wD#h)Y7+WUVU-D5L<# zNvJrpA#eRQxD_ClCWbLFdW!TEop1Bh3HbjQ9bf8Hv{<>ohuudVnen{Y2BzadDvBBk J<#Og>{|5*7tndH; literal 0 HcmV?d00001 diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..689ab539ff5ff191b847660bae2224697a2431cb GIT binary patch literal 1646 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 Yng+u0^Blu4QaK#ZRMS>1Q!xwvKeEnzzW@LL literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon-16.png b/src-tauri/icons/icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..b57aa5ccaa67165a7338b3f521dc5efe66199b17 GIT binary patch literal 745 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0)9zEK~y+Tt&>Yk z6Hyd~yL2J+HJ#~$_O>&nwJ*y^3#}DmP%$P7#z$NjG!W4pDH$*qLAyqoPO< zLy)9Y9tuIiV}YVk6PtkGGXWPQx^VfMds|vdTzZl(b7$^3|2gN*xeCSqG9^c%DxJ8b z;zg(h2^zf}8htS|1_wiH+``~Yt|hL>6(@vj$`9s67N9k`z?;1cbD)C3TYLX|6;Scw8Vz1|@HU-7c>X?iEevDN;xkk(4?wZ8fh@NJrnK`ou!Oj>e zlXuYgK5vUTg=1h%Ih z!ytzsSpDk)PAl!-9I8P0Z5NK8Ig9rzYmC}gS1`5k0xP5S?49nFwYS1~_}~Tsg9BpC zeuO4(;aFo6dKVWFfA<;(US7snY8>B(Ph#%dD%+DoRgm0e5Nsv+0$QsF;g7u#PKuKfwqx;bPf%ntFHs?Pm=JIRiV6MFHDjjf=$i|&;wNI?Fc-K!5wWtAul3WxewQG zK0tS0lC9}Cle87VNHvs#9eQzFzJQV!;W}E6(ClsKd@`C_F5yCJJWByb5RB}G&g5Xe z2B|bHKuv3O{Z{Mk*vwg=DR#5xzfcOcw2#rr1PJk~m;hJgx=Y8Cwo$-aQJBS+k03&A zaIo?XQeY-`7l>cQS-el_c+vr3LqdNOg`T!e&IFZa2+Ro1@^txs8|_ITo}B{m=OdNo b*bw*!znpum);%~w00000NkvXXu0mjfzFPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1W-vtK~zXfy_IWB zTtyVe`RIq1ec!!zUw3!!zF+Ke7h%gvX{nS(@KLR)#`tJ#q9THi7)UE_lbWVR+t^qQ zEs7ze&_qk>1Jsl>LMyG7q7jkW#)wv+mf9HO=f9bAcQ4#U{h-lFewmp&Gv_~Z=FGVz zCI5HPS;VA{6WeqaPmW=*O3Uyo2nB*UQvrLZ-DnS0728vbPABU1W>1fv^FYse34>K4 zjJyv<-mf48@&sYBg$T1ftl)^Sde|aH_+tAd8sk<8CR-3DdkAI+TUa2ngfmK5@|c|w zSR9djJ36j0Zsf$Of^oJ>i!%m`E3P7D>kw<*4takEQMRK6Qe&fL4#CzjL01fBM|7(y zV6ceoic5U?NeaAQMrHqAWTyI1HTVi@`?`p#Bm0o~>`hd@^D><2tY!@}o@c)*py!x- zyyTX`(cSCo$w+eNlW02nA+}vw#O4n^C0Lt%t9&7c%w#XNjGjY7_uETubH%{9;wLo% zbeZ-bOpXYISOqN<*WZjp^+w$KbPlz5Jb;>}dsH?*_ad6dzlPk>frhCOls7*^Yfa85 zI5DmjFmMtHP*m8%s5qQMa#t%}?-{`SFTejSqu*aa`6DgJoP7hFl%xP40e3sZ+&Fvy(TDDZ*%5-(ohTCEeLaO3Pjozn z^jo`O5CXXP^RFtC^9$&l>8JYoyBn}@s#6_{$ZHXO>S0tI*+U+%y377bz>oBy=TVk> z0tUg4sfBAQBbP72d-{H=uN|+$Ph&gPv4~s)TRe^GXPfEmkboyyBtWmzPrG2dVFD9# zSJ8E02HPi2Vfcq{Nr2T?kJn*#bO((y!tzb9c@tQ(?iNMBlPnSt0wiGI14!grAa`{_ z&-*a+-*F$~XMLgU0Ps%J+Y$IRJ-7{7c4qu>1qyDtTIP=>r_1Kph>kSZ1k zLeKjlZMh9qBdzY zR4$YZE5>+%QO=UQY9 z0`Z-zD9Gyin*Pf6OE*#ce6a(QPI%0ou{~?&UvF*ee>kMUPO73TXOMXNc4%e)-J;uI z-7tU3WWCUS4Pm^z>MJj~IT3^dHhXd=QTpxu*UGi?tnu!25H6pWKt}>>{l~Fwg=@Om z#hcQ01q*9f$+NDO`A@|o0mYlTuzlBy)y@b~IzE#|x26B5KeTt#pJo%mo7#z;Q&%>nCTXMdH=pLq)NU)?vd&orRwTF0Pz8hz0UZeEw-p1qCECu*t%O)-hy zNB95yK9C@-pfL~YJ}1$>x7n%XyUgLpVhb?IJ|mLJmDd@oo~6MHm(@Af-gElAs{!SJ zgVVd&iypgbJpI`0CfWSvb=CZtw_@~MkOnlFX^(Q6Kn*z921oAM3blUzbaOLtjG#Y5 z#f#ITV+|-@OA_+(d+Py+DFK)93-*$U!sQ1(U|Ie1JcVD`r#(Js&!y}$cJt-Tb|oB{ z%89EDV&%E$8`gDGsCM;>tR&98OBBL_wze9&1T(N1_Q z8z>YDoA$i;!69L<=qa-gS^`^GeP*a=?sPwePhOMV#uv@uBzCD24P5D6t*NF~VG%;8 z&SLo)hhIp9~*8`*c?F0`C| z_@mgEey}W~ehPkl$Z}PLT_9#VG69244u)(D zuj}zx8mTzi{6KLmsT>w#*3COYf26ngMp%?`Dm=w$-(8N>C|D2|-ODk6E)qH>2D0?4*!{vcAqu`)`C z#;wu&uKV-*tm%I_h#`RDlED!o(FwcC)^=bk*Plz{YpS^F-g}-?9N=cSd)s>R>0?1m z=Oe!7?{A)!_hO23myxM+gr-OKVVV)!+0$XM>!b3L>L8n_e-=`Es?9l zj+*P7RPA}6T6LogG8;!;EBt9Y3xUht>yx3}r8Ie!s2kw*p66$42XRjWFT~|=%$95` z*;j6S;>3SNc)WhNlP59=yQe4U&rE#i)`j*SOMMoZ|VFPakI&Csx0oL;+ z>W+78s^6A<*>KZMIic`W9Zl>@{ezitxf$E{QjGh;I{k2h8vcgC zSKDHJip9@|Ze5hEUb{sroiJzUPIR}e`DG#t9AsG|hh(1kH>{h&6QUQ7ueoBWc82An z?=G@d6CLFx_$6GXqQHTj`Aw@h<{ti?!pb1cRpy+tH0v9JssW zZp+_7ZMEn1gWuNqcvbO3v`1lA=~5>V^dR~y`dw`%w8uY)o6z9Eq+9ujC*kz{dq0(!g(-z zn<2JO40reO_wtikVS4}7!>iFccNqK+BJ)tmR0ePb5*7<}okXs7KaeZ_XxvR13z`9~4|u1P&dqkNzPe*PBHn{|xI60o0k*V18)3b;;H01! z067&?zymgWl9(4cTfY{}Ix2DDs*T&o7xJ@u1iEFf-MBe1mGJVT+z$>v2l)i$3Q0vT z7&JHi=pajy0)9&Ajv2uP$fddj+MivpoQ$WK#&4EmOPSV%KJ{aQE(VpI{l|UmADp=z zO0k9}G_v1Jf<2d7z3{ozYkRniK#{%KWxbgO!-0?xjjkVob)4|tIKEOU{|GmRH+p#Y#6c?N z@uujvL^$WZTf!CDiN!F+bw9N2&f~0GrVnt>sv{u3hUe8L13iA~z$qh5YL-I6WR8A* zgS*v>$y;2!my}r}s=OeHPw2K7NDVFpIV(ZR`s~5GZ>K>Rl6zl62)m7UPRWFgN4-*C ze6Gna{B)dXyhB;oawwW4@OYFb_ef|5sOp=YO*andE-g_%oMY z{d||Q6gj*cT}(b`){dtIr|A&^w^l_muM9H?T#Pn}!}JB`jB~i{Y^5~yPaY`4NR9YL znH6kpWh4&$Tt>=#0(>0TYt{k}jNDg^gYA%l44DJg{7$b|SZhf*aSudXunfpV$Y8OZ9JKJY?)ESXd>zUU^hH_5djap4=MNyo(r*W_jge5IgG zzbJ&oOu@hRw(0O`%%Gd}ecdH*b|^U-rei>WUe)MAhDgO7PIg+~LTFuxz6d&H>$Z6dLxq1Y%GpERnr-v znHAub#kKF})zcR$HkG5^*tFEudYCi&=2Q41(ns$pVa&6A(wY%!q4%SYbSzlL&SeHT z@PP2+^th=7HM+10N0EL*9|aUno%BLnQC(THAz6bwjjM-y4h_*dsp9$abH)y zV9Fh-#`ZJ37cNxg#*y!9y(JJ%q$h2YqLYtmAvrZy1D5+WQq5>ij#doD9_4{@zTeM* zeZ`M>3ho)viXxZELa*pS!sALV_D0UXP-PQ18bKvb)t+(M^{K4+Ml7WlLWiZ1f6OBA zcF1I-@C{(M{9q`(2vO@QCbf{xtwEL&ypVAgEf80hq1EimAHsF3L$8QrOcHwmx3EsST*G?g+OUxn6C;G_H^zBng%fPvQh=GRQy2K*fn2vhk zPieqSj^inj z6jzpz-87k7tGVZU1+u7BI7ceE zIu)!fW!LfY{Grc}Dy^(g%(-La!HM_XzvQ%0(t1u`nXC3>e5TCXj9&20g>HU2+^AkP0YFkQ8ui`-Bql?l8basPg}`7 zEz=0BRG&%DkV6k6tJt1@R{olDUK@ z4iCE`rwuAisb$okhHB<{;r{$|BGdUs4 zudCt*#r2ZN_!(EcEeS^Y@Y{UugM4UHJzJ|W?k;a-5x?8M6(EhtzRGxBW9EB4b?7IA z=vWofu(rBGX0m;|5i^VKYXQB!Os|8qF3Ki9ALy-{dZ@_LA+jyHvX5$H*ExBN7xs}< z=>fCkrZDn_BlWK%#bhp@!{Z_Z+&VbIR1?sBji8@tNCj}wyP(Rq-yjR;P}o^_rqCBE z)&OZYg*VL}&Sau2-ie&BhsI>BC#+wmi$6kwZZJ?4yA61Dd)`{p{!v)XZAxIM+-rvetyl$U%jL2Ycws+%VMGpL3H!i_=5Pb zRtBKPvk!zh4~0L;(>j@*JM84N)J3AcR37FbW^z0gVk3P{NAMn{;SE1dpg|~}OgPQV zXoVu;0Z5z(u_X+NbEh@Kiu@}pfy9+I7-9DV}EpC4#;go8=8$Lqwo6jHrxBpaVQ1oZu>~L*}`cdE793W z>$UV_`bS~}5{eC>v5%$}1sFK)r>8=e2}4wOID%*JMk_ZtzX}#lvedsuDKdXMyTM=| zZ;w6+pn~8z@5Umpvyo@d)r4$2!Ml5-$qG;jZs5i7wM2+BIOp4o8NtVrTw*ET(9L4q z@kh9gsH>boqF!q0w%}*_ru}{4cwXjYeZQ&f0V>Tug46M28h(0-U09dny5r|B+`6wM zGdoB-#&K@l8fAtImn2wNv0a%!D^&uA3jc9bWd9^@zj~yGLVG#mo52^8mbz1x_?w}f ztfUne@24wTk=R=-bR=ZoeaIQOWpj3$&`PBMVg3*PtbtoxQ2;3D5?6@$>M7mhv5H4L zqq`a|Aze(iT1iflU`-;Um$16G$CTA_PQb`d6*>-F(z$@q(o)T;K*{-b`(Ly|L_-n} zn0Q?Ia4i5^K_I~kM7n7C{#dLC^lWh@W@(C#yGCQg{~$arEm~&J8bK z_aMppC5iAs*E_0Ed}pq34@AgA=kQKD&%qMc*ztn4y*3wVtgooF?t1X9v2%;sO9w2g z7N`+3h~}mR&Cg$C62=;Qi9Via4)%E+`kPq(=u|s!xmFBGvtgxSbzb7B`iA*I7?!=W z_Kc7ZWOAZ)Nm*9d!s(2&nZa!%nXG^k#+>5YC=GOv7s5yfgOXNSP6*xg-o5{|LE$LQ zY;{E;v{#QC&}w=rJOuCt79(f`;0SNW}1 z6bStf7*~fP9I&LifV@U%Zt9TjEj~-89Jzy0lT@K7NZYAT-%ZX2vh*&|nc(~DjQ3dP z0woY>qQhuF^(E>T7f+N_P z1Vm>l3mw4A2{=sk!qL ziy!`?&#mcwm)TQCuKig~2=E2ZstL9RKE3U=j>lk{+7?i5iM94y$%a4dmHBMb&RE}p zOUge_9I0|=MU1j+TVc-xcp;%sBY+-33(6*!X*!R@T4RHo7ZL~$;13MxhbRdKdGP`7 zJcRLxsHRrBe~|Ora!#cZ{FCfbfA)2AigS! z8Lj?NZiqEGA1Tv*cOcgyD3uYgZ%v=vkN`#CrP&0Y74oNMb_X@ z;INm~gTmUZ3+4v#P+_N2^I`XYh|JYRmyPJvx{lF$tH>>Vk84xHu+2Grvidj*VnY(L zO&>Cz`aD%9LSN~>DZ^tEZg@hyIT)0v1dmoLq7@Mdl%EQiQ~4R#>&c|YHOui9MJQ;% z!RsXPN>O3&6iue|~|1fVchB>gvz-!2H%7h{Z!`v_M|aVwC{mbj=j~M+>op|Dgr(Q@YnAK|_v$XZ_^PYHwcL&*`zz zUShmreqYGccB6OS{sx%>(Ovi1gSEi+4_6ev9*;|2Fr{jyAmjd9v^U+ePxvLV)PjM? zhVFNEVz1GdGD{qdnfT&`peZO&VT2HX!|DAN{fUqw&}hwc#!TX9$vZ(9*Ms&vlFNv> zf_LdyGeoDFG&~@#oA*G{iJAnLG(*K9XQHI->mwn48Z>Oz>+5MpCRPrTY;K~}%0^QL3{(j~m{U+06Al#)Rv@AJ41Y)Q-v&#;eOB$s z>x!pPJQ(5wd^&lW0Dx-dolb)+x}Yn`MiJHJ(m)n4yEtHS`CH_FMCB_wrjG8^eBr4< zv`zS*U`owj9msd>K+d(XL?jwDy+r9O=?4cX{qF+Er`Md^Tv4Q~E#>}hOuhb*6zgkZ1pRZ`4T0-h&V5=I_DCV2zb>Qc4_ z`-1+oi-5O%AphSqCubv1oMc36ByKMKQk5}P8%t^i3bdxc7o{E#m=;T)sm3@r00ulA^ z{nVTd{V7a`(05F1^(d0zZh#Y6G6C5X00ici2)Ui0zCf<;`$MG5`S73Pzk}^h`~T^9 zqF5};g7tY}XM2kcuv4aZBybUsmf@wr1%QprpH=2wZ%T=@ERnIhDp1JNAh}B=I;tys zc}4zC%YXV}q&jopl78uhu0f?W8YW_P`VfbD~9(S-jYks%&!U309L( zd9L$+n4Tf{c>l{LNn61f&=$mSomu*EncOI^%#IKckZv17$y0r8AYm0ckP+g;O!ZLP z1~(6kC-!MJCoZ5biuUg$_t#ZAAZYgvaiU4v6!u}VdUOc9kL^HFgsH-KgNI1li_W@h z$6v|VB9Cp@>xCik6jTzM;r!kAC+Attm7o5RV*b2#vL^FS4KD`97bMJhOkQ|#oe6K9j6*hDBAm!=KZF5%;E7R^?UNEM>s`cVLzc`8kjR9ZVFC#QX)_i^al0r68zKx7=|O!tPXTNyyEOlY#E4#a_AFj zHjRPz+{5L~<{i2tIEJALJK`!KJcB`VFE^bs(>i@=p5d;ZS!f!a4BB31&6wm$)@`V4 z`7H`y>7X}f79=tw*-I?f<OSFdLH;&yF=nk%DwJn`hnhT;gQ4=G`Sw#k*X|1*{ZkEl}JF0(V3bklki3bb;tBIHJA%RI_sJUN8GJZvxhzC40KRaUOTMgd4e;#~5$K)t&}}aB zg$Ln4BWu5xzUgEFA)#eYW4~X~=s}C%C8YxZyfO`lujSoY6QRtOimVj|0%b)o*!|x4 z!Nm86-7lZA%}WiTi`(-7U%XEkLdwq%e-6Z->(v`x`r!Run&0AB?7$|+b+5<~BruhrG@WS;$iQaRQJf__*DdbODG^@V5<6kO1j$vSs zUGQS`L-!q3eMiBXIv9!~+yQ$Pcrr*&Md|{Y2=-v2GD_LY57ERmUN-gid*j~cDgt%% zD>~*oF^?Gxwgu8tVVBK$KqJs3!uClXbD%R-`Q07wHv_6+rrNEMh^P7G4x$Jq+CQ)R z#qq5H*J{vpc3bdEAGH|{xWg;Dvg>Be%ik;~K4=PoIY=?C&C7Gf-RKmj890(iM5q}k z2XDS>PDXtNqo(p(ayhsFW0m2FCQ}FpP5TAQh`N}G$D+g(ZR8_4qzCWMes=GgUw|`x z>!QY_^HXg>!#pgYOj_nl%D5_ZmqukM5A$F1ZdL!j;k3u67~tx9`r zm0HZF-T}LJeRGP$b46QOo}5$<$#fOeT+VC&AG%}wKafSu+~b}AX8e(0>)iLFj|T!I zf>VwA3z3G+>!V{5uxQ5A!IKxMNCM6pF3w;uDJac=*qKx3rZLHDrW^P52;Td@Af>V_ z-kW2PFlc_+^YJX1bUzF4^3{Uh9K49P<)Kym^;SVDUbj7^y(KJ{WKck_q8x7LSdoCXScz3YFqwep>K|b-H8A-dBmzXS#y- zAlgDTNkOkvw4CX+d84?Hp6H zX7Y*!&6CLADY)e&3i_r9~c^48m>=JoH^t3cIo2;WaMSKqlwqRROvHohn)-#S{_4-*R$cI zvvxBeLf92*DZ0yke{@&kTcG`D;Jb2^f$7)2CdKQ&-S|4X6d%>Ka~C0>bo|5Y`bgWc z@!O&+`jSp|-a?_?H!~ZCno*H3EBs_hI!>&@Wq)0fLcSgd>YTmGmGqyV(}ND_dKNty z()-0HgO-&Kv#r|L2@7ySS+z3X|<*^X`Yp$fZ)!KSo3!Ko=l%_xuif@LPoGeZwhJOcggl_}e+%hp*I zUPveF1j4qOC@JE_u{Oq}G!TQu$JO_Ys>pJ&AGb2v-s38}44><0Dj}|{jN>MBAMncd z6H1*n@rcY-m|k4d>{8;6G!q1FL4h(Sh;V32U}Ury&~L=8H~KJ8{N3y-KTsBiNlO*Bj3yQxO)C)w-F{yz;rIKAjNGDhj}l z(zVBGseV820Fz|ZuXJq8=V^$k@tWjiI0!tSVJT5}cfS**c3GsQevxHAR-iV5TKf0! zaQ}XD;w-mcxKyv^=)@~XYSFz1>M%prqm|sWof|ZstCet{8B|W9(U?@D%#fLf?(O01 z@Si)RvU%bzioajcgt#&ra)JAmswaha*=m5=sYULM{mzZ^1Y~e=^78zQIgc>E#(+p>62R_3Jsd2lQ$LTAt zlV}QseQ`BiAFK3F(P!Mf@T7j_knf$>!pCb{cNCzd9hG=Y(5iiu zJlcjt4;TxSzt)~aIfX5R(S$_0^uv~NqImEbDW2KmSC)44&LNY`lBbTG6@i!h*D(bp z&I6jGZ&jR?BU$4|Hz0f!d>_yOggLourog6uaAhVmum$IF{gORWuM%X6f3WA?bmV%r z2{(f#oV@q`t^HZa{%(>18|Rr};g#PsB*wx=C-<0FPO2dwmW+=U@pcFGtkK30bD0>1gt>?-=viwAj+Rf@Zmeh3^Z0|z(>=2PU~ryYp#qN6e*WZBTh zkuAJ3cdzTB^ouD0?g1=78f;a;>9TGZV!A4FgJw7&uK>6rpBErbbqqe=XLF(NF!JZ;M7w7)ba0fR$RIRUdgO2D*Z6y}!<%ZA<$ z7Jj^idmXpFUZqgFbL{IL$N5R5YGaKxhyv?y0#iFk$xR@PUXDpUPbRvMZ<1D|` zEVMXrheE0CHa1D*m-PLT<;&Gdq%u2Qz(mvC-RB_*7;!X<0F`nhD&&K}Xe|V(k8{7} z|1>X>Fm8EpwM;EA7M(x`ZV+7O1QDl^gid1OvtJgFcGg3l_z6)BOA+ zvWsZpTM)YWTLLvu3c+`w z7EBIZftltNXgm_U8woV#fH< z(VKZe?0kT$d`wG*msW|1H{d)JtAT;Io{@&LmC0H^eybdkc&05kHG;T|*}IzVP^(c>>tVOuyn`!sJYl9`RdE-Po(K>8(FBWEm0ISBJ)5H!JqM?e?>tcA(#h6&Nvn%~O z#I1?1N$_}Sq)p{CSzjNG;u*`P5`UJKbv~XMrs_lj90*nT>|#d!T<~$R z(Nl4(&C)n9lUZm|{BeQ6wTEM|A0uV&?}8Kn$DH&?eba@W3X<76@GF zu(_AOR(!2V!6XReXajHk7S3d;RrzC+H#g83{>RswgPL~ds#!#iV=xuU+I#6mj`Lrv zpY|weWw|-1cK{Q*n$NHf6BXvJ*a3a&!nw>Oy^U5iN5epZx~pklsZIPjE+$jqrMa&3 zC-?7l1%zp(XN3GMm=F2`N0N98DrE5eKmH4dJCPYYu6gR≫{md+GcC#;qZE{Q2=f z@A})NuM!!U%duM`6C)j<2`Gm{_b&8t%$G@LD?csNRM)NB_5{XNy#Y0JDgh5ReLsB8 zvGG+B1mE5P!>X0Q0f2a|K5Lb*3a&@K@Kp`V{92;*H!DNzj+YKJLb+i4*#c2+LM8jZ z)aj27UTe4-UR%3&L$9ol!)b0?@pJg% zUxp+>A4!^u*X-zuP4R1uI&>y(*4HG7xMXR+_?J1+gJxexYwB96?6R{~m3Uf+6GmfI z9E~bEC9VKUas%d?lT@KM+JN*c)tdNCeb0)v=hIih0o~J2z)wD#h)Y7+WUVU-D5L<# zNvJrpA#eRQxD_ClCWbLFdW!TEop1Bh3HbjQ9bf8Hv{<>ohuudVnen{Y2BzadDvBBk J<#Og>{|5*7tndH; literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon-32.png b/src-tauri/icons/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..689ab539ff5ff191b847660bae2224697a2431cb GIT binary patch literal 1646 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon-64.png b/src-tauri/icons/icon-64.png new file mode 100644 index 0000000000000000000000000000000000000000..3ce8b9d40da80fe9eedbae541abeb54863879cdb GIT binary patch literal 3060 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≫{md+GcC#;qZE{Q2=f z@A})NuM!!U%duM`6C)j<2`Gm{_b&8t%$G@LD?csNRM)NB_5{XNy#Y0JDgh5ReLsB8 zvGG+B1mE5P!>X0Q0f2a|K5Lb*3a&@K@Kp`V{92;*H!DNzj+YKJLb+i4*#c2+LM8jZ z)aj27UTe4-UR%3&L$9ol!)b0?@pJg% zUxp+>A4!^u*X-zuP4R1uI&>y(*4HG7xMXR+_?J1+gJxexYwB96?6R{~m3Uf+6GmfI z9E~bEC9VKUas%d?lT@KM+JN*c)tdNCeb0)v=hIih0o~J2z)wD#h)Y7+WUVU-D5L<# zNvJrpA#eRQxD_ClCWbLFdW!TEop1Bh3HbjQ9bf8Hv{<>ohuudVnen{Y2BzadDvBBk J<#Og>{|5*7tndH; literal 0 HcmV?d00001 diff --git a/src-tauri/src/codex_rpc.rs b/src-tauri/src/codex_rpc.rs new file mode 100644 index 0000000..77024f8 --- /dev/null +++ b/src-tauri/src/codex_rpc.rs @@ -0,0 +1,248 @@ +use crate::{models::RateLimitBucket, settings::AppSettings}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::{collections::BTreeMap, process::Stdio, time::Duration}; +use thiserror::Error; +use tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + process::{ChildStdout, Command}, + time::timeout, +}; + +const RPC_TIMEOUT: Duration = Duration::from_secs(20); +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; + +#[derive(Debug, Error)] +pub enum CodexRpcError { + #[error("failed to start Codex app-server: {0}")] + Spawn(#[from] std::io::Error), + #[error("Codex app-server did not expose stdio")] + MissingPipe, + #[error("Codex app-server timed out")] + Timeout, + #[error("Codex app-server closed stdout")] + Closed, + #[error("invalid app-server JSON: {0}")] + InvalidJson(#[from] serde_json::Error), + #[error("app-server returned error: {0}")] + Rpc(String), + #[error("app-server response was missing result")] + MissingResult, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RateLimitsEnvelope { + rate_limits: Option, + rate_limits_by_limit_id: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawRateLimitBucket { + limit_id: String, + limit_name: Option, + primary: Option, + secondary: Option, + rate_limit_reached_type: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawLimitWindow { + used_percent: Option, + window_duration_mins: Option, + resets_at: Option, +} + +pub async fn fetch_rate_limits( + settings: &AppSettings, +) -> Result, CodexRpcError> { + let executable = settings.resolve_codex_executable(); + let mut command = Command::new(executable); + command + .arg("app-server") + .arg("--listen") + .arg("stdio://") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true); + + #[cfg(windows)] + command.creation_flags(CREATE_NO_WINDOW); + + let mut child = command.spawn()?; + + let mut stdin = child.stdin.take().ok_or(CodexRpcError::MissingPipe)?; + let stdout = child.stdout.take().ok_or(CodexRpcError::MissingPipe)?; + let mut reader = BufReader::new(stdout).lines(); + + write_json( + &mut stdin, + json!({ + "jsonrpc": "2.0", + "method": "initialize", + "id": 0, + "params": { + "clientInfo": { + "name": "codex_limit_monitor", + "title": "Codex Limit Monitor", + "version": env!("CARGO_PKG_VERSION") + } + } + }), + ) + .await?; + + read_response(&mut reader, 0).await?; + + write_json( + &mut stdin, + json!({ + "jsonrpc": "2.0", + "method": "initialized", + "params": {} + }), + ) + .await?; + + write_json( + &mut stdin, + json!({ + "jsonrpc": "2.0", + "method": "account/rateLimits/read", + "id": 1 + }), + ) + .await?; + + let response = read_response(&mut reader, 1).await?; + let _ = child.kill().await; + + let result = response + .get("result") + .cloned() + .ok_or(CodexRpcError::MissingResult)?; + parse_rate_limits(result) +} + +async fn write_json( + stdin: &mut tokio::process::ChildStdin, + value: Value, +) -> Result<(), CodexRpcError> { + let mut line = serde_json::to_vec(&value)?; + line.push(b'\n'); + stdin.write_all(&line).await?; + stdin.flush().await?; + Ok(()) +} + +async fn read_response( + reader: &mut tokio::io::Lines>, + id: u64, +) -> Result { + loop { + let line = timeout(RPC_TIMEOUT, reader.next_line()) + .await + .map_err(|_| CodexRpcError::Timeout)? + .map_err(CodexRpcError::Spawn)? + .ok_or(CodexRpcError::Closed)?; + + let value: Value = serde_json::from_str(&line)?; + if value.get("id").and_then(Value::as_u64) != Some(id) { + continue; + } + + if let Some(error) = value.get("error") { + return Err(CodexRpcError::Rpc(error.to_string())); + } + + return Ok(value); + } +} + +fn parse_rate_limits(result: Value) -> Result, CodexRpcError> { + let envelope: RateLimitsEnvelope = serde_json::from_value(result)?; + let mut buckets: Vec = Vec::new(); + + if let Some(by_id) = envelope.rate_limits_by_limit_id { + for raw in by_id.into_values() { + buckets.push(raw.into()); + } + } else if let Some(raw) = envelope.rate_limits { + buckets.push(raw.into()); + } + + buckets.sort_by(|a, b| a.limit_id.cmp(&b.limit_id)); + Ok(buckets) +} + +impl From for RateLimitBucket { + fn from(raw: RawRateLimitBucket) -> Self { + let primary = raw.primary.unwrap_or_default(); + let secondary = raw.secondary; + Self { + limit_id: raw.limit_id, + limit_name: raw.limit_name, + used_percent: primary.used_percent.unwrap_or(0.0), + window_duration_mins: primary.window_duration_mins, + resets_at: primary.resets_at, + secondary_used_percent: secondary.as_ref().and_then(|value| value.used_percent), + secondary_resets_at: secondary.and_then(|value| value.resets_at), + reached_type: raw.rate_limit_reached_type, + } + } +} + +impl Default for RawLimitWindow { + fn default() -> Self { + Self { + used_percent: Some(0.0), + window_duration_mins: None, + resets_at: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_multi_bucket_rate_limits() { + let value = json!({ + "rateLimitsByLimitId": { + "codex": { + "limitId": "codex", + "limitName": null, + "primary": { + "usedPercent": 25, + "windowDurationMins": 15, + "resetsAt": 1730947200 + }, + "secondary": null, + "rateLimitReachedType": null + }, + "codex_other": { + "limitId": "codex_other", + "limitName": "codex_other", + "primary": { + "usedPercent": 42, + "windowDurationMins": 60, + "resetsAt": 1730950800 + } + } + } + }); + + let buckets = parse_rate_limits(value).unwrap(); + + assert_eq!(buckets.len(), 2); + assert_eq!(buckets[0].limit_id, "codex"); + assert_eq!(buckets[0].used_percent, 25.0); + assert_eq!(buckets[1].limit_id, "codex_other"); + assert_eq!(buckets[1].window_duration_mins, Some(60)); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..8958ad4 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,201 @@ +pub mod codex_rpc; +pub mod local_usage; +pub mod models; +#[cfg(not(test))] +pub mod monitor; +pub mod settings; +#[cfg(not(test))] +pub mod tray; + +#[cfg(not(test))] +use monitor::MonitorService; +#[cfg(not(test))] +use settings::{AppSettings, BadgePosition}; +#[cfg(not(test))] +use std::sync::Arc; +#[cfg(not(test))] +use tauri::{AppHandle, Emitter, Manager, PhysicalPosition, State}; + +#[cfg(not(test))] +#[derive(Clone)] +pub struct AppState { + monitor: Arc, +} + +#[cfg(not(test))] +#[tauri::command] +async fn get_snapshot(state: State<'_, AppState>) -> Result { + state.monitor.snapshot_or_refresh(None).await +} + +#[cfg(not(test))] +#[tauri::command] +async fn refresh_now( + app: AppHandle, + state: State<'_, AppState>, +) -> Result { + state.monitor.refresh(Some(app)).await +} + +#[cfg(not(test))] +#[tauri::command] +async fn get_usage_history( + range: String, + state: State<'_, AppState>, +) -> Result { + let settings = state.monitor.settings().await; + local_usage::read_usage_history(&settings, &range).map_err(|err| err.to_string()) +} + +#[cfg(not(test))] +#[tauri::command] +async fn get_settings(state: State<'_, AppState>) -> Result { + Ok(state.monitor.settings().await) +} + +#[cfg(not(test))] +#[tauri::command] +async fn update_settings( + app: AppHandle, + settings: AppSettings, + state: State<'_, AppState>, +) -> Result { + settings.save().map_err(|err| err.to_string())?; + state.monitor.replace_settings(settings.clone()).await; + apply_badge_visibility(&app, settings.badge_visible)?; + apply_badge_position(&app, &settings.badge_position)?; + let snapshot = state.monitor.refresh(Some(app.clone())).await?; + let _ = app.emit("settings_updated", &settings); + let _ = app.emit("rate_limit_updated", &snapshot); + Ok(settings) +} + +#[cfg(not(test))] +#[tauri::command] +async fn toggle_badge( + app: AppHandle, + visible: bool, + state: State<'_, AppState>, +) -> Result { + let mut settings = state.monitor.settings().await; + settings.badge_visible = visible; + settings.save().map_err(|err| err.to_string())?; + state.monitor.replace_settings(settings.clone()).await; + apply_badge_visibility(&app, visible)?; + let _ = app.emit("settings_updated", &settings); + Ok(settings) +} + +#[cfg(not(test))] +#[tauri::command] +async fn set_badge_position( + app: AppHandle, + position: BadgePosition, + state: State<'_, AppState>, +) -> Result { + let mut settings = state.monitor.settings().await; + settings.badge_position = position; + settings.save().map_err(|err| err.to_string())?; + state.monitor.replace_settings(settings.clone()).await; + apply_badge_position(&app, &settings.badge_position)?; + let _ = app.emit("settings_updated", &settings); + Ok(settings) +} + +#[cfg(not(test))] +#[tauri::command] +async fn move_badge(app: AppHandle, position: BadgePosition) -> Result<(), String> { + apply_badge_position(&app, &position) +} + +#[cfg(not(test))] +#[tauri::command] +async fn pause_notifications( + until: Option, + state: State<'_, AppState>, +) -> Result { + let mut settings = state.monitor.settings().await; + settings.notifications_muted_until = until; + settings.save().map_err(|err| err.to_string())?; + state.monitor.replace_settings(settings.clone()).await; + Ok(settings) +} + +#[cfg(not(test))] +#[tauri::command] +async fn open_dashboard(app: AppHandle) -> Result<(), String> { + show_dashboard(&app) +} + +#[cfg(not(test))] +pub(crate) fn show_dashboard(app: &AppHandle) -> Result<(), String> { + if let Some(window) = app.get_webview_window("main") { + window.show().map_err(|err| err.to_string())?; + window.set_focus().map_err(|err| err.to_string())?; + } + Ok(()) +} + +#[cfg(not(test))] +pub(crate) fn apply_badge_visibility(app: &AppHandle, visible: bool) -> Result<(), String> { + if let Some(window) = app.get_webview_window("badge") { + if visible { + window.show().map_err(|err| err.to_string())?; + } else { + window.hide().map_err(|err| err.to_string())?; + } + } + Ok(()) +} + +#[cfg(not(test))] +fn apply_badge_position(app: &AppHandle, position: &BadgePosition) -> Result<(), String> { + let Some(window) = app.get_webview_window("badge") else { + return Ok(()); + }; + + window + .set_position(PhysicalPosition::new(position.x, position.y)) + .map_err(|err| err.to_string()) +} + +#[cfg(not(test))] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_notification::init()) + .setup(|app| { + let settings = AppSettings::load().unwrap_or_default(); + let monitor = Arc::new(MonitorService::new(settings.clone())); + app.manage(AppState { + monitor: monitor.clone(), + }); + + tray::setup_tray(app.handle())?; + apply_badge_visibility(app.handle(), settings.badge_visible)?; + apply_badge_position(app.handle(), &settings.badge_position)?; + if let Some(window) = app.get_webview_window("badge") { + let _ = window.set_shadow(false); + } + + let app_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + monitor.run(app_handle).await; + }); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + get_snapshot, + refresh_now, + get_usage_history, + get_settings, + update_settings, + toggle_badge, + set_badge_position, + move_badge, + pause_notifications, + open_dashboard + ]) + .run(tauri::generate_context!()) + .expect("error while running Codex Limit Monitor"); +} diff --git a/src-tauri/src/local_usage.rs b/src-tauri/src/local_usage.rs new file mode 100644 index 0000000..8a2ab99 --- /dev/null +++ b/src-tauri/src/local_usage.rs @@ -0,0 +1,275 @@ +use crate::{ + models::{ModelUsage, ThreadUsage, TokenBreakdown, UsageHistory, UsagePoint, UsageSummary}, + settings::AppSettings, +}; +use chrono::{Local, TimeZone, Utc}; +use regex::Regex; +use rusqlite::{Connection, OpenFlags, Row}; +use std::path::{Path, PathBuf}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum LocalUsageError { + #[error("Codex state database was not found at {0}")] + MissingStateDb(String), + #[error("SQLite error: {0}")] + Sqlite(#[from] rusqlite::Error), +} + +pub fn read_usage_summary(settings: &AppSettings) -> UsageSummary { + match read_usage_summary_inner(settings) { + Ok(summary) => summary, + Err(error) => UsageSummary { + source_path: Some(settings.codex_home.clone()), + error: Some(error.to_string()), + ..UsageSummary::default() + }, + } +} + +pub fn read_usage_history( + settings: &AppSettings, + range: &str, +) -> Result { + let state_path = state_db_path(settings); + if !state_path.exists() { + return Err(LocalUsageError::MissingStateDb( + state_path.to_string_lossy().to_string(), + )); + } + + let conn = open_read_only(&state_path)?; + let days = match range { + "24h" => 1, + "30d" => 30, + _ => 7, + }; + let since = Utc::now().timestamp() - days * 86_400; + let mut statement = conn.prepare( + "SELECT date(updated_at, 'unixepoch', 'localtime') AS bucket, + COALESCE(SUM(tokens_used), 0) AS tokens, + COUNT(*) AS threads + FROM threads + WHERE updated_at >= ?1 + GROUP BY bucket + ORDER BY bucket ASC", + )?; + + let points = statement + .query_map([since], |row| { + Ok(UsagePoint { + label: row.get(0)?, + tokens: row.get(1)?, + threads: row.get(2)?, + }) + })? + .collect::, _>>()?; + + Ok(UsageHistory { + range: range.to_string(), + points, + }) +} + +fn read_usage_summary_inner(settings: &AppSettings) -> Result { + let state_path = state_db_path(settings); + if !state_path.exists() { + return Err(LocalUsageError::MissingStateDb( + state_path.to_string_lossy().to_string(), + )); + } + + let conn = open_read_only(&state_path)?; + let today_start = local_midnight_timestamp(); + let week_start = Utc::now().timestamp() - 7 * 86_400; + + let (total_tokens, thread_count) = sum_tokens(&conn, None)?; + let (today_tokens, today_thread_count) = sum_tokens(&conn, Some(today_start))?; + let (week_tokens, week_thread_count) = sum_tokens(&conn, Some(week_start))?; + let recent_threads = recent_threads(&conn)?; + let current_thread = recent_threads.first().cloned(); + let model_breakdown = model_breakdown(&conn)?; + let token_breakdown = read_token_breakdown(settings, week_start).unwrap_or_default(); + + Ok(UsageSummary { + total_tokens, + today_tokens, + week_tokens, + thread_count, + today_thread_count, + week_thread_count, + current_thread, + recent_threads, + model_breakdown, + token_breakdown, + source_path: Some(state_path.to_string_lossy().to_string()), + error: None, + }) +} + +fn open_read_only(path: &Path) -> Result { + Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY) +} + +fn state_db_path(settings: &AppSettings) -> PathBuf { + settings.codex_home_path().join("state_5.sqlite") +} + +fn logs_db_path(settings: &AppSettings) -> PathBuf { + settings.codex_home_path().join("logs_2.sqlite") +} + +fn local_midnight_timestamp() -> i64 { + let date = Local::now().date_naive(); + let midnight = date.and_hms_opt(0, 0, 0).expect("valid midnight"); + Local + .from_local_datetime(&midnight) + .earliest() + .expect("valid local midnight") + .timestamp() +} + +fn sum_tokens(conn: &Connection, since: Option) -> Result<(i64, i64), rusqlite::Error> { + if let Some(since) = since { + conn.query_row( + "SELECT COALESCE(SUM(tokens_used), 0), COUNT(*) FROM threads WHERE updated_at >= ?1", + [since], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + } else { + conn.query_row( + "SELECT COALESCE(SUM(tokens_used), 0), COUNT(*) FROM threads", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + } +} + +fn recent_threads(conn: &Connection) -> Result, rusqlite::Error> { + let mut statement = conn.prepare( + "SELECT id, title, tokens_used, model, reasoning_effort, updated_at, cwd + FROM threads + ORDER BY updated_at DESC + LIMIT 12", + )?; + + let rows = statement + .query_map([], row_to_thread)? + .collect::, _>>(); + rows +} + +fn model_breakdown(conn: &Connection) -> Result, rusqlite::Error> { + let mut statement = conn.prepare( + "SELECT COALESCE(NULLIF(model, ''), 'unknown') AS model, + COALESCE(SUM(tokens_used), 0) AS tokens, + COUNT(*) AS threads + FROM threads + GROUP BY model + ORDER BY tokens DESC + LIMIT 8", + )?; + + let rows = statement + .query_map([], |row| { + Ok(ModelUsage { + model: row.get(0)?, + tokens: row.get(1)?, + threads: row.get(2)?, + }) + })? + .collect::, _>>(); + rows +} + +fn row_to_thread(row: &Row<'_>) -> Result { + Ok(ThreadUsage { + id: row.get(0)?, + title: row + .get::<_, String>(1) + .unwrap_or_else(|_| "Untitled".to_string()), + tokens_used: row.get(2)?, + model: row.get(3)?, + reasoning_effort: row.get(4)?, + updated_at: row.get(5)?, + cwd: row.get(6)?, + }) +} + +fn read_token_breakdown( + settings: &AppSettings, + since: i64, +) -> Result { + let logs_path = logs_db_path(settings); + if !logs_path.exists() { + return Ok(TokenBreakdown::default()); + } + + let conn = open_read_only(&logs_path)?; + let mut statement = conn.prepare( + "SELECT feedback_log_body + FROM logs + WHERE ts >= ?1 + AND feedback_log_body LIKE '%event.kind=response.completed%' + ORDER BY ts DESC + LIMIT 5000", + )?; + + let mut rows = statement.query([since])?; + let mut breakdown = TokenBreakdown::default(); + while let Some(row) = rows.next()? { + let body: Option = row.get(0)?; + if let Some(body) = body { + add_response_tokens(&mut breakdown, &body); + } + } + + Ok(breakdown) +} + +fn add_response_tokens(breakdown: &mut TokenBreakdown, body: &str) { + let regex = Regex::new(r"(input|output|cached|reasoning|tool)_token_count=(\d+)") + .expect("static token regex is valid"); + let mut matched = false; + + for capture in regex.captures_iter(body) { + let count = capture + .get(2) + .and_then(|value| value.as_str().parse::().ok()) + .unwrap_or(0); + match capture.get(1).map(|value| value.as_str()) { + Some("input") => breakdown.input += count, + Some("output") => breakdown.output += count, + Some("cached") => breakdown.cached += count, + Some("reasoning") => breakdown.reasoning += count, + Some("tool") => breakdown.tool += count, + _ => {} + } + matched = true; + } + + if matched { + breakdown.responses += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_response_token_counts() { + let mut breakdown = TokenBreakdown::default(); + add_response_tokens( + &mut breakdown, + "event.kind=response.completed input_token_count=40917 output_token_count=410 cached_token_count=39296 reasoning_token_count=32 tool_token_count=41327", + ); + + assert_eq!(breakdown.input, 40917); + assert_eq!(breakdown.output, 410); + assert_eq!(breakdown.cached, 39296); + assert_eq!(breakdown.reasoning, 32); + assert_eq!(breakdown.tool, 41327); + assert_eq!(breakdown.responses, 1); + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..5d0dbfe --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + codex_limit_monitor::run(); +} diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs new file mode 100644 index 0000000..a19ac9b --- /dev/null +++ b/src-tauri/src/models.rs @@ -0,0 +1,125 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RateLimitBucket { + pub limit_id: String, + pub limit_name: Option, + pub used_percent: f64, + pub window_duration_mins: Option, + pub resets_at: Option, + pub secondary_used_percent: Option, + pub secondary_resets_at: Option, + pub reached_type: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct LiveStatus { + pub state: String, + pub detail: Option, +} + +impl LiveStatus { + pub fn online() -> Self { + Self { + state: "online".to_string(), + detail: None, + } + } + + pub fn fallback(detail: impl Into) -> Self { + Self { + state: "fallback".to_string(), + detail: Some(detail.into()), + } + } + + pub fn offline(detail: impl Into) -> Self { + Self { + state: "offline".to_string(), + detail: Some(detail.into()), + } + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct TokenBreakdown { + pub input: i64, + pub output: i64, + pub cached: i64, + pub reasoning: i64, + pub tool: i64, + pub responses: i64, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ModelUsage { + pub model: String, + pub tokens: i64, + pub threads: i64, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ThreadUsage { + pub id: String, + pub title: String, + pub tokens_used: i64, + pub model: Option, + pub reasoning_effort: Option, + pub updated_at: i64, + pub cwd: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UsageSummary { + pub total_tokens: i64, + pub today_tokens: i64, + pub week_tokens: i64, + pub thread_count: i64, + pub today_thread_count: i64, + pub week_thread_count: i64, + pub current_thread: Option, + pub recent_threads: Vec, + pub model_breakdown: Vec, + pub token_breakdown: TokenBreakdown, + pub source_path: Option, + pub error: Option, +} + +impl UsageSummary { + pub fn has_data(&self) -> bool { + self.thread_count > 0 || self.total_tokens > 0 || !self.recent_threads.is_empty() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MonitorSnapshot { + pub source: String, + pub fetched_at: i64, + pub live_status: LiveStatus, + pub buckets: Vec, + pub usage_summary: UsageSummary, + pub current_thread: Option, + pub message: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UsagePoint { + pub label: String, + pub tokens: i64, + pub threads: i64, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UsageHistory { + pub range: String, + pub points: Vec, +} diff --git a/src-tauri/src/monitor.rs b/src-tauri/src/monitor.rs new file mode 100644 index 0000000..f88dd77 --- /dev/null +++ b/src-tauri/src/monitor.rs @@ -0,0 +1,185 @@ +use crate::{ + codex_rpc, local_usage, + models::{LiveStatus, MonitorSnapshot, RateLimitBucket}, + settings::AppSettings, + tray, +}; +use chrono::Utc; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tauri::{AppHandle, Emitter}; +use tauri_plugin_notification::NotificationExt; +use tokio::sync::{Mutex, RwLock}; + +#[derive(Default)] +struct NotificationMemory { + seen: HashMap, +} + +pub struct MonitorService { + settings: RwLock, + snapshot: RwLock>, + notifications: Mutex, +} + +impl MonitorService { + pub fn new(settings: AppSettings) -> Self { + Self { + settings: RwLock::new(settings), + snapshot: RwLock::new(None), + notifications: Mutex::new(NotificationMemory::default()), + } + } + + pub async fn settings(&self) -> AppSettings { + self.settings.read().await.clone() + } + + pub async fn replace_settings(&self, settings: AppSettings) { + *self.settings.write().await = settings; + } + + pub async fn snapshot_or_refresh( + &self, + app: Option, + ) -> Result { + if let Some(snapshot) = self.snapshot.read().await.clone() { + return Ok(snapshot); + } + self.refresh(app).await + } + + pub async fn refresh(&self, app: Option) -> Result { + let settings = self.settings().await; + if let Some(app) = app.as_ref() { + let _ = app.emit("refresh_started", ()); + } + let usage_summary = local_usage::read_usage_summary(&settings); + let live = codex_rpc::fetch_rate_limits(&settings).await; + let fetched_at = Utc::now().timestamp(); + + let (source, live_status, buckets, message) = match live { + Ok(buckets) => ( + "codex-app-server".to_string(), + LiveStatus::online(), + buckets, + None, + ), + Err(error) if usage_summary.has_data() => ( + "local-sqlite-fallback".to_string(), + LiveStatus::fallback(error.to_string()), + Vec::new(), + Some("Live Codex limits are unavailable; showing local usage history.".to_string()), + ), + Err(error) => ( + "offline".to_string(), + LiveStatus::offline(error.to_string()), + Vec::new(), + Some("No live Codex limits or local usage data could be read.".to_string()), + ), + }; + + let current_thread = usage_summary.current_thread.clone(); + let snapshot = MonitorSnapshot { + source, + fetched_at, + live_status, + buckets, + usage_summary, + current_thread, + message, + }; + + *self.snapshot.write().await = Some(snapshot.clone()); + + if let Some(app) = app { + let _ = app.emit("rate_limit_updated", &snapshot); + let _ = app.emit("usage_updated", &snapshot.usage_summary); + tray::update_tray(&app, &snapshot); + self.maybe_notify(&app, &settings, &snapshot.buckets).await; + } + + Ok(snapshot) + } + + pub async fn run(self: Arc, app: AppHandle) { + loop { + let settings = self.settings().await; + let interval = settings.refresh_interval_secs.max(15); + let _ = self.refresh(Some(app.clone())).await; + tokio::time::sleep(Duration::from_secs(interval)).await; + } + } + + async fn maybe_notify( + &self, + app: &AppHandle, + settings: &AppSettings, + buckets: &[RateLimitBucket], + ) { + if settings + .notifications_muted_until + .is_some_and(|until| until > Utc::now().timestamp()) + { + return; + } + + let mut memory = self.notifications.lock().await; + for bucket in buckets { + let Some(level) = threshold_level(settings, bucket.used_percent) else { + continue; + }; + let reset = bucket.resets_at.unwrap_or_default(); + let key = format!("{}:{reset}", bucket.limit_id); + if memory.seen.get(&key).is_some_and(|seen| seen == level) { + continue; + } + + memory.seen.insert(key, level.to_string()); + let title = match level { + "exhausted" => "Codex limit reached", + "critical" => "Codex limit nearly reached", + _ => "Codex limit warning", + }; + let body = format!( + "{} is at {:.0}% and resets {}.", + bucket + .limit_name + .as_deref() + .unwrap_or(bucket.limit_id.as_str()), + bucket.used_percent, + bucket + .resets_at + .map(format_reset) + .unwrap_or_else(|| "soon".to_string()) + ); + + let _ = app.notification().builder().title(title).body(body).show(); + } + } +} + +fn threshold_level(settings: &AppSettings, percent: f64) -> Option<&'static str> { + if percent >= settings.thresholds.exhausted { + Some("exhausted") + } else if percent >= settings.thresholds.critical { + Some("critical") + } else if percent >= settings.thresholds.warning { + Some("warning") + } else { + None + } +} + +fn format_reset(timestamp: i64) -> String { + let now = Utc::now().timestamp(); + if timestamp <= now { + return "now".to_string(); + } + + let seconds = timestamp - now; + if seconds < 60 { + format!("in {seconds}s") + } else { + format!("in {}m", seconds / 60) + } +} diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs new file mode 100644 index 0000000..fd1e4d7 --- /dev/null +++ b/src-tauri/src/settings.rs @@ -0,0 +1,173 @@ +use serde::{Deserialize, Serialize}; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BadgePosition { + pub x: i32, + pub y: i32, +} + +impl Default for BadgePosition { + fn default() -> Self { + Self { x: 1530, y: 18 } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct NotificationThresholds { + pub warning: f64, + pub critical: f64, + pub exhausted: f64, +} + +impl Default for NotificationThresholds { + fn default() -> Self { + Self { + warning: 70.0, + critical: 90.0, + exhausted: 100.0, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AppSettings { + pub codex_executable: Option, + pub codex_home: String, + pub refresh_interval_secs: u64, + pub badge_visible: bool, + pub badge_position: BadgePosition, + pub privacy_mode: bool, + pub thresholds: NotificationThresholds, + pub notifications_muted_until: Option, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + codex_executable: None, + codex_home: default_codex_home().to_string_lossy().to_string(), + refresh_interval_secs: 60, + badge_visible: true, + badge_position: BadgePosition::default(), + privacy_mode: false, + thresholds: NotificationThresholds::default(), + notifications_muted_until: None, + } + } +} + +impl AppSettings { + pub fn load() -> io::Result { + let path = settings_path(); + if !path.exists() { + return Ok(Self::default()); + } + + let body = fs::read_to_string(path)?; + let mut settings = serde_json::from_str::(&body).unwrap_or_default(); + settings.normalize(); + Ok(settings) + } + + pub fn save(&self) -> io::Result<()> { + let path = settings_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let body = serde_json::to_string_pretty(self).map_err(io::Error::other)?; + fs::write(path, body) + } + + pub fn codex_home_path(&self) -> PathBuf { + PathBuf::from(&self.codex_home) + } + + pub fn resolve_codex_executable(&self) -> String { + if let Some(path) = self + .codex_executable + .as_ref() + .filter(|value| !value.is_empty()) + { + return path.clone(); + } + + for candidate in codex_executable_candidates() { + if candidate.exists() { + return candidate.to_string_lossy().to_string(); + } + } + + "codex".to_string() + } + + fn normalize(&mut self) { + if self.refresh_interval_secs < 15 { + self.refresh_interval_secs = 15; + } + if self.codex_home.trim().is_empty() { + self.codex_home = default_codex_home().to_string_lossy().to_string(); + } + } +} + +pub fn settings_path() -> PathBuf { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("CodexLimitMonitor") + .join("settings.json") +} + +fn default_codex_home() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".codex") +} + +fn codex_executable_candidates() -> Vec { + let mut candidates = Vec::new(); + + if let Some(app_data) = std::env::var_os("APPDATA") { + candidates.push(Path::new(&app_data).join("npm").join("codex.cmd")); + } + + candidates.push( + Path::new("C:\\Program Files") + .join("nodejs") + .join("codex.cmd"), + ); + candidates.push(Path::new("C:\\Program Files").join("nodejs").join("codex")); + + if let Some(home) = dirs::home_dir() { + candidates.push( + home.join(".config") + .join("herd") + .join("bin") + .join("nvm") + .join("v25.2.1") + .join("node_modules") + .join(".bin") + .join("codex.cmd"), + ); + candidates.push( + home.join(".config") + .join("herd") + .join("bin") + .join("nvm") + .join("v25.2.1") + .join("node_modules") + .join("@openai") + .join("codex") + .join("bin") + .join("codex.js"), + ); + } + + candidates +} diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs new file mode 100644 index 0000000..2faa636 --- /dev/null +++ b/src-tauri/src/tray.rs @@ -0,0 +1,120 @@ +use crate::{models::MonitorSnapshot, show_dashboard}; +use chrono::{Local, TimeZone}; +use tauri::{ + image::Image, + menu::{Menu, MenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + AppHandle, Emitter, Manager, +}; + +const TRAY_ID: &str = "codex-limit-monitor"; + +pub fn setup_tray(app: &AppHandle) -> tauri::Result<()> { + let open = MenuItem::with_id(app, "open", "Open Dashboard", true, None::<&str>)?; + let refresh = MenuItem::with_id(app, "refresh", "Refresh Now", true, None::<&str>)?; + let badge = MenuItem::with_id(app, "badge", "Toggle Badge", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&open, &refresh, &badge, &quit])?; + + TrayIconBuilder::with_id(TRAY_ID) + .icon(app_tray_icon()) + .tooltip("Codex Limit Monitor") + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| match event.id().as_ref() { + "open" => { + let _ = show_dashboard(app); + } + "refresh" => { + let app = app.clone(); + let monitor = app + .try_state::() + .map(|state| state.monitor.clone()); + tauri::async_runtime::spawn(async move { + if let Some(monitor) = monitor { + let _ = monitor.refresh(Some(app.clone())).await; + } + }); + } + "badge" => { + let app = app.clone(); + let monitor = app + .try_state::() + .map(|state| state.monitor.clone()); + tauri::async_runtime::spawn(async move { + if let Some(monitor) = monitor { + let mut settings = monitor.settings().await; + settings.badge_visible = !settings.badge_visible; + if settings.save().is_ok() { + monitor.replace_settings(settings.clone()).await; + let _ = crate::apply_badge_visibility(&app, settings.badge_visible); + let _ = app.emit("settings_updated", &settings); + } + } + }); + } + "quit" => app.exit(0), + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + let _ = show_dashboard(tray.app_handle()); + } + }) + .build(app)?; + + Ok(()) +} + +pub fn update_tray(app: &AppHandle, snapshot: &MonitorSnapshot) { + let Some(tray) = app.tray_by_id(TRAY_ID) else { + return; + }; + + let _ = tray.set_tooltip(Some(&tooltip(snapshot))); +} + +fn app_tray_icon() -> Image<'static> { + Image::from_bytes(include_bytes!("../icons/icon.png")).expect("valid embedded tray icon") +} + +fn tooltip(snapshot: &MonitorSnapshot) -> String { + if let Some(bucket) = snapshot.buckets.first() { + let reset = bucket + .resets_at + .map(format_reset_datetime) + .map(|value| format!(", resets {value}")) + .unwrap_or_default(); + let weekly_reset = bucket + .secondary_resets_at + .map(format_reset_datetime) + .map(|value| format!(", weekly resets {value}")) + .unwrap_or_default(); + let weekly = bucket + .secondary_used_percent + .map(|percent| format!("{percent:.0}% weekly")) + .unwrap_or_else(|| "weekly unavailable".to_string()); + format!( + "Codex: {:.0}% current, {}{}{}", + bucket.used_percent, weekly, reset, weekly_reset + ) + } else { + format!( + "Codex usage: {} tokens today ({})", + snapshot.usage_summary.today_tokens, snapshot.live_status.state + ) + } +} + +fn format_reset_datetime(timestamp: i64) -> String { + Local + .timestamp_opt(timestamp, 0) + .single() + .map(|date| date.format("%b %-d, %I:%M %p").to_string()) + .unwrap_or_else(|| "unknown".to_string()) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..6bffd92 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Codex Limit Monitor", + "version": "0.1.0", + "identifier": "dev.meghdad.codex-limit-monitor", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://127.0.0.1:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Codex Limit Monitor", + "width": 1180, + "height": 760, + "minWidth": 900, + "minHeight": 620, + "resizable": true, + "center": true + }, + { + "label": "badge", + "title": "Codex Badge", + "url": "badge.html", + "width": 164, + "height": 82, + "decorations": false, + "transparent": true, + "shadow": false, + "alwaysOnTop": true, + "skipTaskbar": true, + "resizable": false, + "visible": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/icon.ico", + "icons/icon.png", + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png" + ] + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..e2c67c7 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,355 @@ +import { listen } from "@tauri-apps/api/event"; +import { useEffect, useMemo, useState } from "react"; +import { + getSettings, + getSnapshot, + getUsageHistory, + pauseNotifications, + refreshNow, + toggleBadge, + updateSettings, +} from "./api"; +import type { AppSettings, MonitorSnapshot, UsageHistory } from "./types"; +import { + currentLimitPercent, + formatCompact, + formatDateTime, + formatNumber, + statusTone, + truncateTitle, + weeklyLimitPercent, + weeklyLimitResetAt, +} from "./utils"; + +function App() { + const [snapshot, setSnapshot] = useState(null); + const [settings, setSettings] = useState(null); + const [history, setHistory] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + Promise.all([getSnapshot(), getSettings(), getUsageHistory("7d")]) + .then(([nextSnapshot, nextSettings, nextHistory]) => { + setSnapshot(nextSnapshot); + setSettings(nextSettings); + setHistory(nextHistory); + }) + .catch((err) => setError(String(err))); + + const unlisten = listen("rate_limit_updated", (event) => { + setSnapshot(event.payload); + }); + return () => { + unlisten.then((dispose) => dispose()).catch(() => undefined); + }; + }, []); + + const percent = currentLimitPercent(snapshot); + const weeklyPercent = weeklyLimitPercent(snapshot); + const weeklyReset = weeklyLimitResetAt(snapshot); + const tone = statusTone(percent); + const weeklyTone = statusTone(weeklyPercent); + const primaryBucket = snapshot?.buckets[0] ?? null; + const currentThreadTitle = useMemo(() => { + if (!snapshot?.currentThread) return "No active thread"; + if (settings?.privacyMode) return "Hidden by privacy mode"; + return truncateTitle(snapshot.currentThread.title || "Untitled thread", 88); + }, [settings?.privacyMode, snapshot?.currentThread]); + + async function runRefresh() { + setBusy(true); + setError(null); + try { + setSnapshot(await refreshNow()); + setHistory(await getUsageHistory("7d")); + } catch (err) { + setError(String(err)); + } finally { + setBusy(false); + } + } + + async function saveSettings(next: AppSettings) { + setSettings(next); + try { + setSettings(await updateSettings(next)); + } catch (err) { + setError(String(err)); + } + } + + return ( +
+ + +
+
+
+

Windows desktop monitor

+

Codex capacity at a glance

+
+
+ {snapshot?.liveStatus.detail ?? snapshot?.message ?? "Live app-server connection"} +
+
+ + {error &&
{error}
} + {snapshot?.message &&
{snapshot.message}
} + +
+
+
+
+ Codex limits +

{primaryBucket?.limitName ?? primaryBucket?.limitId ?? "Codex"}

+
+ +
+
+ + +
+
+ + + +
+
+ +
+ + + + +
+
+ +
+
+
+
+ Current thread +

{currentThreadTitle}

+
+
+
+ + + +
+
+ +
+
+
+ Token shape +

Last 7 days

+
+
+ +
+
+ +
+
+
+ Usage trend +

Daily tokens

+
+
+ +
+ + {settings && ( +
+
+
+ Settings +

Monitor behavior

+
+
+
+ + + + + +
+
+ )} +
+
+ ); +} + +function StatusPill({ state }: { state: string }) { + return
{state}
; +} + +function Metric({ + label, + value, + detail, +}: { + label: string; + value: string | number; + detail?: string; +}) { + return ( +
+ {label} + {value} + {detail && {detail}} +
+ ); +} + +function LimitGauge({ + label, + percent, + tone, + detail, +}: { + label: string; + percent: number; + tone: string; + detail: string; +}) { + const value = Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : 0; + return ( +
+
+
+ {Number.isFinite(percent) ? `${Math.round(percent)}%` : "--"} + used +
+
+
+ {label} + {detail} +
+
+ ); +} + +function TokenBars({ snapshot }: { snapshot: MonitorSnapshot | null }) { + const breakdown = snapshot?.usageSummary.tokenBreakdown; + const values = [ + ["Input", breakdown?.input ?? 0], + ["Output", breakdown?.output ?? 0], + ["Cached", breakdown?.cached ?? 0], + ["Reasoning", breakdown?.reasoning ?? 0], + ["Tool", breakdown?.tool ?? 0], + ] as const; + const max = Math.max(1, ...values.map(([, value]) => value)); + + return ( +
+ {values.map(([label, value]) => ( +
+ {label} +
+ +
+ {formatCompact(value)} +
+ ))} +
+ ); +} + +function Trend({ history }: { history: UsageHistory | null }) { + const points = history?.points ?? []; + const max = Math.max(1, ...points.map((point) => point.tokens)); + if (points.length === 0) { + return
No local usage history found for this range.
; + } + + return ( +
+ {points.map((point) => ( +
+ + {point.label.slice(5)} +
+ ))} +
+ ); +} + +export default App; diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..9719b7b --- /dev/null +++ b/src/api.ts @@ -0,0 +1,43 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { + AppSettings, + BadgePosition, + MonitorSnapshot, + UsageHistory, +} from "./types"; + +export function getSnapshot(): Promise { + return invoke("get_snapshot"); +} + +export function refreshNow(): Promise { + return invoke("refresh_now"); +} + +export function getUsageHistory(range: string): Promise { + return invoke("get_usage_history", { range }); +} + +export function getSettings(): Promise { + return invoke("get_settings"); +} + +export function updateSettings(settings: AppSettings): Promise { + return invoke("update_settings", { settings }); +} + +export function toggleBadge(visible: boolean): Promise { + return invoke("toggle_badge", { visible }); +} + +export function setBadgePosition(position: BadgePosition): Promise { + return invoke("set_badge_position", { position }); +} + +export function moveBadge(position: BadgePosition): Promise { + return invoke("move_badge", { position }); +} + +export function pauseNotifications(until: number | null): Promise { + return invoke("pause_notifications", { until }); +} diff --git a/src/badge.tsx b/src/badge.tsx new file mode 100644 index 0000000..fdea910 --- /dev/null +++ b/src/badge.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useState } from "react"; +import ReactDOM from "react-dom/client"; +import { listen } from "@tauri-apps/api/event"; +import { getSettings, getSnapshot, moveBadge, setBadgePosition } from "./api"; +import type { BadgePosition, MonitorSnapshot } from "./types"; +import { + currentLimitPercent, + currentLimitResetAt, + formatCountdown, + resetWindowProgress, + statusTone, + weeklyLimitPercent, +} from "./utils"; +import "./styles.css"; + +function Badge() { + const [snapshot, setSnapshot] = useState(null); + const [position, setPosition] = useState({ x: 1530, y: 18 }); + const [refreshing, setRefreshing] = useState(true); + const [nowMs, setNowMs] = useState(Date.now()); + + useEffect(() => { + Promise.all([getSnapshot(), getSettings()]) + .then(([nextSnapshot, settings]) => { + setSnapshot(nextSnapshot); + setPosition(settings.badgePosition); + }) + .catch(() => undefined) + .finally(() => setRefreshing(false)); + const started = listen("refresh_started", () => { + setRefreshing(true); + }); + const unlisten = listen("rate_limit_updated", (event) => { + setSnapshot(event.payload); + setRefreshing(false); + }); + const timer = window.setInterval(() => setNowMs(Date.now()), 30_000); + return () => { + window.clearInterval(timer); + started.then((dispose) => dispose()).catch(() => undefined); + unlisten.then((dispose) => dispose()).catch(() => undefined); + }; + }, []); + + useEffect(() => { + const blockContextMenu = (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + window.addEventListener("contextmenu", blockContextMenu, true); + return () => window.removeEventListener("contextmenu", blockContextMenu, true); + }, []); + + const currentPercent = currentLimitPercent(snapshot); + const weeklyPercent = weeklyLimitPercent(snapshot); + const resetAt = currentLimitResetAt(snapshot); + const resetProgress = resetWindowProgress(resetAt, nowMs); + const peakPercent = Math.max( + Number.isFinite(currentPercent) ? currentPercent : 0, + Number.isFinite(weeklyPercent) ? weeklyPercent : 0, + ); + const tone = statusTone(peakPercent); + + function dragBadge(event: React.PointerEvent) { + if (event.button !== 0) return; + event.preventDefault(); + event.currentTarget.setPointerCapture(event.pointerId); + + const startMouse = { x: event.screenX, y: event.screenY }; + const startPosition = position; + let latest = startPosition; + let scheduled = false; + + const calculate = (pointer: PointerEvent): BadgePosition => ({ + x: Math.round(startPosition.x + pointer.screenX - startMouse.x), + y: Math.round(startPosition.y + pointer.screenY - startMouse.y), + }); + + const onMove = (pointer: PointerEvent) => { + latest = calculate(pointer); + if (scheduled) return; + scheduled = true; + requestAnimationFrame(() => { + scheduled = false; + setPosition(latest); + moveBadge(latest).catch(() => undefined); + }); + }; + + const onUp = (pointer: PointerEvent) => { + latest = calculate(pointer); + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + setPosition(latest); + setBadgePosition(latest).catch(() => undefined); + }; + + window.addEventListener("pointermove", onMove); + window.addEventListener("pointerup", onUp, { once: true }); + } + + return ( +
+ +
+ +
+
+ + +
+
+ ); +} + +function MiniUsage({ label, percent }: { label: string; percent: number }) { + const value = Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : 0; + return ( +
+
+ {label} + {Number.isFinite(percent) ? `${Math.round(value)}%` : "--"} +
+
+ +
+
+ ); +} + +ReactDOM.createRoot(document.getElementById("badge-root") as HTMLElement).render( + + + , +); diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..81da346 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..af267ab --- /dev/null +++ b/src/styles.css @@ -0,0 +1,695 @@ +:root { + color-scheme: dark; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: #111318; + color: #edf0f7; + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: + linear-gradient(180deg, rgba(38, 44, 54, 0.98), rgba(16, 18, 24, 1) 45%), + #111318; +} + +.badge-html, +.badge-html body, +.badge-html #badge-root { + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; + background: transparent !important; +} + +button, +input { + font: inherit; +} + +button { + cursor: pointer; +} + +.app-shell { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + display: flex; + flex-direction: column; + gap: 14px; + padding: 22px; + border-right: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(11, 13, 18, 0.72); +} + +.brand-block { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 10px; +} + +.brand-mark { + display: grid; + width: 44px; + height: 44px; + place-items: center; + border-radius: 8px; + background: #e9f6ff; + color: #101318; + font-weight: 800; +} + +.brand-block h1, +.topbar h2, +.panel h3, +.limit-panel h3, +.settings-panel h3 { + margin: 0; + letter-spacing: 0; +} + +.brand-block h1 { + font-size: 16px; +} + +.brand-block p, +.eyebrow, +.panel-header span, +.metric span, +.thread-row span, +.sidebar-note span { + color: #9ea7b8; +} + +.brand-block p { + margin: 2px 0 0; + font-size: 12px; +} + +.status-pill, +.connection { + width: fit-content; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 999px; + padding: 7px 11px; + color: #cdd5e3; + background: rgba(255, 255, 255, 0.06); + text-transform: capitalize; +} + +.status-pill.online, +.connection.online { + border-color: rgba(34, 197, 94, 0.35); + color: #a7f3d0; +} + +.status-pill.fallback, +.connection.fallback { + border-color: rgba(245, 158, 11, 0.42); + color: #fde68a; +} + +.status-pill.offline, +.connection.offline { + border-color: rgba(239, 68, 68, 0.42); + color: #fecaca; +} + +.primary-action, +.secondary-action { + min-height: 40px; + border: 0; + border-radius: 8px; + padding: 0 13px; + font-weight: 700; +} + +.primary-action { + background: #f4f7fb; + color: #111318; +} + +.primary-action:disabled { + opacity: 0.55; +} + +.secondary-action { + border: 1px solid rgba(255, 255, 255, 0.11); + background: rgba(255, 255, 255, 0.07); + color: #edf0f7; +} + +.sidebar-note { + display: grid; + gap: 4px; + margin-top: auto; + padding: 12px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.055); +} + +.sidebar-note strong { + overflow-wrap: anywhere; +} + +.content { + display: flex; + flex-direction: column; + gap: 18px; + padding: 26px; + min-width: 0; +} + +.topbar { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.eyebrow { + margin: 0 0 5px; + font-size: 12px; + text-transform: uppercase; +} + +.topbar h2 { + font-size: 28px; +} + +.connection { + max-width: 460px; + text-transform: none; + text-align: right; +} + +.alert { + border: 1px solid rgba(245, 158, 11, 0.28); + border-radius: 8px; + padding: 11px 13px; + background: rgba(245, 158, 11, 0.09); + color: #fde68a; +} + +.alert.danger { + border-color: rgba(239, 68, 68, 0.32); + background: rgba(239, 68, 68, 0.09); + color: #fecaca; +} + +.overview-grid, +.work-grid { + display: grid; + grid-template-columns: minmax(320px, 0.92fr) minmax(340px, 1fr); + gap: 18px; +} + +.work-grid.lower { + grid-template-columns: minmax(420px, 1.1fr) minmax(320px, 0.9fr); +} + +.limit-panel, +.metric-panel, +.panel, +.settings-panel { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + background: rgba(255, 255, 255, 0.055); + box-shadow: 0 18px 60px rgba(0, 0, 0, 0.18); +} + +.limit-panel, +.panel, +.settings-panel { + padding: 18px; +} + +.metric-panel { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px; + overflow: hidden; +} + +.metric-panel .metric { + padding: 18px; + background: rgba(255, 255, 255, 0.025); +} + +.panel-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.tone-dot { + width: 12px; + height: 12px; + border-radius: 999px; + background: #6b7280; +} + +.tone-dot.ok { + background: #22c55e; +} + +.tone-dot.warn { + background: #f59e0b; +} + +.tone-dot.danger { + background: #ef4444; +} + +.gauge { + display: grid; + place-items: center; + margin: 8px auto 18px; + width: 220px; + aspect-ratio: 1; + border-radius: 50%; + background: + radial-gradient(circle at 50% 50%, #151922 0 57%, transparent 58%), + conic-gradient(var(--accent) calc(var(--value) * 1%), rgba(255, 255, 255, 0.08) 0); +} + +.gauge.ok { + --accent: #22c55e; +} + +.gauge.warn { + --accent: #f59e0b; +} + +.gauge.danger { + --accent: #ef4444; +} + +.gauge.muted { + --accent: #64748b; +} + +.gauge strong { + display: block; + font-size: 42px; +} + +.gauge span { + color: #9ea7b8; +} + +.limit-gauge-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 18px; +} + +.limit-gauge-card { + display: grid; + justify-items: center; + gap: 10px; + min-width: 0; + padding: 14px 10px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.035); +} + +.limit-gauge-card .gauge { + width: 132px; + margin: 0; +} + +.limit-gauge-card .gauge strong { + font-size: 28px; +} + +.limit-gauge-card > div:last-child { + display: grid; + gap: 2px; + text-align: center; +} + +.limit-gauge-card > div:last-child span { + color: #9ea7b8; +} + +.limit-meta, +.thread-current { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.metric { + display: grid; + gap: 4px; + min-width: 0; +} + +.metric strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 24px; +} + +.metric em { + color: #b6c0cf; + font-style: normal; +} + +.bars { + display: grid; + gap: 12px; +} + +.bar-row { + display: grid; + grid-template-columns: 86px minmax(0, 1fr) 70px; + gap: 10px; + align-items: center; +} + +.bar-row > span { + color: #b6c0cf; +} + +.bar-row > div { + height: 10px; + border-radius: 999px; + overflow: hidden; + background: rgba(255, 255, 255, 0.08); +} + +.bar-row i { + display: block; + height: 100%; + border-radius: inherit; + background: #67e8f9; +} + +.bar-row strong { + text-align: right; +} + +.thread-list { + display: grid; + gap: 10px; +} + +.thread-row { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: center; + padding: 11px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.045); + min-width: 0; +} + +.thread-row div { + min-width: 0; + flex: 1 1 auto; +} + +.thread-row strong, +.thread-row span { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.thread-row em { + color: #e0f2fe; + font-style: normal; + font-weight: 700; +} + +.trend { + display: grid; + grid-auto-flow: column; + grid-auto-columns: minmax(28px, 1fr); + gap: 8px; + height: 210px; + align-items: end; +} + +.trend-column { + display: grid; + grid-template-rows: 1fr 18px; + gap: 7px; + height: 100%; + align-items: end; +} + +.trend-column i { + display: block; + min-height: 8px; + border-radius: 5px 5px 2px 2px; + background: linear-gradient(180deg, #a7f3d0, #22c55e); +} + +.trend-column span { + color: #9ea7b8; + font-size: 11px; + text-align: center; +} + +.empty-state { + display: grid; + min-height: 160px; + place-items: center; + color: #9ea7b8; +} + +.settings-grid { + display: grid; + grid-template-columns: 1.2fr 1.2fr 0.6fr 0.6fr auto; + gap: 12px; + align-items: end; +} + +.settings-grid label { + display: grid; + gap: 7px; + color: #b6c0cf; +} + +.settings-grid input { + width: 100%; + min-height: 40px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 0 11px; + background: rgba(0, 0, 0, 0.22); + color: #edf0f7; +} + +.toggle-row { + grid-template-columns: 1fr auto; + align-items: center; +} + +.toggle-row input { + width: 20px; +} + +.badge-body { + width: 100vw; + height: 100vh; + min-width: 0; + min-height: 0; + overflow: hidden; + background: transparent; +} + +.badge-shell { + display: grid; + grid-template-rows: 5px minmax(0, 1fr); + align-items: stretch; + gap: 6px; + position: relative; + width: 164px; + height: 82px; + margin: 0; + border: 1px solid rgba(34, 197, 94, 0.48); + border-radius: 8px; + padding: 10px 10px 12px; + color: white; + background: rgba(17, 19, 24, 0.92); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.34); + cursor: move; + user-select: none; +} + +.badge-shell.ok { + border-color: rgba(34, 197, 94, 0.48); +} + +.badge-shell.warn { + border-color: rgba(245, 158, 11, 0.55); +} + +.badge-shell.danger { + border-color: rgba(239, 68, 68, 0.62); +} + +.badge-shell span { + color: #cbd5e1; + font-size: 12px; +} + +.badge-charts { + display: grid; + gap: 6px; + min-width: 0; +} + +.badge-chart { + display: grid; + grid-template-rows: 12px 8px; + gap: 3px; + min-width: 0; +} + +.badge-chart header { + display: flex; + justify-content: space-between; + gap: 8px; + min-width: 0; +} + +.badge-chart span, +.badge-chart strong { + color: #9ea7b8; + font-size: 10px; + font-weight: 800; + line-height: 1; +} + +.badge-chart strong { + color: #edf0f7; + text-align: right; +} + +.badge-chart-track { + height: 8px; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.13); +} + +.badge-chart i { + display: block; + height: 100%; + min-width: 3px; + border-radius: inherit; + background: #22c55e; +} + +.badge-shell.warn .badge-chart i { + background: #f59e0b; +} + +.badge-shell.danger .badge-chart i { + background: #ef4444; +} + +.badge-activity { + position: absolute; + top: 0; + left: 8px; + right: 8px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 5px; + opacity: 0; + pointer-events: none; +} + +.badge-activity.active { + opacity: 1; +} + +.badge-activity i { + display: block; + height: 3px; + border-radius: 0 0 999px 999px; + background: #67e8f9; + animation: badge-pulse 0.8s ease-in-out infinite; +} + +.badge-reset { + align-self: end; + height: 5px; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.12); +} + +.badge-reset i { + display: block; + min-width: 3px; + height: 100%; + border-radius: inherit; + background: #67e8f9; +} + +.badge-activity i:nth-child(2) { + animation-delay: 0.12s; +} + +.badge-activity i:nth-child(3) { + animation-delay: 0.24s; +} + +@keyframes badge-pulse { + 0%, + 100% { + opacity: 0.25; + transform: scaleX(0.72); + } + + 50% { + opacity: 1; + transform: scaleX(1); + } +} + +@media (max-width: 980px) { + .app-shell, + .overview-grid, + .work-grid, + .work-grid.lower { + grid-template-columns: 1fr; + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + } + + .settings-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..968e43e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,96 @@ +export type RateLimitBucket = { + limitId: string; + limitName: string | null; + usedPercent: number; + windowDurationMins: number | null; + resetsAt: number | null; + secondaryUsedPercent: number | null; + secondaryResetsAt: number | null; + reachedType: string | null; +}; + +export type LiveStatus = { + state: "online" | "fallback" | "offline" | string; + detail: string | null; +}; + +export type TokenBreakdown = { + input: number; + output: number; + cached: number; + reasoning: number; + tool: number; + responses: number; +}; + +export type ThreadUsage = { + id: string; + title: string; + tokensUsed: number; + model: string | null; + reasoningEffort: string | null; + updatedAt: number; + cwd: string | null; +}; + +export type ModelUsage = { + model: string; + tokens: number; + threads: number; +}; + +export type UsageSummary = { + totalTokens: number; + todayTokens: number; + weekTokens: number; + threadCount: number; + todayThreadCount: number; + weekThreadCount: number; + currentThread: ThreadUsage | null; + recentThreads: ThreadUsage[]; + modelBreakdown: ModelUsage[]; + tokenBreakdown: TokenBreakdown; + sourcePath: string | null; + error: string | null; +}; + +export type MonitorSnapshot = { + source: string; + fetchedAt: number; + liveStatus: LiveStatus; + buckets: RateLimitBucket[]; + usageSummary: UsageSummary; + currentThread: ThreadUsage | null; + message: string | null; +}; + +export type UsagePoint = { + label: string; + tokens: number; + threads: number; +}; + +export type UsageHistory = { + range: string; + points: UsagePoint[]; +}; + +export type BadgePosition = { + x: number; + y: number; +}; + +export type AppSettings = { + codexExecutable: string | null; + codexHome: string; + refreshIntervalSecs: number; + badgeVisible: boolean; + badgePosition: BadgePosition; + privacyMode: boolean; + thresholds: { + warning: number; + critical: number; + exhausted: number; + }; + notificationsMutedUntil: number | null; +}; diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..094bc68 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { + currentLimitPercent, + formatCountdown, + resetWindowProgress, + statusTone, + truncateTitle, + weeklyLimitPercent, + weeklyLimitResetAt, +} from "./utils"; +import type { MonitorSnapshot } from "./types"; + +describe("statusTone", () => { + it("maps percentages to tones", () => { + expect(statusTone(Number.NaN)).toBe("muted"); + expect(statusTone(10)).toBe("ok"); + expect(statusTone(70)).toBe("warn"); + expect(statusTone(90)).toBe("danger"); + }); +}); + +describe("formatCountdown", () => { + it("formats future reset timestamps", () => { + const future = Math.round(Date.now() / 1000) + 120; + expect(formatCountdown(future)).toBe("2m"); + }); +}); + +describe("limit helpers", () => { + it("separates current and weekly app-server percentages", () => { + const snapshot = { + buckets: [ + { + limitId: "codex", + limitName: null, + usedPercent: 8, + windowDurationMins: 300, + resetsAt: 100, + secondaryUsedPercent: 44, + secondaryResetsAt: 200, + reachedType: null, + }, + ], + } as MonitorSnapshot; + + expect(currentLimitPercent(snapshot)).toBe(8); + expect(weeklyLimitPercent(snapshot)).toBe(44); + expect(weeklyLimitResetAt(snapshot)).toBe(200); + }); +}); + +describe("truncateTitle", () => { + it("limits long titles", () => { + expect(truncateTitle("abcdefghijklmnopqrstuvwxyz", 10)).toBe("abcdefg..."); + }); +}); + +describe("resetWindowProgress", () => { + it("fills based on elapsed time in the five hour reset window", () => { + const resetAt = Date.UTC(2026, 0, 1, 5, 0, 0) / 1000; + const nowMs = Date.UTC(2026, 0, 1, 1, 0, 0); + + expect(resetWindowProgress(resetAt, nowMs)).toBe(20); + }); +}); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..c0ffb53 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,92 @@ +import type { MonitorSnapshot } from "./types"; + +export function formatNumber(value: number): string { + return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value); +} + +export function formatCompact(value: number): string { + return new Intl.NumberFormat(undefined, { + notation: "compact", + maximumFractionDigits: 1, + }).format(value); +} + +export function maxBucketPercent(snapshot: MonitorSnapshot | null): number { + if (!snapshot || snapshot.buckets.length === 0) return Number.NaN; + return Math.max(...snapshot.buckets.map((bucket) => bucket.usedPercent)); +} + +export function currentLimitPercent(snapshot: MonitorSnapshot | null): number { + return snapshot?.buckets[0]?.usedPercent ?? Number.NaN; +} + +export function weeklyLimitPercent(snapshot: MonitorSnapshot | null): number { + const primary = snapshot?.buckets[0]; + if (typeof primary?.secondaryUsedPercent === "number") { + return primary.secondaryUsedPercent; + } + + const weeklyBucket = snapshot?.buckets.find( + (bucket) => (bucket.windowDurationMins ?? 0) >= 10_080, + ); + return weeklyBucket?.usedPercent ?? Number.NaN; +} + +export function weeklyLimitResetAt(snapshot: MonitorSnapshot | null): number | null { + const primary = snapshot?.buckets[0]; + if (typeof primary?.secondaryResetsAt === "number") { + return primary.secondaryResetsAt; + } + + const weeklyBucket = snapshot?.buckets.find( + (bucket) => (bucket.windowDurationMins ?? 0) >= 10_080, + ); + return weeklyBucket?.resetsAt ?? null; +} + +export function currentLimitResetAt(snapshot: MonitorSnapshot | null): number | null { + return snapshot?.buckets[0]?.resetsAt ?? null; +} + +export function resetWindowProgress( + resetAt: number | null | undefined, + nowMs = Date.now(), + windowHours = 5, +): number { + if (typeof resetAt !== "number" || !Number.isFinite(resetAt)) return Number.NaN; + const windowMs = windowHours * 60 * 60 * 1000; + const resetMs = resetAt * 1000; + const startMs = resetMs - windowMs; + return Math.max(0, Math.min(100, ((nowMs - startMs) / windowMs) * 100)); +} + +export function statusTone(percent: number): "ok" | "warn" | "danger" | "muted" { + if (!Number.isFinite(percent)) return "muted"; + if (percent >= 90) return "danger"; + if (percent >= 70) return "warn"; + return "ok"; +} + +export function formatCountdown(timestamp: number): string { + const seconds = Math.max(0, Math.round(timestamp - Date.now() / 1000)); + if (seconds === 0) return "resetting"; + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.round(seconds / 60)}m`; + return `${Math.round(seconds / 3600)}h`; +} + +export function formatDateTime(timestamp: number): string { + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(timestamp * 1000)); +} + +export function truncateTitle(title: string | null | undefined, max = 74): string { + const clean = (title ?? "Untitled").trim() || "Untitled"; + if (clean.length <= max) return clean; + if (max <= 3) return clean.slice(0, max); + return `${clean.slice(0, max - 3)}...`; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..df11947 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5a4b8c1 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { resolve } from "node:path"; + +export default defineConfig({ + plugins: [react()], + clearScreen: false, + server: { + strictPort: true, + host: "127.0.0.1", + port: 1420, + }, + envPrefix: ["VITE_", "TAURI_"], + build: { + target: "es2022", + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + badge: resolve(__dirname, "badge.html"), + }, + }, + }, +});