Compare commits

...
Author SHA1 Message Date
renovate[bot] 6e3be5a27f chore(deps): update node.js to v26.13.1 2026-11-11 17:45:06 +00:00
4adac9e4b6 fix(config): stop rewriting unchanged configs on exit (#8017)
* fix(config): stop rewriting unchanged configs on exit

apply_all_and_save_file() committed the drafts and then saved clash.yaml,
verge.yaml and profiles.yaml unconditionally. With nothing staged that write
replaced each file with content equal to what was already on disk - fields are
encrypted with a fresh nonce, so the bytes differ every time - which reset the
files' creation time on every exit (#7759).

Draft::apply now reports whether it committed a draft, and the exit path saves
only the configs that had one.

* refactor(tests): remove test snippets

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

* docs(changelog): add entry of the PR modifications

Assisted-by: Gemini 3.8 Flash Medium (VSCode Copilot Chat)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

---------

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: sdhfsl <sdhfsl@users.noreply.github.com>
Co-authored-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-11 17:33:03 +00:00
Tunglies 2c60f55f45 feat(connections): add network protocol filter (#8611)
* feat(connections): add network protocol filter

Filter connections by metadata.network (All/TCP/UDP) in both table and
list layouts, combined with search, and kept across page switches like
the existing search state.

Refs #6140

* fix(connections): keep network filter labels in English

Remove the All Networks translation and use All alongside TCP and UDP.\n\nRefs #6140

* fix(connections): narrow network filter select
2026-11-11 09:50:25 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> b8c13c0370 chore(deps): update dependency github/gh-aw to v1.2.2 (#8620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-11 04:02:03 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> f86db4b344 chore(deps): update cargo dependencies (#8619)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-10 17:29:51 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>❤是纱雾酱哟~
389dddf512 chore(deps): update github/gh-aw-actions action to v1.2.2 (#8617)
* chore(deps): update github/gh-aw-actions action to v1.2.2

* ci(workflows): recompile workflow lockfiles with gh-aw v1.2.2

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

* ci(workflows): set permissions to suppress tips

Agentic workflows of this repository are using available credits by some
collaborators' personal API Key. Configuring permissions of these
workflows to suppress warnings by GitHub Agentic Workflow extension
during lockfile compilation.

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

---------

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-11-10 14:01:34 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 304a65fc36 chore(deps): update npm dependencies (#8615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-10 05:17:19 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 9d0bee4af9 chore(deps): update npm dependencies (#8608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-09 16:35:40 +00:00
1e4f709766 fix(proxy): show a profile-defined GLOBAL in rule mode when it is the only group (#8607)
* fix(proxy): show a profile-defined GLOBAL in rule mode when it is the only group

Rule mode renders only proxyView.groups, and the backend always moves
GLOBAL out of that list. A profile whose only group is GLOBAL, or that
has no proxy-groups, therefore showed the "no proxy info" empty state.
Fall back to GLOBAL in that case, as v2.5.2 did before #7542.

Fixes #8223

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(changelog): note the rule-mode GLOBAL fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: drop the regression test and append the changelog entry

Per review: the test was only needed to confirm the fix locally, and new
changelog entries go at the end of their list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: toBerlinWay <toBerlinWay@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 16:33:47 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
aae5dc90ba chore(deps): update cargo dependencies (#8595)
* chore(deps): update cargo dependencies

* fix(deps): select rfd Linux backend

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-11-09 15:10:19 +00:00
Tunglies 05a6e9a64f fix(proxy): unify node switching and connection cleanup (#8592)
Route frontend and tray node selections through the same backend flow for
selection, persistence, state refresh, and connection cleanup. Read the
group's previous selection before switching and preserve the original
rule of closing only connections whose chains contain that selection.

Run optional cleanup in the background and keep its failures separate
from successful selection. Remove redundant frontend effects and unused
IPC commands, and refresh the proxy view for every successful switch.

Refs: 32095daf90
Refs: bea0dde074
2026-11-09 14:28:44 +00:00
Tunglies 7764289ba5 chore(ci): prepare Copilot build environment 2026-11-09 22:38:20 +08:00
Tunglies 417f0b4c32 fix(ci): recognize Copilot assignment login 2026-11-09 22:22:48 +08:00
42 changed files with 1281 additions and 755 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "26.13.0",
"version": "26.13.1",
"pnpmVersion": "12.18.0"
},
"ghcr.io/devcontainers/features/rust:1": {
+3 -3
View File
@@ -5,10 +5,10 @@
"version": "v9.0.0",
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
},
"github/gh-aw-actions/setup@v1.0.1": {
"github/gh-aw-actions/setup@v1.2.2": {
"repo": "github/gh-aw-actions/setup",
"version": "v1.0.1",
"sha": "221ba4f4319de6b8e809d1a15528b3ae6a818aa5"
"version": "v1.2.2",
"sha": "53d0749e19424a918acd82b4d140264ce48f2f14"
}
},
"containers": {
+5 -5
View File
@@ -51,7 +51,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -182,7 +182,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -262,7 +262,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -395,7 +395,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -479,7 +479,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6.1.1
name: Install pnpm
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
+23 -2
View File
@@ -31,6 +31,27 @@ jobs:
with:
persist-credentials: false
- name: Install gh-aw extension
uses: github/gh-aw-actions/setup-cli@406c7719e31d7fe354224d4bb8fe3d4aeb47da1c # v1.2.1
uses: github/gh-aw-actions/setup-cli@53d0749e19424a918acd82b4d140264ce48f2f14 # v1.2.2
with:
version: v1.0.1
version: v1.2.2
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Install Linux build dependencies
run: |
sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install pnpm
uses: pnpm/action-setup@v6
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
cache: 'pnpm'
- name: Install frontend dependencies
env:
HUSKY: 0
run: pnpm install --frozen-lockfile
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+1 -1
View File
@@ -104,7 +104,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
File diff suppressed because one or more lines are too long
+1
View File
@@ -33,6 +33,7 @@ max-daily-ai-credits: 5000
permissions:
issues: read
pull-requests: read
copilot-requests: none
pre-agent-steps:
- name: Make Copilot quota exhaustion non-blocking
+288 -107
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -30,6 +30,7 @@ permissions:
contents: read
issues: read
pull-requests: read
copilot-requests: none
steps:
- name: Load review rules from the base revision
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
if: github.event.action != 'closed'
+8 -8
View File
@@ -46,7 +46,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- name: Validate source and publish the approved snapshot
id: source
env:
@@ -231,7 +231,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -330,7 +330,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -461,7 +461,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
cache: 'pnpm'
- name: Pnpm install and check
@@ -542,7 +542,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
with:
run_install: false
@@ -565,7 +565,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -593,7 +593,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -661,7 +661,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
File diff suppressed because one or more lines are too long
@@ -28,6 +28,7 @@ permissions:
contents: read
issues: read
pull-requests: read
copilot-requests: none
max-ai-credits: 60
max-turns: 12
max-daily-ai-credits: 500
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- name: Install pnpm
uses: pnpm/action-setup@v6
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -39,7 +39,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
node-version: '26.13.1'
- uses: pnpm/action-setup@v6
name: Install pnpm
+1 -1
View File
@@ -1,3 +1,3 @@
nodejs 26.13.0
nodejs 26.13.1
pnpm 12.18.0
rust 1.99.1
Generated
+70 -14
View File
@@ -159,9 +159,9 @@ dependencies = [
[[package]]
name = "arc-swap"
version = "1.9.3"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e"
checksum = "84a22b5d51209f6de83d37a0b33d320acb8da88823e42a94c075140708f032a5"
dependencies = [
"rustversion",
]
@@ -1120,7 +1120,7 @@ dependencies = [
"percent-encoding",
"reqwest 0.13.5",
"reqwest_dav",
"rfd",
"rfd 0.18.0",
"runas",
"rustls",
"scopeguard",
@@ -2404,9 +2404,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.10"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
checksum = "626d4928326b646e70f65ed36fc7cf9f206bdb68a0019ee7c6b4709da4e084c4"
dependencies = [
"crc32fast",
"miniz_oxide 0.9.1",
@@ -3241,9 +3241,9 @@ dependencies = [
[[package]]
name = "http-body-util"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [
"bytes",
"futures-core",
@@ -4070,9 +4070,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lzma-rust2"
version = "0.24.0"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490750560fa7851fbbf5d3ab1a8750f09350ed361ed8aed85b1e1e40587812ee"
checksum = "ef1d73c59a56771343712b22ea5615092c612c16ba291c0979e3105dfeec4890"
dependencies = [
"sha2 0.11.1",
]
@@ -4538,6 +4538,7 @@ dependencies = [
"objc2-core-video",
"objc2-foundation",
"objc2-quartz-core",
"objc2-uniform-type-identifiers",
]
[[package]]
@@ -4759,6 +4760,16 @@ dependencies = [
"objc2-user-notifications",
]
[[package]]
name = "objc2-uniform-type-identifiers"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7902ac02859fc1f7045f8b598c63f1ae0cc7efeaa06a9bc9f3d9a3c955974fa4"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-user-notifications"
version = "0.3.2"
@@ -5302,6 +5313,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "pollster"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336"
[[package]]
name = "polonius-the-crab"
version = "0.5.0"
@@ -5934,6 +5951,31 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rfd"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4b9d4857b10297ffb0f64cae711e983a7aeaee561848d56d429d3858bd8ed93"
dependencies = [
"block2",
"dispatch2",
"js-sys",
"libc",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-uniform-type-identifiers",
"percent-encoding",
"pollster",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -7427,7 +7469,7 @@ checksum = "ce0fb9c2b6d34a9720627ed930ebc8af26f61e648695aecaeb03d5c33c5896f5"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"rfd 0.17.2",
"serde",
"serde_json",
"tauri",
@@ -10032,6 +10074,20 @@ name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zerotrie"
@@ -10070,9 +10126,9 @@ dependencies = [
[[package]]
name = "zip"
version = "9.3.0"
version = "9.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaeb7f16d2605b49aef586996ded29e65642fab7259a3077e537351df6d80426"
checksum = "0700e6c427aff64acdbf02e85fbfd946c2ee12319d1af013092f3a2178914ff8"
dependencies = [
"aes",
"bzip2",
@@ -10100,9 +10156,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
version = "0.6.6"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5"
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
[[package]]
name = "zmij"
+4
View File
@@ -24,10 +24,13 @@
<details>
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复应用退出时无条件重写 `clash.yaml`、`verge.yaml` 和 `profiles.yaml` 导致文件创建时间丢失的问题
- 修复检测更新后又发布新版时,应用内更新失败
- 修复订阅响应过大导致内存占用过高,限制单次下载为 4 MiB
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
- 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」
**🍎 macOS**
@@ -39,6 +42,7 @@
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增连接页面按网络协议(TCP/UDP)筛选连接
</details>
+8 -4
View File
@@ -74,12 +74,16 @@ impl<T: Clone> Draft<T> {
result
}
/// Commits and clears the draft.
/// Commits and clears the draft, reporting whether one was staged.
#[inline]
pub fn apply(&self) {
pub fn apply(&self) -> bool {
let mut guard = self.inner.data.lock();
if let Some(d) = guard.1.take() {
guard.0 = d;
match guard.1.take() {
Some(d) => {
guard.0 = d;
true
}
None => false,
}
}
+4 -4
View File
@@ -67,7 +67,7 @@
"dayjs": "1.11.23",
"foxact": "^0.3.12",
"foxts": "^5.9.1",
"i18next": "^26.4.2",
"i18next": "^27.0.0",
"js-yaml": "^5.4.2",
"lodash-es": "^4.18.1",
"meta-json-schema": "^1.19.31",
@@ -79,7 +79,7 @@
"react-dom": "19.3.1",
"react-error-boundary": "6.1.6",
"react-hook-form": "^7.88.0",
"react-i18next": "17.1.13",
"react-i18next": "18.0.1",
"react-markdown": "10.1.0",
"react-router": "^8.4.0",
"rehype-raw": "^7.0.0",
@@ -121,7 +121,7 @@
"https-proxy-agent": "^9.1.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"knip": "6.44.1",
"knip": "6.44.2",
"lint-staged": "^17.5.1",
"sass": "^1.104.1",
"tar": "^7.5.22",
@@ -133,7 +133,7 @@
"vite-plugin-svgr": "^5.2.0",
"vitest": "^5.0.1",
"webdriverio": "10.2.0",
"ws": "8.22.1"
"ws": "8.22.2"
},
"lint-staged": {
"*.{ts,tsx,js,mjs}": [
+110 -118
View File
@@ -239,8 +239,8 @@ importers:
specifier: ^5.9.1
version: 5.9.1
i18next:
specifier: ^26.4.2
version: 26.4.2(typescript@6.0.3)
specifier: ^27.0.0
version: 27.0.1(typescript@6.0.3)
js-yaml:
specifier: ^5.4.2
version: 5.4.2
@@ -275,8 +275,8 @@ importers:
specifier: ^7.88.0
version: 7.88.0(react@19.3.1)
react-i18next:
specifier: 17.1.13
version: 17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
specifier: 18.0.1
version: 18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3)
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
@@ -396,8 +396,8 @@ importers:
specifier: ^2.7.0
version: 2.7.0
knip:
specifier: 6.44.1
version: 6.44.1
specifier: 6.44.2
version: 6.44.2
lint-staged:
specifier: ^17.5.1
version: 17.5.1
@@ -432,8 +432,8 @@ importers:
specifier: 10.2.0
version: 10.2.0(supports-color@7.2.0)(yauzl@2.10.0)
ws:
specifier: 8.22.1
version: 8.22.1
specifier: 8.22.2
version: 8.22.2
packages:
@@ -1595,131 +1595,128 @@ packages:
'@octokit/types@18.0.0':
resolution: {integrity: sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==}
'@oxc-parser/binding-android-arm-eabi@0.155.0':
resolution: {integrity: sha512-A6xv7hDtnTm+xlZ7utn6rRQpRMJHRhVr3gjRGqlx0RUX84mNk0pAsy6YN/wWhuOTkaZhRrrzaLxf0NfbH77j2w==}
'@oxc-parser/binding-android-arm-eabi@0.156.0':
resolution: {integrity: sha512-RMq7DKNzEp1T51QKNLTn0NryNEt2IYfh7mZBlIdi2Ju52ltOEDMeIXv0XBpA6dL7j7ChjlcovtGcc1Jv4qMipg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@oxc-parser/binding-android-arm64@0.155.0':
resolution: {integrity: sha512-T1wWwH9/JI5g7Z4kEQwDOwhToxdDhRyOTklH+uuNpJ16m3MZFmGl+l2rKc0NHQdbVRG2LTOBsNjpW3h1BWytPA==}
'@oxc-parser/binding-android-arm64@0.156.0':
resolution: {integrity: sha512-mwkWKhOBpmh8fWc3m4MiLsmEAtx0YuHmvyo9ff557cnb3QZ2kt2eauSz75wCGIpiZG3gUsJ4WwzEjm4R4/BHig==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@oxc-parser/binding-darwin-arm64@0.155.0':
resolution: {integrity: sha512-ZoRB3qJxJ8DWA41jHja+YeB0lLfjBy4T+Kr15vnN1k65X3zbJt3rPZftEwN4Ju4tmA4K/T9dWWD7Z8ewQLnrVA==}
'@oxc-parser/binding-darwin-arm64@0.156.0':
resolution: {integrity: sha512-6pGR5fUszM9M4eBBapqpGjCRJolFwuCldx8bRQjqXyM09cQysBTWVF2Ttg4UNJVxYSaw5RRINbveCOOCrsuGvA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@oxc-parser/binding-darwin-x64@0.155.0':
resolution: {integrity: sha512-vxtL86BtM+IgrpejA86nbTk5j0HKTIMeuaJ1fQciw6p9RfavPtIPjI7gN34owap0yG9txXBaAWpGdxokNwdqIw==}
'@oxc-parser/binding-darwin-x64@0.156.0':
resolution: {integrity: sha512-M9QghL7uCAqMKjYJv1rDGblfEvkeQu3QzTPd7Uwo16gG3uO7m9ikVQ9OL8FZTnKzU4vbCEby6kTlEkqN+JeEMA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@oxc-parser/binding-freebsd-x64@0.155.0':
resolution: {integrity: sha512-tN6Ru5e0UVnWaIuiH8jEwnvxuxg7FvhWwSd3oe5oDhOD4ObsSr9/N6WfHLjilUWlPPj65ptwrSDH0Y7lzwTzmw==}
'@oxc-parser/binding-freebsd-x64@0.156.0':
resolution: {integrity: sha512-lX0ANxSwAOrpOT7NHB+YsrBqDJ8sOVon5m7GS9eQtkuUeNzQMMGpFsJLsStawTbrdwOF5KDVxgPWcKmt/DIZUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@oxc-parser/binding-linux-arm-gnueabihf@0.155.0':
resolution: {integrity: sha512-h4ozA5n6L6lwolsz9iwunEhKWnIVbmFRots5QxEix0H6nJ5ICpuDa7PuShuhfpIDFe3+Qvyscioyiv+9g+4Syg==}
'@oxc-parser/binding-linux-arm-gnueabihf@0.156.0':
resolution: {integrity: sha512-iPgYpasRcqk7X3TGncQg7B+6RotDvG8weug0UrgF+FXLf4KmaNDyT91BKONlqbHcNgK9Nryp7AEZGuQn32fP6g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm-musleabihf@0.155.0':
resolution: {integrity: sha512-058/+LardpfuUlsKIvBGk/CYoakEVcMgqYx8GgyUZeOxFWTojYLlCc1Gb60/y21v3esDhHidgiY/Zbq9LpBhaQ==}
'@oxc-parser/binding-linux-arm-musleabihf@0.156.0':
resolution: {integrity: sha512-sJqJDEZevtkEfoxrXfaRTkSt6Itckr549cIfBbs11UgXv8bFyqQsXTQa6iKm8mTiWYmCDDFJf7TLnqxcbqzPOw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm64-gnu@0.155.0':
resolution: {integrity: sha512-nrOt2Y3xIAzu+lSPzH0864ieO+iCCXgoo9qFlGANE8Xh0cQ+RP8Wstfp+/wXN2YCyb98cAGzX0VruKYw7it/jw==}
'@oxc-parser/binding-linux-arm64-gnu@0.156.0':
resolution: {integrity: sha512-mrQs5OdRiqKskRuWEORPKT6YGIFhrpVvCvL9hqVaIh60EzgR4qjPvSsV6hWZcOfyWHBTbdEvfnDJPh/eQlxPRQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-arm64-musl@0.155.0':
resolution: {integrity: sha512-KYAayyN5hQbJPtGKtRxlyc8XYmyntOplyXSQzTDrmbz7l/GCrzQbSswvyJFBTH1LGtsZrbXOAfN52i/Kz6kLKg==}
'@oxc-parser/binding-linux-arm64-musl@0.156.0':
resolution: {integrity: sha512-ZtPqgCU3+wpU7dvX7YbaEiw71r0UdguzuPhcx4PIEZSs4XFqc6F5wXCtNdb6M7m3DMErlxrwosryoXrzYOY/Gg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-parser/binding-linux-ppc64-gnu@0.155.0':
resolution: {integrity: sha512-CSuSY5hfjtsBnHXydOj8jiNfS1qHbdPZuPz1YL1cSgK7VmNYP+QlZIbG0BBYYBgX/vYEgZwbG9mBdW4YairZ5w==}
'@oxc-parser/binding-linux-ppc64-gnu@0.156.0':
resolution: {integrity: sha512-ZuGRT/adReZZJn3PYNneb1HQIOH0iIZ2fzFgC1/XJYpMQ5KsRKmWIEzy3wEFsUwIOTomCmnmTZ/zbFYo9z9VeA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-riscv64-gnu@0.155.0':
resolution: {integrity: sha512-7KCfOQm+Nz2N8trAXbQkal4TQ5756WVEmhm35y816YHvyFnEfVsICsiV5k0pEszNf/fRm4cm2i6i18SJ6/iCWg==}
'@oxc-parser/binding-linux-riscv64-gnu@0.156.0':
resolution: {integrity: sha512-T+iWCeAOoEAJpHqr6/pogHIv98g9p8Ctccysm/h962hG4ayX5OL2c0Ih4oYe+JuFndLXy/OASvwMlTPe76sIiw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-riscv64-musl@0.155.0':
resolution: {integrity: sha512-JSlhgoK+/g1TGJI5NuADx2+6syhxnQNLCWLXyO5QLFyetomcr/vIxSdiNlmyAv963YXUzCuwAwRG6npZ5xyJ1A==}
'@oxc-parser/binding-linux-riscv64-musl@0.156.0':
resolution: {integrity: sha512-wL55bI2kVKfwdxScbw60zVgzAqbQbmE5zpSPWPZoZ9RSG8egm6ITE/UWE5mDWlglxz1QElJCcUbpaJCtHD11BQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxc-parser/binding-linux-s390x-gnu@0.155.0':
resolution: {integrity: sha512-WFIX+dPokWp64mi434Md3U3lJaTwNxAphvGjpltoyQjGUsdXavOIRrnp9dOS34Hd75HOAwDxAI+YwWOwqmJbtg==}
'@oxc-parser/binding-linux-s390x-gnu@0.156.0':
resolution: {integrity: sha512-+D+JilhLqwrDLODYEl4qtovVwgxKALb23FkAWBiPvgmUWYgjCV83KKuwwxZJM8GCdKlNA1vt3iKtow8HUFjXtg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-x64-gnu@0.155.0':
resolution: {integrity: sha512-g6xSDC/JpbnVOz5uafMGyK3txBVRFMQTx8opv/yHXGxqV3ZMVl3bf7W8XrSnF68dmy3gM4dgReNryuE53+WkeQ==}
'@oxc-parser/binding-linux-x64-gnu@0.156.0':
resolution: {integrity: sha512-jRfwpN4xKrsEpC+SgrUPY5k0mINYZn7aLrt22I3uQFoumiSgHgsLYgS9f84ntbkvSydH4HiwGuDa2b1XCHaJzw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-parser/binding-linux-x64-musl@0.155.0':
resolution: {integrity: sha512-6/4xlxKlZJWrQ0uwDFPc0H9+xLu7068CKhX3vIDW3SbvP/lVgnVuT5v6dyugeWskWpuxqF/WJ4WKwhxjU3pG2w==}
'@oxc-parser/binding-linux-x64-musl@0.156.0':
resolution: {integrity: sha512-P/ajnypgLmxmuRRyCIZcp2o4lOwmCaXYH5+eqDTcCW+MCTI+csKSw/Zc6GB67jZoVa4ey3q1jXEt7S60adBEMQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-parser/binding-openharmony-arm64@0.155.0':
resolution: {integrity: sha512-tltklL0J0XjDxIRTBXWOBhvc2mitcmES45pIxhwDWfxRyqz7Vkeu0NekTZEIxGE4f/lXgq3vZcjjffteQOVorg==}
'@oxc-parser/binding-openharmony-arm64@0.156.0':
resolution: {integrity: sha512-WSN6rQZZc9KqgZ4lMfjVDvqKhVOCjey/YEZIPisGXmwBqwgWOzru207dyiH50LHcf1n9LO2oceGk/id6RL9v0g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@oxc-parser/binding-win32-arm64-msvc@0.155.0':
resolution: {integrity: sha512-alAb9TzjatUEsOeoncAziUnfdTDxfyswhalFZ9sC6plUgATIUx2NIZ0+bMn6aJ28AduS1OpG/OAisr/YISn4Ig==}
'@oxc-parser/binding-win32-arm64-msvc@0.156.0':
resolution: {integrity: sha512-l21Tz6tjraGBC3JefVeJZ9pojD0fCiB62PydAv+i9nrqJX7z/pDmyDNYLamDr/PRcQ4UQbFwkSnz7NKBB9UMLw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@oxc-parser/binding-win32-ia32-msvc@0.155.0':
resolution: {integrity: sha512-yc5cbSfcGmRv/KpxDFuyk8adEBaWd/9HilWvBbQ4bFbus1V2PUh2mGB2Cfq9rqZTqLeiMT8x0skV+LKp9hOMgQ==}
'@oxc-parser/binding-win32-ia32-msvc@0.156.0':
resolution: {integrity: sha512-CUq0ZFl/RODqPiIgfNNSamkpTj5x8AjBTkR/y1DNVIQiTGnmWqjHcn3o08ZW3q/LgkI4g9G9g6UirmmQFvuOZw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@oxc-parser/binding-win32-x64-msvc@0.155.0':
resolution: {integrity: sha512-8heq6jGq+rfpTDRDjoQiBHw0maQZf3IXkS99pdHQZPoTL5kyF5qyPS0RidUlSdnWDIqxXb5nux7GBD9cT+imUw==}
'@oxc-parser/binding-win32-x64-msvc@0.156.0':
resolution: {integrity: sha512-dcpx5Y1x2v/cs7U/q3SZMvmxfyXjVvqC86jMuw2ZZUbHDd7W2cW7uSMQfg3GLVaE68Su3H9xd/Pc4v6QNyeSRg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@oxc-project/types@0.155.0':
resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==}
'@oxc-project/types@0.156.0':
resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==}
@@ -3692,8 +3689,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
i18next@26.4.2:
resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==}
i18next@27.0.1:
resolution: {integrity: sha512-KaE5ozP8UUeVpOJIwMVbbnpRKpyYQuxd9ZbseJoDzezE3nvLnZZQn20pkzyIJwPI2Uet/Qs3WzPTG812f6F/Eg==}
engines: {node: '>=22'}
peerDependencies:
typescript: ^5 || ^6 || ^7
peerDependenciesMeta:
@@ -3858,8 +3856,8 @@ packages:
keyv@5.6.0:
resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
knip@6.44.1:
resolution: {integrity: sha512-Hbd8nvkIsNgFfXgC7Jsb5LHv7jj6VwA3tmfHd53XPX2z9Vc9RnAZqV0BIiP4OrtYqKwCoLseFHQZerb1bNlbSw==}
knip@6.44.2:
resolution: {integrity: sha512-6URO7pnqtOnyvSsXfW6+8C9j1lfcicxUCysXO0RgMtHTZ96HqCXh29op8iabeQW5JeIc8dJWwzSdJVWDokeScQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -4230,8 +4228,8 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
oxc-parser@0.155.0:
resolution: {integrity: sha512-C/ReSf1JUFukK/zvk6jhcNibSssvas5+ktIzK7fbKVd3CedWe59C3QwHEOrEZj/4Rt9+xg0Jeis/Jv1F/KM1mw==}
oxc-parser@0.156.0:
resolution: {integrity: sha512-7JhjgOfZjrIFJH5TlHrpRua864Si5GvwrPr5+eIRCY2sszhKXo6pyhP0HQKOGTtfO4UowwZ72XTGba0+lZphOQ==}
engines: {node: ^20.19.0 || >=22.12.0}
oxc-resolver@11.24.2:
@@ -4399,18 +4397,16 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@17.1.13:
resolution: {integrity: sha512-ZxhOSlyKSKPtbUNPLwYMmE3w3bhoc0jaZJZ6bc7wGOT6cLbrbs//6m01HxCLtFdSjHvFpwFwoPhxxzIlHSs9cw==}
react-i18next@18.0.1:
resolution: {integrity: sha512-kPWXI0E/lrPj3+/umcKNzYZVnWmEedLiamMzDIHfbs+5ANf6yzvlZsjKou8f7kaGkfMMDLIZBejV+4aSN1CyBw==}
engines: {node: '>=22'}
peerDependencies:
i18next: '>= 26.2.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
'@types/react': '*'
i18next: '>= 27.0.1'
react: '>= 18.0.0'
typescript: ^5 || ^6 || ^7
peerDependenciesMeta:
react-dom:
optional: true
react-native:
'@types/react':
optional: true
typescript:
optional: true
@@ -5067,8 +5063,8 @@ packages:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
ws@8.22.1:
resolution: {integrity: sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==}
ws@8.22.2:
resolution: {integrity: sha512-/rEZ2ejown2N5gxVTHUu9bVVC7ttu0/Tgzf5hbTOixR3Fs7mbbXgn2DbdbcKgJOmpQMtrIUWivtYuVMnp/W/yA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -6486,65 +6482,63 @@ snapshots:
dependencies:
'@octokit/openapi-types': 29.0.1
'@oxc-parser/binding-android-arm-eabi@0.155.0':
'@oxc-parser/binding-android-arm-eabi@0.156.0':
optional: true
'@oxc-parser/binding-android-arm64@0.155.0':
'@oxc-parser/binding-android-arm64@0.156.0':
optional: true
'@oxc-parser/binding-darwin-arm64@0.155.0':
'@oxc-parser/binding-darwin-arm64@0.156.0':
optional: true
'@oxc-parser/binding-darwin-x64@0.155.0':
'@oxc-parser/binding-darwin-x64@0.156.0':
optional: true
'@oxc-parser/binding-freebsd-x64@0.155.0':
'@oxc-parser/binding-freebsd-x64@0.156.0':
optional: true
'@oxc-parser/binding-linux-arm-gnueabihf@0.155.0':
'@oxc-parser/binding-linux-arm-gnueabihf@0.156.0':
optional: true
'@oxc-parser/binding-linux-arm-musleabihf@0.155.0':
'@oxc-parser/binding-linux-arm-musleabihf@0.156.0':
optional: true
'@oxc-parser/binding-linux-arm64-gnu@0.155.0':
'@oxc-parser/binding-linux-arm64-gnu@0.156.0':
optional: true
'@oxc-parser/binding-linux-arm64-musl@0.155.0':
'@oxc-parser/binding-linux-arm64-musl@0.156.0':
optional: true
'@oxc-parser/binding-linux-ppc64-gnu@0.155.0':
'@oxc-parser/binding-linux-ppc64-gnu@0.156.0':
optional: true
'@oxc-parser/binding-linux-riscv64-gnu@0.155.0':
'@oxc-parser/binding-linux-riscv64-gnu@0.156.0':
optional: true
'@oxc-parser/binding-linux-riscv64-musl@0.155.0':
'@oxc-parser/binding-linux-riscv64-musl@0.156.0':
optional: true
'@oxc-parser/binding-linux-s390x-gnu@0.155.0':
'@oxc-parser/binding-linux-s390x-gnu@0.156.0':
optional: true
'@oxc-parser/binding-linux-x64-gnu@0.155.0':
'@oxc-parser/binding-linux-x64-gnu@0.156.0':
optional: true
'@oxc-parser/binding-linux-x64-musl@0.155.0':
'@oxc-parser/binding-linux-x64-musl@0.156.0':
optional: true
'@oxc-parser/binding-openharmony-arm64@0.155.0':
'@oxc-parser/binding-openharmony-arm64@0.156.0':
optional: true
'@oxc-parser/binding-win32-arm64-msvc@0.155.0':
'@oxc-parser/binding-win32-arm64-msvc@0.156.0':
optional: true
'@oxc-parser/binding-win32-ia32-msvc@0.155.0':
'@oxc-parser/binding-win32-ia32-msvc@0.156.0':
optional: true
'@oxc-parser/binding-win32-x64-msvc@0.155.0':
'@oxc-parser/binding-win32-x64-msvc@0.156.0':
optional: true
'@oxc-project/types@0.155.0': {}
'@oxc-project/types@0.156.0': {}
'@oxc-resolver/binding-android-arm-eabi@11.24.2':
@@ -8538,7 +8532,7 @@ snapshots:
husky@9.1.7: {}
i18next@26.4.2(typescript@6.0.3):
i18next@27.0.1(typescript@6.0.3):
optionalDependencies:
typescript: 6.0.3
@@ -8690,12 +8684,12 @@ snapshots:
dependencies:
'@keyv/serialize': 1.1.1
knip@6.44.1:
knip@6.44.2:
dependencies:
formatly: 0.11.4
get-tsconfig: 4.14.3
jiti: 2.7.0
oxc-parser: 0.155.0
oxc-parser: 0.156.0
oxc-resolver: 11.24.2
picomatch: 4.0.7
smol-toml: 1.9.1
@@ -9176,29 +9170,29 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
oxc-parser@0.155.0:
oxc-parser@0.156.0:
dependencies:
'@oxc-project/types': 0.155.0
'@oxc-project/types': 0.156.0
optionalDependencies:
'@oxc-parser/binding-android-arm-eabi': 0.155.0
'@oxc-parser/binding-android-arm64': 0.155.0
'@oxc-parser/binding-darwin-arm64': 0.155.0
'@oxc-parser/binding-darwin-x64': 0.155.0
'@oxc-parser/binding-freebsd-x64': 0.155.0
'@oxc-parser/binding-linux-arm-gnueabihf': 0.155.0
'@oxc-parser/binding-linux-arm-musleabihf': 0.155.0
'@oxc-parser/binding-linux-arm64-gnu': 0.155.0
'@oxc-parser/binding-linux-arm64-musl': 0.155.0
'@oxc-parser/binding-linux-ppc64-gnu': 0.155.0
'@oxc-parser/binding-linux-riscv64-gnu': 0.155.0
'@oxc-parser/binding-linux-riscv64-musl': 0.155.0
'@oxc-parser/binding-linux-s390x-gnu': 0.155.0
'@oxc-parser/binding-linux-x64-gnu': 0.155.0
'@oxc-parser/binding-linux-x64-musl': 0.155.0
'@oxc-parser/binding-openharmony-arm64': 0.155.0
'@oxc-parser/binding-win32-arm64-msvc': 0.155.0
'@oxc-parser/binding-win32-ia32-msvc': 0.155.0
'@oxc-parser/binding-win32-x64-msvc': 0.155.0
'@oxc-parser/binding-android-arm-eabi': 0.156.0
'@oxc-parser/binding-android-arm64': 0.156.0
'@oxc-parser/binding-darwin-arm64': 0.156.0
'@oxc-parser/binding-darwin-x64': 0.156.0
'@oxc-parser/binding-freebsd-x64': 0.156.0
'@oxc-parser/binding-linux-arm-gnueabihf': 0.156.0
'@oxc-parser/binding-linux-arm-musleabihf': 0.156.0
'@oxc-parser/binding-linux-arm64-gnu': 0.156.0
'@oxc-parser/binding-linux-arm64-musl': 0.156.0
'@oxc-parser/binding-linux-ppc64-gnu': 0.156.0
'@oxc-parser/binding-linux-riscv64-gnu': 0.156.0
'@oxc-parser/binding-linux-riscv64-musl': 0.156.0
'@oxc-parser/binding-linux-s390x-gnu': 0.156.0
'@oxc-parser/binding-linux-x64-gnu': 0.156.0
'@oxc-parser/binding-linux-x64-musl': 0.156.0
'@oxc-parser/binding-openharmony-arm64': 0.156.0
'@oxc-parser/binding-win32-arm64-msvc': 0.156.0
'@oxc-parser/binding-win32-ia32-msvc': 0.156.0
'@oxc-parser/binding-win32-x64-msvc': 0.156.0
oxc-resolver@11.24.2:
optionalDependencies:
@@ -9362,15 +9356,13 @@ snapshots:
dependencies:
react: 19.3.1
react-i18next@17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
react-i18next@18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3):
dependencies:
'@babel/runtime': 7.29.7
html-parse-stringify: 4.0.1
i18next: 26.4.2(typescript@6.0.3)
i18next: 27.0.1(typescript@6.0.3)
react: 19.3.1
use-sync-external-store: 1.7.0(react@19.3.1)
optionalDependencies:
react-dom: 19.3.1(react@19.3.1)
'@types/react': 19.3.0
typescript: 6.0.3
react-is@16.13.1: {}
@@ -10007,7 +9999,7 @@ snapshots:
deepmerge-ts: 8.0.2
https-proxy-agent: 9.1.0(supports-color@7.2.0)
undici: 8.12.3
ws: 8.22.1
ws: 8.22.2
transitivePeerDependencies:
- bufferutil
- kerberos
@@ -10075,7 +10067,7 @@ snapshots:
string-width: 7.2.0
strip-ansi: 7.2.0
ws@8.22.1: {}
ws@8.22.2: {}
y18n@5.0.8: {}
+1 -1
View File
@@ -1,7 +1,7 @@
import { createHash } from 'node:crypto'
const reviewer = 'renovate-minor-review.lock.yml'
const agents = ['copilot-swe-agent', 'copilot-swe-agent[bot]']
const agents = ['Copilot', 'copilot-swe-agent', 'copilot-swe-agent[bot]']
const checks = {
'.github/workflows/frontend-check.yml': [
'Run Prettier check',
+5 -5
View File
@@ -58,13 +58,13 @@ smartstring = { workspace = true, features = ["serde"] }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
open = "5.4.5"
rfd = { version = "0.17.2", default-features = false, features = ["common-controls-v6"] }
rfd = { version = "0.18.0", default-features = false, features = ["common-controls-v6", "xdg-portal"] }
dirs = "7.0.0"
dunce = "1.0.5"
nanoid = "0.5"
chrono = { workspace = true }
boa_engine = "0.22.0"
http-body-util = "0.1.4"
http-body-util = "0.1.5"
percent-encoding = "2.3.2"
reqwest = { workspace = true }
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [
@@ -77,8 +77,8 @@ tauri-plugin-dialog = "2.8.4"
tauri-plugin-fs = "2.7.1"
tauri-plugin-deep-link = "2.6.3"
tauri-plugin-window-state = "2.5.0"
zip = "9.3.0"
flate2 = "1.1.10"
zip = "9.5.0"
flate2 = "1.1.11"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
base64 = "0.23.1"
@@ -97,7 +97,7 @@ async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
"client",
] }
arc-swap = "1.9.3"
arc-swap = "1.9.5"
tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.11"
+3 -57
View File
@@ -5,16 +5,10 @@ use crate::{
core::{
handle::Handle,
proxy_view::{GroupScope, ProxyViewBuilder, ProxyViewInput, ProxyViewV1},
tray::Tray,
},
process::AsyncHandler,
};
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::Mapping;
use std::{
collections::{BTreeMap, HashSet},
sync::atomic::{AtomicBool, Ordering},
};
use std::collections::{BTreeMap, HashSet};
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
#[tauri::command]
@@ -27,15 +21,12 @@ pub async fn record_selected_node(group_name: String, node: String) -> CmdResult
#[tauri::command]
#[specta::specta]
pub async fn forget_selected_node(group_name: String) -> CmdResult<()> {
crate::config::profiles::forget_selected_node(&group_name)
pub async fn switch_proxy_node(group_name: String, proxy_name: String, unfix: bool) -> CmdResult<()> {
crate::feat::switch_proxy_node(&group_name, &proxy_name, unfix)
.await
.stringify_err()
}
static TRAY_SYNC_RUNNING: AtomicBool = AtomicBool::new(false);
static TRAY_SYNC_PENDING: AtomicBool = AtomicBool::new(false);
fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
let mut seen = HashSet::new();
@@ -124,48 +115,3 @@ pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
providers: providers.ok(),
}))
}
#[tauri::command]
#[specta::specta]
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
if TRAY_SYNC_RUNNING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
AsyncHandler::spawn(move || async move {
run_tray_sync_loop().await;
});
} else {
TRAY_SYNC_PENDING.store(true, Ordering::Release);
}
Ok(())
}
async fn run_tray_sync_loop() {
// Permanent watcher: keeps syncing until a pass finds no pending sync request.
loop {
match Tray::global().update_menu().await {
Ok(_) => {
logging!(debug, Type::Cmd, "Tray proxy selection synced successfully");
}
Err(e) => {
logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e:#}");
}
}
if !TRAY_SYNC_PENDING.swap(false, Ordering::AcqRel) {
TRAY_SYNC_RUNNING.store(false, Ordering::Release);
if TRAY_SYNC_PENDING.swap(false, Ordering::AcqRel)
&& TRAY_SYNC_RUNNING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
continue;
}
break;
}
}
}
+10 -6
View File
@@ -280,25 +280,29 @@ impl Config {
}
/// Commits drafts during exit/restart/shutdown so user changes are not lost.
/// A config with nothing staged keeps its file, so its timestamps stay put.
pub async fn apply_all_and_save_file() {
logging!(debug, Type::Config, "save all draft data");
let save_clash_task = AsyncHandler::spawn(|| async {
let clash = Self::clash().await;
clash.apply();
logging_error!(Type::Config, clash.data_arc().save_config().await);
if clash.apply() {
logging_error!(Type::Config, clash.data_arc().save_config().await);
}
});
let save_verge_task = AsyncHandler::spawn(|| async {
let verge = Self::verge().await;
verge.apply();
logging_error!(Type::Config, verge.data_arc().save_file().await);
if verge.apply() {
logging_error!(Type::Config, verge.data_arc().save_file().await);
}
});
let save_profiles_task = AsyncHandler::spawn(|| async {
let _profile_write = super::profiles::PROFILE_WRITE_LOCK.lock().await;
let profiles = Self::profiles().await;
profiles.apply();
logging_error!(Type::Config, profiles.data_arc().save_file().await);
if profiles.apply() {
logging_error!(Type::Config, profiles.data_arc().save_file().await);
}
});
let _ = tokio::join!(save_clash_task, save_verge_task, save_profiles_task);
+3 -1
View File
@@ -1000,7 +1000,9 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
Some((g, p)) => (g, p),
None => return,
};
feat::switch_proxy_node(group_name, proxy_name).await;
if let Err(err) = feat::switch_proxy_node(group_name, proxy_name, false).await {
logging!(error, Type::Tray, "Failed to switch proxy: {err:#}");
}
}
_ => {
logging!(debug, Type::Tray, "Unhandled tray menu event: {:?}", event.id);
+73 -24
View File
@@ -4,6 +4,7 @@ use crate::{
cmd,
config::{Config, PrfItem, PrfOption, profiles::profiles_update_item_safe},
core::{CoreManager, handle, tray, validate::ValidationOutcome},
process::AsyncHandler,
utils::help::{mask_err, mask_url},
};
use anyhow::{Result, bail};
@@ -21,37 +22,85 @@ pub async fn toggle_proxy_profile(profile_index: String) {
}
}
/// Tell the profile which node this group is on now.
///
/// The core is already switched by the time this runs, so failing to record is not a reason to
/// report the switch as failed — but it is worth a log line, because what the profile holds is
/// what gets re-applied the next time a core starts.
async fn record_switched_node(group_name: &str, proxy_name: &str) {
if let Err(error) = crate::config::profiles::record_selected_node(group_name, proxy_name).await {
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str, unfix: bool) -> Result<()> {
let mihomo = handle::Handle::mihomo();
let previous_proxy = if Config::verge().await.data_arc().auto_close_connection.unwrap_or(false) {
match mihomo.get_proxy_by_name(group_name).await {
Ok(group) => group.now.filter(|name| !name.is_empty()),
Err(err) => {
logging!(
warn,
Type::Core,
"Failed to read previous proxy selection for {group_name}: {err}"
);
None
}
}
} else {
None
};
if unfix {
mihomo.unfixed_proxy(group_name).await?;
} else {
mihomo.select_node_for_group(group_name, proxy_name).await?;
}
let save_result = if unfix {
crate::config::profiles::forget_selected_node(group_name).await
} else {
crate::config::profiles::record_selected_node(group_name, proxy_name).await
};
// The core selection is already applied; persistence failure must not report it as failed.
if let Err(error) = save_result {
logging!(
warn,
Type::Config,
"切换代理成功但未能记录到配置: {} -> {}, 错误: {error:#}",
group_name,
proxy_name
"Failed to persist proxy selection for {group_name}: {error:#}"
);
}
}
announce(Refresh::Proxies);
pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
match handle::Handle::mihomo()
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
record_switched_node(group_name, proxy_name).await;
announce(Refresh::Proxies);
let _ = tray::Tray::global().update_menu().await;
}
Err(err) => {
logging!(error, Type::Tray, "切换代理失败: {err:?}");
}
if let Some(previous_proxy) = previous_proxy {
AsyncHandler::spawn(move || async move {
let connections = match mihomo.get_connections().await {
Ok(connections) => connections,
Err(err) => {
logging!(
warn,
Type::Core,
"Failed to read connections after switching proxy: {err}"
);
return;
}
};
futures::future::join_all(
connections
.connections
.unwrap_or_default()
.into_iter()
.filter(|connection| connection.chains.contains(&previous_proxy))
.map(|connection| async move {
if let Err(err) = mihomo.close_connection(&connection.id).await {
logging!(
warn,
Type::Core,
"Failed to close connection {} after switching proxy: {err}",
connection.id
);
}
}),
)
.await;
});
}
if let Err(err) = tray::Tray::global().update_menu().await {
logging!(
warn,
Type::Tray,
"Failed to refresh tray after switching proxy: {err:#}"
);
}
Ok(())
}
async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
+1 -2
View File
@@ -45,9 +45,8 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::sync_tray_proxy_selection,
cmd::record_selected_node,
cmd::forget_selected_node,
cmd::switch_proxy_node,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::set_dns_override,
+1 -5
View File
@@ -606,9 +606,6 @@ export const CurrentProxyCard = () => {
)
const { select } = useProxySelection({
onSuccess: () => {
refreshProxy()
},
onError: (error) => {
console.error('代理切换失败', error)
refreshProxy()
@@ -755,10 +752,9 @@ export const CurrentProxyCard = () => {
if (!selectedGroup || !option || !isInteractableMember(option.member)) {
return
}
const previousProxy = selectedGroup.now
const nextName = option.member.ref.name
// The profile selection is the durable source across core restarts and run modes.
select(selectedGroup.name, nextName, previousProxy!, selectedGroup.fixed!)
select(selectedGroup.name, nextName, selectedGroup.fixed!)
},
[isDirectMode, select, selectedGroup, unsortedProxyOptions],
)
+2 -5
View File
@@ -470,9 +470,6 @@ function NormalProxyGroups(props: { mode: string }) {
}, [handleScroll])
const { select } = useProxySelection({
onSuccess: () => {
onProxies()
},
onError: (error) => {
console.error('代理切换失败', error)
onProxies()
@@ -487,12 +484,12 @@ function NormalProxyGroups(props: { mode: string }) {
if (group.name === globalGroupName) {
pendingRevealRef.current =
member.kind === 'group' ? member.ref.name : null
// The selection refetches the view, so skip onHeadState's refetch.
// The backend selection event refreshes the view.
if (member.kind === 'group') {
setHeadState(member.ref.name, { open: true })
}
}
select(group.name, member.ref.name, group.now!, group.fixed!)
select(group.name, member.ref.name, group.fixed!)
},
[globalGroupName, select, setHeadState],
)
+7 -1
View File
@@ -209,7 +209,13 @@ export const useRenderList = (
const renderGroups = useMemo(() => {
if (!proxyView) return []
if (mode === 'rule') return proxyView.groups
// A profile whose only group is GLOBAL (mihomo then skips its built-in one)
// or that has no proxy-groups has nothing else to show in rule mode.
if (mode === 'rule') {
return proxyView.groups.length || !proxyView.global
? proxyView.groups
: [proxyView.global]
}
const { global } = proxyView
if (!global) return []
const current = findCurrentGroupMember(proxyView, global)?.member
+13 -29
View File
@@ -1,51 +1,35 @@
import { selectNodeForGroup } from 'tauri-plugin-mihomo-api'
import { expect, it, vi } from 'vitest'
import { switchProxyNode } from '@/services/cmds'
import { useProxySelection } from './use-proxy-selection'
const record = vi.hoisted(() => vi.fn())
vi.mock('react', () => ({
useCallback: (callback: unknown) => callback,
useRef: (current: unknown) => ({ current }),
}))
vi.mock('@/hooks/use-record-selection', () => ({
useRecordSelection: () => record,
useForgetSelection: () => vi.fn(),
}))
vi.mock('@/hooks/use-verge', () => ({ useVerge: () => ({ verge: {} }) }))
vi.mock('tauri-plugin-mihomo-api', () => ({
selectNodeForGroup: vi.fn(),
unfixedProxy: vi.fn(),
}))
vi.mock('@/services/cmds', () => ({ syncTrayProxySelection: vi.fn() }))
vi.mock('@/services/cmds', () => ({ switchProxyNode: vi.fn() }))
vi.mock('@/services/notice-service', () => ({ showNotice: { error: vi.fn() } }))
vi.mock('@/services/query-client', () => ({ revalidateQueries: vi.fn() }))
it('applies and records independent group selections while another group is pending', async () => {
it('requests independent group selections while another group is pending', async () => {
let finishFirst!: () => void
vi.mocked(selectNodeForGroup)
vi.mocked(switchProxyNode)
.mockImplementationOnce(
() =>
new Promise((resolve) => {
finishFirst = resolve
finishFirst = () => resolve(null)
}),
)
.mockResolvedValueOnce(undefined)
const firstSuccess = vi.fn()
const secondSuccess = vi.fn()
const first = useProxySelection({ onSuccess: firstSuccess })
const second = useProxySelection({ onSuccess: secondSuccess })
.mockResolvedValueOnce(null)
const first = useProxySelection()
const second = useProxySelection()
first.select('Group A', 'Node A')
second.select('Group B', 'Node B')
second.select('Group B', 'Node B', 'Node B')
await vi.waitFor(() =>
expect(record).toHaveBeenCalledWith('Group B', 'Node B'),
expect(switchProxyNode).toHaveBeenCalledWith('Group B', 'Node B', true),
)
expect(selectNodeForGroup).toHaveBeenCalledTimes(2)
expect(firstSuccess).not.toHaveBeenCalled()
expect(secondSuccess).toHaveBeenCalledOnce()
expect(switchProxyNode).toHaveBeenCalledWith('Group A', 'Node A', false)
expect(switchProxyNode).toHaveBeenCalledTimes(2)
finishFirst()
await vi.waitFor(() =>
expect(record).toHaveBeenCalledWith('Group A', 'Node A'),
)
expect(firstSuccess).toHaveBeenCalledOnce()
})
+8 -96
View File
@@ -1,132 +1,44 @@
import { useCallback } from 'react'
import {
closeConnection,
getConnections,
selectNodeForGroup,
unfixedProxy,
} from 'tauri-plugin-mihomo-api'
import {
useForgetSelection,
useRecordSelection,
} from '@/hooks/use-record-selection'
import { useVerge } from '@/hooks/use-verge'
import { syncTrayProxySelection } from '@/services/cmds'
import { switchProxyNode } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { debugLog } from '@/utils/debug'
// 缓存连接清理
const cleanupConnections = async (previousProxy: string) => {
try {
const { connections } = await getConnections()
const cleanupPromises = (connections ?? [])
.filter((conn) => conn.chains.includes(previousProxy))
.map((conn) => closeConnection(conn.id))
if (cleanupPromises.length > 0) {
await Promise.allSettled(cleanupPromises)
debugLog(`[ProxySelection] 清理了 ${cleanupPromises.length} 个连接`)
}
} catch (error) {
console.warn('[ProxySelection] 连接清理失败:', error)
}
}
interface ProxySelectionOptions {
onSuccess?: () => void
onError?: (error: any) => void
}
interface ProxyChangeRequest {
groupName: string
proxyName: string
previousProxy?: string
fixed?: string
}
// 代理选择 Hook
export const useProxySelection = (options: ProxySelectionOptions = {}) => {
const recordSelection = useRecordSelection()
const forgetSelection = useForgetSelection()
const { verge } = useVerge()
const { onSuccess, onError } = options
const autoCloseConnection = verge?.auto_close_connection ?? false
// 切换节点
const syncTraySelection = useCallback(() => {
mutate(() => syncTrayProxySelection(), {
id: 'sync-tray-selection',
errorNotice: false,
}).catch((error) => {
console.error('[ProxySelection] 托盘状态同步失败:', error)
})
}, [])
const { onError } = options
const executeChange = useCallback(
async (request: ProxyChangeRequest) => {
const { groupName, proxyName, previousProxy, fixed } = request
const isFixedProxy = fixed === proxyName
if (isFixedProxy) {
debugLog(`[ProxySelection] 代理取消固定: ${groupName} -> ${proxyName}`)
} else {
debugLog(`[ProxySelection] 代理切换: ${groupName} -> ${proxyName}`)
}
async ({ groupName, proxyName, fixed }: ProxyChangeRequest) => {
try {
const result = await mutate(
() =>
isFixedProxy
? unfixedProxy(groupName)
: selectNodeForGroup(groupName, proxyName),
await mutate(
() => switchProxyNode(groupName, proxyName, fixed === proxyName),
{ id: `select-proxy-node:${groupName}`, errorNotice: false },
)
if (!result.ok) return
onSuccess?.()
syncTraySelection()
if (isFixedProxy) {
await forgetSelection(groupName)
debugLog(`[ProxySelection] 代理和状态同步完成: ${groupName}`)
} else {
await recordSelection(groupName, proxyName)
debugLog(
`[ProxySelection] 代理和状态同步完成: ${groupName} -> ${proxyName}`,
)
}
if (autoCloseConnection && previousProxy) {
void cleanupConnections(previousProxy)
}
} catch (error) {
console.error(
`[ProxySelection] 代理切换失败: ${groupName} -> ${proxyName}`,
`[ProxySelection] Failed to switch proxy: ${groupName} -> ${proxyName}`,
error,
)
onError?.(error)
}
},
[
autoCloseConnection,
forgetSelection,
onError,
onSuccess,
recordSelection,
syncTraySelection,
],
[onError],
)
const select = useCallback(
(
groupName: string,
proxyName: string,
previousProxy?: string,
fixed?: string,
) => {
(groupName: string, proxyName: string, fixed?: string) => {
void executeChange({
groupName,
proxyName,
previousProxy,
fixed,
})
},
+1 -14
View File
@@ -1,6 +1,6 @@
import { useCallback } from 'react'
import { forgetSelectedNode, recordSelectedNode } from '@/services/cmds'
import { recordSelectedNode } from '@/services/cmds'
import { mutate } from '@/services/mutate'
/**
@@ -19,16 +19,3 @@ export const useRecordSelection = () => {
}
}, [])
}
export const useForgetSelection = () => {
return useCallback(async (groupName: string) => {
try {
await mutate(() => forgetSelectedNode(groupName), {
id: `persist-selected-node:${groupName}`,
errorNotice: false,
})
} catch (error) {
console.error('[Selection] 清除代理选择失败:', error)
}
}, [])
}
+42 -5
View File
@@ -71,6 +71,10 @@ const ORDER_OPTIONS = [
type OrderKey = (typeof ORDER_OPTIONS)[number]['id']
type NetworkFilter = 'all' | 'tcp' | 'udp'
let savedNetworkFilter: NetworkFilter = 'all'
const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
(acc, option) => {
acc[option.id] = option.fn
@@ -86,6 +90,9 @@ const ConnectionsPage = () => {
const { searchState, match, onSearch } = usePageSearch('connections')
const hasSearch = searchState.text.length > 0
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>('default')
const [networkFilter, setNetworkFilter] =
useState<NetworkFilter>(savedNetworkFilter)
const hasFilter = networkFilter !== 'all'
const [connectionsType, setConnectionsType] = useState<'active' | 'closed'>(
'active',
)
@@ -112,18 +119,30 @@ const ConnectionsPage = () => {
const filterConn = useMemo(() => {
const orderFunc = orderFunctionMap[curOrderOpt]
if (isTableLayout && !hasSearch) return selectedConnections
if (!hasSearch) return orderFunc([...selectedConnections])
if (isTableLayout && !hasSearch && !hasFilter) return selectedConnections
if (!hasSearch && !hasFilter) return orderFunc([...selectedConnections])
const matchConns = selectedConnections.filter((conn) => {
const { host, destinationIP, process } = conn.metadata
const { host, destinationIP, process, network } = conn.metadata
if (networkFilter !== 'all' && network !== networkFilter) return false
return (
match(host || '') || match(destinationIP || '') || match(process || '')
!hasSearch ||
match(host || '') ||
match(destinationIP || '') ||
match(process || '')
)
})
return orderFunc ? orderFunc(matchConns) : matchConns
}, [selectedConnections, isTableLayout, hasSearch, match, curOrderOpt])
}, [
selectedConnections,
isTableLayout,
hasSearch,
hasFilter,
networkFilter,
match,
curOrderOpt,
])
const displayRows = useConnectionRowViews(
isTableLayout ? EMPTY_CONNECTIONS : filterConn,
@@ -255,6 +274,24 @@ const ConnectionsPage = () => {
))}
</BaseStyledSelect>
)}
<BaseStyledSelect
style={{ width: 80 }}
value={networkFilter}
onChange={(e) => {
savedNetworkFilter = e.target.value as NetworkFilter
setNetworkFilter(savedNetworkFilter)
}}
>
<MenuItem value="all">
<span style={{ fontSize: 14 }}>All</span>
</MenuItem>
<MenuItem value="tcp">
<span style={{ fontSize: 14 }}>TCP</span>
</MenuItem>
<MenuItem value="udp">
<span style={{ fontSize: 14 }}>UDP</span>
</MenuItem>
</BaseStyledSelect>
<Box
sx={{
flex: 1,
+2 -4
View File
@@ -78,13 +78,11 @@ export const commands = {
}),
invokeUwpTool: () => __TAURI_INVOKE<null>('invoke_uwp_tool'),
copyClashEnv: () => __TAURI_INVOKE<null>('copy_clash_env'),
syncTrayProxySelection: () =>
__TAURI_INVOKE<null>('sync_tray_proxy_selection'),
/** Merges one selection pair into fresh backend state to avoid stale-list overwrites. */
recordSelectedNode: (groupName: string, node: string) =>
__TAURI_INVOKE<null>('record_selected_node', { groupName, node }),
forgetSelectedNode: (groupName: string) =>
__TAURI_INVOKE<null>('forget_selected_node', { groupName }),
switchProxyNode: (groupName: string, proxyName: string, unfix: boolean) =>
__TAURI_INVOKE<null>('switch_proxy_node', { groupName, proxyName, unfix }),
saveDnsConfig: (dnsConfig: YamlMapping) =>
__TAURI_INVOKE<null>('save_dns_config', { dnsConfig }),
applyDnsConfig: (apply: boolean) =>
+2 -12
View File
@@ -29,14 +29,11 @@ export interface BusHandlerDeps {
/**
* Pure per-event handler table for the bus. Handlers may trigger documented
* side effects (SWR revalidation, notice toasts) but write store state only
* through dispatched pure actions. Created exactly once per bus mount; the
* throttles below live in the returned closure, mirroring the previous
* scattered listeners.
* through dispatched pure actions. Created exactly once per bus mount.
*/
export const createEventBus = (deps: BusHandlerDeps) => {
let lastProfileId: string | null = null
let lastProfileChangeTime = 0
let lastProxyRefreshTime = 0
const refreshThrottle = 800
const handleProfileChanged = (newProfileId: string) => {
@@ -52,13 +49,6 @@ export const createEventBus = (deps: BusHandlerDeps) => {
deps.revalidateKeys(['getProfiles'])
}
const handleRefreshProxyConfig = () => {
const now = Date.now()
if (now - lastProxyRefreshTime <= refreshThrottle) return
lastProxyRefreshTime = now
deps.revalidateKeys(['getProxyView'])
}
const handlers: BusHandlers = {
'verge://refresh-clash-config': () =>
deps.revalidateKeys([
@@ -74,7 +64,7 @@ export const createEventBus = (deps: BusHandlerDeps) => {
'verge://refresh-verge-config': () =>
deps.revalidateKeys(['getVergeConfig', 'getSystemProxy', 'getAutoProxy']),
'verge://refresh-profiles': () => deps.revalidateKeys(['getProfiles']),
'verge://refresh-proxy-config': handleRefreshProxyConfig,
'verge://refresh-proxy-config': () => deps.revalidateKeys(['getProxyView']),
'verge://notice-message': (payload) => deps.handleNotice(payload),
'profile-changed': handleProfileChanged,
'verge://timer-updated': (uid) =>
+6 -6
View File
@@ -152,17 +152,17 @@ export async function patchClashMode(payload: string) {
return commands.patchClashMode(payload)
}
export async function syncTrayProxySelection() {
return commands.syncTrayProxySelection()
}
/** Sends one selection pair so the backend can merge against current profile state. */
export async function recordSelectedNode(groupName: string, node: string) {
return commands.recordSelectedNode(groupName, node)
}
export async function forgetSelectedNode(groupName: string) {
return commands.forgetSelectedNode(groupName)
export async function switchProxyNode(
groupName: string,
proxyName: string,
unfix: boolean,
) {
return commands.switchProxyNode(groupName, proxyName, unfix)
}
export async function getProxyView(): Promise<ProxyViewV1> {