From 782f3fa54df900a7721f544f6c501f4281d8fe94 Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Wed, 24 Jun 2026 16:39:36 +0800 Subject: [PATCH 1/9] Drop unused JDK 8 from the dev shell JAVA8_HOME was only for ad-hoc Java 8 downgrade checks and is not needed to build: the core targets Java 8 bytecode via the JDK 21 compiler's --release flag, and the dev mvn exec:exec fork cannot run on Java 8 anyway (--add-opens is Java 9+). Remove pkgs.jdk8 from both devShells and the justfile note. --- flake.nix | 2 -- justfile | 1 - 2 files changed, 3 deletions(-) diff --git a/flake.nix b/flake.nix index 2404d9c5..90a10901 100644 --- a/flake.nix +++ b/flake.nix @@ -153,7 +153,6 @@ UV_PYTHON = python.interpreter; # JDK configuration JAVA_HOME = pkgs.jdk21.home; - JAVA8_HOME = pkgs.jdk8.home; } // lib.optionalAttrs pkgs.stdenv.isLinux { # Python libraries often load native shared objects using dlopen(3). @@ -206,7 +205,6 @@ # JDK configuration JAVA_HOME = pkgs.jdk21.home; - JAVA8_HOME = pkgs.jdk8.home; }; shellHook = '' diff --git a/justfile b/justfile index 1c5529f0..e76f032a 100644 --- a/justfile +++ b/justfile @@ -2,7 +2,6 @@ # # JDK configuration (set via flake.nix env vars): # JAVA_HOME -> JDK 21 (builds the single JAR: core -> Java 8 bytecode, MCP -> Java 17) -# JAVA8_HOME -> JDK 8 (only for ad-hoc Java 8 downgrade checks; not required to build) default: @just --list From 51814b7c95bec61b1107ecb9be2411bc079a7132 Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Sat, 20 Jun 2026 12:16:56 +0800 Subject: [PATCH 2/9] Bump vite to v8 in /web - vite ^6.4.3 -> ^8.0.12 (resolves to 8.0.16; now Rolldown + Oxc + Lightning CSS) - @tailwindcss/vite ^4.2.1 -> ^4.2.2 (required: Vite 8 support landed in 4.2.2) - tailwindcss ^4.2.1 -> ^4.2.2 (lockstep with the vite plugin) - @vitejs/plugin-vue ^5.2.4 -> ^6.0.7 (current major; 5.x also declares vite ^8) No vite.config.ts / tsconfig / Node changes needed (Node already >=22.12 in the nix shell, CI, and Dockerfile). vue-tsc, vite build, and prettier all pass. --- web/package.json | 2 +- web/pnpm-lock.yaml | 942 ++++++++++++++++++--------------------------- 2 files changed, 381 insertions(+), 563 deletions(-) diff --git a/web/package.json b/web/package.json index 06532830..13109875 100644 --- a/web/package.json +++ b/web/package.json @@ -37,7 +37,7 @@ "prettier-plugin-organize-imports": "^4.3.0", "typescript": "~5.9.3", "unplugin-vue-components": "^32.1.0", - "vite": "^6.4.3", + "vite": "^8.0.12", "vue-tsc": "^3.3.5" }, "prettier": { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 128607e0..8e719b7a 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -16,16 +16,16 @@ importers: version: 2.0.3 '@tabler/icons-vue': specifier: ^3.44.0 - version: 3.44.0(vue@3.5.38(typescript@5.9.3)) + version: 3.44.0(vue@3.5.39(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.20 version: 0.5.20(tailwindcss@4.3.1) '@tailwindcss/vite': specifier: ^4.3.1 - version: 4.3.1(vite@6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + version: 4.3.1(vite@8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0)) '@vueuse/core': specifier: ^14.3.0 - version: 14.3.0(vue@3.5.38(typescript@5.9.3)) + version: 14.3.0(vue@3.5.39(typescript@5.9.3)) date-fns: specifier: ^4.4.0 version: 4.4.0 @@ -43,10 +43,10 @@ importers: version: 2.2.4(marked@18.0.5) pinia: specifier: ^3.0.4 - version: 3.0.4(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3)) + version: 3.0.4(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)) primevue: specifier: ^4.5.5 - version: 4.5.5(vue@3.5.38(typescript@5.9.3)) + version: 4.5.5(vue@3.5.39(typescript@5.9.3)) tailwind-merge: specifier: ^3.6.0 version: 3.6.0 @@ -61,26 +61,26 @@ importers: version: 14.0.0 vue: specifier: ^3.5.38 - version: 3.5.38(typescript@5.9.3) + version: 3.5.39(typescript@5.9.3) vue-router: specifier: ^5.1.0 - version: 5.1.0(@vue/compiler-sfc@3.5.38)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3)))(vite@6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3)) + version: 5.1.0(@vue/compiler-sfc@3.5.39)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)))(vite@8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@5.9.3)) devDependencies: '@primevue/auto-import-resolver': specifier: ^4.5.5 version: 4.5.5 '@types/node': specifier: ^26.0.0 - version: 26.0.0 + version: 26.0.1 '@types/uuid': specifier: ^11.0.0 version: 11.0.0 '@vitejs/plugin-vue': specifier: ^6.0.7 - version: 6.0.7(vite@6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3)) + version: 6.0.7(vite@8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@5.9.3)) '@vue/tsconfig': specifier: ^0.9.1 - version: 0.9.1(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3)) + version: 0.9.1(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)) prettier-plugin-organize-imports: specifier: ^4.3.0 version: 4.3.0(prettier@3.6.0)(typescript@5.9.3)(vue-tsc@3.3.5(typescript@5.9.3)) @@ -89,10 +89,10 @@ importers: version: 5.9.3 unplugin-vue-components: specifier: ^32.1.0 - version: 32.1.0(vue@3.5.38(typescript@5.9.3)) + version: 32.1.0(vue@3.5.39(typescript@5.9.3)) vite: - specifier: ^6.4.3 - version: 6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + specifier: ^8.0.12 + version: 8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0) vue-tsc: specifier: ^3.3.5 version: 3.3.5(typescript@5.9.3) @@ -137,161 +137,14 @@ packages: resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==} engines: {node: ^22.18.0 || >=24.11.0} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -309,6 +162,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@primeuix/styled@0.7.4': resolution: {integrity: sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==} engines: {node: '>=12.11.0'} @@ -341,133 +203,103 @@ packages: resolution: {integrity: sha512-ZYLu9m3Nm5BmL0woqCJX84EZfLBepJn+T19v5oj3PlsMpUVNnDAsm2jgYdT6/b3RkdOuccxk4Pg/0EvlATOAhA==} engines: {node: '>=12.11.0'} - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@rollup/rollup-android-arm-eabi@4.62.0': - resolution: {integrity: sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.62.0': - resolution: {integrity: sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==} + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.62.0': - resolution: {integrity: sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==} + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.62.0': - resolution: {integrity: sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==} + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.62.0': - resolution: {integrity: sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.62.0': - resolution: {integrity: sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==} + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.62.0': - resolution: {integrity: sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.62.0': - resolution: {integrity: sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.62.0': - resolution: {integrity: sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==} + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.62.0': - resolution: {integrity: sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==} + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.62.0': - resolution: {integrity: sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.62.0': - resolution: {integrity: sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.62.0': - resolution: {integrity: sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.62.0': - resolution: {integrity: sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==} + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.62.0': - resolution: {integrity: sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.62.0': - resolution: {integrity: sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.62.0': - resolution: {integrity: sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==} + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.62.0': - resolution: {integrity: sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==} + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.62.0': - resolution: {integrity: sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==} + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] - '@rollup/rollup-openbsd-x64@4.62.0': - resolution: {integrity: sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.62.0': - resolution: {integrity: sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==} + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.62.0': - resolution: {integrity: sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==} - cpu: [arm64] - os: [win32] + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] - '@rollup/rollup-win32-ia32-msvc@4.62.0': - resolution: {integrity: sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==} - cpu: [ia32] + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.62.0': - resolution: {integrity: sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==} + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.62.0': - resolution: {integrity: sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==} - cpu: [x64] - os: [win32] + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@tabler/icons-vue@3.44.0': resolution: {integrity: sha512-mABxdhq3SWo2ZI77w/t0reiOGNim/SEDSlfMT5PeiWA3cZwnZoQUYRiq/X6SgeTaA7LzCTX0IuvQWVf4RWOvsg==} @@ -515,24 +347,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.1': resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.1': resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.1': resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.1': resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} @@ -572,14 +408,14 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} - '@types/node@26.0.0': - resolution: {integrity: sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==} + '@types/node@26.0.1': + resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==} '@types/uuid@11.0.0': resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==} @@ -613,56 +449,80 @@ packages: vue: optional: true + '@vue/compiler-core@3.5.30': + resolution: {integrity: sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==} + '@vue/compiler-core@3.5.38': resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==} + '@vue/compiler-core@3.5.39': + resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==} + + '@vue/compiler-dom@3.5.30': + resolution: {integrity: sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==} + '@vue/compiler-dom@3.5.38': resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==} - '@vue/compiler-sfc@3.5.38': - resolution: {integrity: sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==} + '@vue/compiler-dom@3.5.39': + resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==} + + '@vue/compiler-sfc@3.5.30': + resolution: {integrity: sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==} - '@vue/compiler-ssr@3.5.38': - resolution: {integrity: sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==} + '@vue/compiler-sfc@3.5.39': + resolution: {integrity: sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==} + + '@vue/compiler-ssr@3.5.30': + resolution: {integrity: sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==} + + '@vue/compiler-ssr@3.5.39': + resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==} '@vue/devtools-api@7.7.9': resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} - '@vue/devtools-api@8.1.3': - resolution: {integrity: sha512-73NMCvxXh8Hyozc/jiwqTFWVcCMyi11U1zmrq4DoukQJnuo8JHt6FsNu4HdeUDa8SpIp5vb7Q22GWgIq0efsXg==} + '@vue/devtools-api@8.1.5': + resolution: {integrity: sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==} '@vue/devtools-kit@7.7.9': resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} - '@vue/devtools-kit@8.1.3': - resolution: {integrity: sha512-cRn7GXiCQkMYU2Z3h3pM4YO/ndbx9FY1yLDAqIqPLcmIq4H6zAOJHein6tvZU3AfPwgrodqLiPBEF+YQaS8AxA==} + '@vue/devtools-kit@8.1.5': + resolution: {integrity: sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==} '@vue/devtools-shared@7.7.9': resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} - '@vue/devtools-shared@8.1.3': - resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==} + '@vue/devtools-shared@8.1.5': + resolution: {integrity: sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==} '@vue/language-core@3.3.5': resolution: {integrity: sha512-UkKu5nhX89fg4VhlG/FOeI10G3cj/7radKT/cy9BT4Q9qJmJlSTAc/dP63Xqs29aypN4f39xUV6PsLNk/dcD6g==} - '@vue/reactivity@3.5.38': - resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==} + '@vue/reactivity@3.5.39': + resolution: {integrity: sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==} - '@vue/runtime-core@3.5.38': - resolution: {integrity: sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==} + '@vue/runtime-core@3.5.39': + resolution: {integrity: sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==} - '@vue/runtime-dom@3.5.38': - resolution: {integrity: sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==} + '@vue/runtime-dom@3.5.39': + resolution: {integrity: sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==} - '@vue/server-renderer@3.5.38': - resolution: {integrity: sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==} + '@vue/server-renderer@3.5.39': + resolution: {integrity: sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==} peerDependencies: - vue: 3.5.38 + vue: 3.5.39 + + '@vue/shared@3.5.30': + resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==} '@vue/shared@3.5.38': resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==} + '@vue/shared@3.5.39': + resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==} + '@vue/tsconfig@0.9.1': resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==} peerDependencies: @@ -687,8 +547,8 @@ packages: peerDependencies: vue: ^3.5.0 - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true @@ -746,11 +606,6 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -834,24 +689,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -869,6 +728,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + local-pkg@1.2.1: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} @@ -893,6 +756,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -939,8 +805,8 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.3.1: - resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} @@ -979,9 +845,9 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rollup@4.62.0: - resolution: {integrity: sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true scule@1.3.0: @@ -1018,13 +884,16 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -1054,31 +923,34 @@ packages: resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true - vite@6.4.3: - resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -1121,8 +993,8 @@ packages: peerDependencies: typescript: '>=5.0.0' - vue@3.5.38: - resolution: {integrity: sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==} + vue@3.5.39: + resolution: {integrity: sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -1174,82 +1046,20 @@ snapshots: '@babel/helper-string-parser': 8.0.0 '@babel/helper-validator-identifier': 8.0.2 - '@esbuild/aix-ppc64@0.25.12': - optional: true - - '@esbuild/android-arm64@0.25.12': - optional: true - - '@esbuild/android-arm@0.25.12': - optional: true - - '@esbuild/android-x64@0.25.12': - optional: true - - '@esbuild/darwin-arm64@0.25.12': - optional: true - - '@esbuild/darwin-x64@0.25.12': - optional: true - - '@esbuild/freebsd-arm64@0.25.12': - optional: true - - '@esbuild/freebsd-x64@0.25.12': - optional: true - - '@esbuild/linux-arm64@0.25.12': - optional: true - - '@esbuild/linux-arm@0.25.12': - optional: true - - '@esbuild/linux-ia32@0.25.12': - optional: true - - '@esbuild/linux-loong64@0.25.12': - optional: true - - '@esbuild/linux-mips64el@0.25.12': - optional: true - - '@esbuild/linux-ppc64@0.25.12': - optional: true - - '@esbuild/linux-riscv64@0.25.12': - optional: true - - '@esbuild/linux-s390x@0.25.12': - optional: true - - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.25.12': + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 optional: true - '@esbuild/win32-ia32@0.25.12': + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/win32-x64@0.25.12': + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 optional: true '@jridgewell/gen-mapping@0.3.13': @@ -1271,6 +1081,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.133.0': {} + '@primeuix/styled@0.7.4': dependencies: '@primeuix/utils': 0.6.4 @@ -1289,102 +1108,76 @@ snapshots: dependencies: '@primevue/metadata': 4.5.5 - '@primevue/core@4.5.5(vue@3.5.38(typescript@5.9.3))': + '@primevue/core@4.5.5(vue@3.5.39(typescript@5.9.3))': dependencies: '@primeuix/styled': 0.7.4 '@primeuix/utils': 0.6.4 - vue: 3.5.38(typescript@5.9.3) + vue: 3.5.39(typescript@5.9.3) - '@primevue/icons@4.5.5(vue@3.5.38(typescript@5.9.3))': + '@primevue/icons@4.5.5(vue@3.5.39(typescript@5.9.3))': dependencies: '@primeuix/utils': 0.6.4 - '@primevue/core': 4.5.5(vue@3.5.38(typescript@5.9.3)) + '@primevue/core': 4.5.5(vue@3.5.39(typescript@5.9.3)) transitivePeerDependencies: - vue '@primevue/metadata@4.5.5': {} - '@rolldown/pluginutils@1.0.1': {} - - '@rollup/rollup-android-arm-eabi@4.62.0': - optional: true - - '@rollup/rollup-android-arm64@4.62.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.62.0': - optional: true - - '@rollup/rollup-darwin-x64@4.62.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.62.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.62.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.62.0': + '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.62.0': + '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.62.0': + '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.62.0': + '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.62.0': + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rollup/rollup-linux-loong64-musl@4.62.0': + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.62.0': + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rollup/rollup-linux-ppc64-musl@4.62.0': + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.62.0': + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rollup/rollup-linux-riscv64-musl@4.62.0': + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.62.0': + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.62.0': + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rollup/rollup-linux-x64-musl@4.62.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.62.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.62.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.62.0': + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rollup/rollup-win32-ia32-msvc@4.62.0': + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rollup/rollup-win32-x64-gnu@4.62.0': + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.62.0': - optional: true + '@rolldown/pluginutils@1.0.1': {} - '@tabler/icons-vue@3.44.0(vue@3.5.38(typescript@5.9.3))': + '@tabler/icons-vue@3.44.0(vue@3.5.39(typescript@5.9.3))': dependencies: '@tabler/icons': 3.44.0 - vue: 3.5.38(typescript@5.9.3) + vue: 3.5.39(typescript@5.9.3) '@tabler/icons@3.44.0': {} @@ -1454,18 +1247,21 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.3.1 - '@tailwindcss/vite@4.3.1(vite@6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.1(vite@8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.1 '@tailwindcss/oxide': 4.3.1 tailwindcss: 4.3.1 - vite: 6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + vite: 8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0) - '@types/estree@1.0.9': {} + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true '@types/jsesc@2.5.1': {} - '@types/node@26.0.0': + '@types/node@26.0.1': dependencies: undici-types: 8.3.0 @@ -1475,11 +1271,11 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@vitejs/plugin-vue@6.0.7(vite@6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) - vue: 3.5.38(typescript@5.9.3) + vite: 8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0) + vue: 3.5.39(typescript@5.9.3) '@volar/language-core@2.4.28': dependencies: @@ -1493,15 +1289,23 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue-macros/common@3.1.2(vue@3.5.38(typescript@5.9.3))': + '@vue-macros/common@3.1.2(vue@3.5.39(typescript@5.9.3))': dependencies: - '@vue/compiler-sfc': 3.5.38 + '@vue/compiler-sfc': 3.5.30 ast-kit: 2.2.0 - local-pkg: 1.2.1 + local-pkg: 1.1.2 magic-string-ast: 1.0.3 unplugin-utils: 0.3.1 optionalDependencies: - vue: 3.5.38(typescript@5.9.3) + vue: 3.5.39(typescript@5.9.3) + + '@vue/compiler-core@3.5.30': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.30 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 '@vue/compiler-core@3.5.38': dependencies: @@ -1511,35 +1315,70 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.39': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.39 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.30': + dependencies: + '@vue/compiler-core': 3.5.30 + '@vue/shared': 3.5.30 + '@vue/compiler-dom@3.5.38': dependencies: '@vue/compiler-core': 3.5.38 '@vue/shared': 3.5.38 - '@vue/compiler-sfc@3.5.38': + '@vue/compiler-dom@3.5.39': + dependencies: + '@vue/compiler-core': 3.5.39 + '@vue/shared': 3.5.39 + + '@vue/compiler-sfc@3.5.30': dependencies: '@babel/parser': 7.29.7 - '@vue/compiler-core': 3.5.38 - '@vue/compiler-dom': 3.5.38 - '@vue/compiler-ssr': 3.5.38 - '@vue/shared': 3.5.38 + '@vue/compiler-core': 3.5.30 + '@vue/compiler-dom': 3.5.30 + '@vue/compiler-ssr': 3.5.30 + '@vue/shared': 3.5.30 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.15 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.38': + '@vue/compiler-sfc@3.5.39': dependencies: - '@vue/compiler-dom': 3.5.38 - '@vue/shared': 3.5.38 + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.39 + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.30': + dependencies: + '@vue/compiler-dom': 3.5.30 + '@vue/shared': 3.5.30 + + '@vue/compiler-ssr@3.5.39': + dependencies: + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 '@vue/devtools-api@7.7.9': dependencies: '@vue/devtools-kit': 7.7.9 - '@vue/devtools-api@8.1.3': + '@vue/devtools-api@8.1.5': dependencies: - '@vue/devtools-kit': 8.1.3 + '@vue/devtools-kit': 8.1.5 '@vue/devtools-kit@7.7.9': dependencies: @@ -1551,9 +1390,9 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.6 - '@vue/devtools-kit@8.1.3': + '@vue/devtools-kit@8.1.5': dependencies: - '@vue/devtools-shared': 8.1.3 + '@vue/devtools-shared': 8.1.5 birpc: 2.9.0 hookable: 5.5.3 perfect-debounce: 2.1.0 @@ -1562,7 +1401,7 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/devtools-shared@8.1.3': {} + '@vue/devtools-shared@8.1.5': {} '@vue/language-core@3.3.5': dependencies: @@ -1574,49 +1413,53 @@ snapshots: path-browserify: 1.0.1 picomatch: 4.0.4 - '@vue/reactivity@3.5.38': + '@vue/reactivity@3.5.39': dependencies: - '@vue/shared': 3.5.38 + '@vue/shared': 3.5.39 - '@vue/runtime-core@3.5.38': + '@vue/runtime-core@3.5.39': dependencies: - '@vue/reactivity': 3.5.38 - '@vue/shared': 3.5.38 + '@vue/reactivity': 3.5.39 + '@vue/shared': 3.5.39 - '@vue/runtime-dom@3.5.38': + '@vue/runtime-dom@3.5.39': dependencies: - '@vue/reactivity': 3.5.38 - '@vue/runtime-core': 3.5.38 - '@vue/shared': 3.5.38 + '@vue/reactivity': 3.5.39 + '@vue/runtime-core': 3.5.39 + '@vue/shared': 3.5.39 csstype: 3.2.3 - '@vue/server-renderer@3.5.38(vue@3.5.38(typescript@5.9.3))': + '@vue/server-renderer@3.5.39(vue@3.5.39(typescript@5.9.3))': dependencies: - '@vue/compiler-ssr': 3.5.38 - '@vue/shared': 3.5.38 - vue: 3.5.38(typescript@5.9.3) + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 + vue: 3.5.39(typescript@5.9.3) + + '@vue/shared@3.5.30': {} '@vue/shared@3.5.38': {} - '@vue/tsconfig@0.9.1(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3))': + '@vue/shared@3.5.39': {} + + '@vue/tsconfig@0.9.1(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))': optionalDependencies: typescript: 5.9.3 - vue: 3.5.38(typescript@5.9.3) + vue: 3.5.39(typescript@5.9.3) - '@vueuse/core@14.3.0(vue@3.5.38(typescript@5.9.3))': + '@vueuse/core@14.3.0(vue@3.5.39(typescript@5.9.3))': dependencies: '@types/web-bluetooth': 0.0.21 '@vueuse/metadata': 14.3.0 - '@vueuse/shared': 14.3.0(vue@3.5.38(typescript@5.9.3)) - vue: 3.5.38(typescript@5.9.3) + '@vueuse/shared': 14.3.0(vue@3.5.39(typescript@5.9.3)) + vue: 3.5.39(typescript@5.9.3) '@vueuse/metadata@14.3.0': {} - '@vueuse/shared@14.3.0(vue@3.5.38(typescript@5.9.3))': + '@vueuse/shared@14.3.0(vue@3.5.39(typescript@5.9.3))': dependencies: - vue: 3.5.38(typescript@5.9.3) + vue: 3.5.39(typescript@5.9.3) - acorn@8.17.0: {} + acorn@8.16.0: {} alien-signals@3.2.1: {} @@ -1662,35 +1505,6 @@ snapshots: entities@7.0.1: {} - esbuild@0.25.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - estree-walker@2.0.2: {} exsolve@1.0.8: {} @@ -1765,10 +1579,16 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + local-pkg@1.1.2: + dependencies: + mlly: 1.8.1 + pkg-types: 2.3.0 + quansync: 0.2.11 + local-pkg@1.2.1: dependencies: - mlly: 1.8.2 - pkg-types: 2.3.1 + mlly: 1.8.1 + pkg-types: 2.3.0 quansync: 0.2.11 magic-string-ast@1.0.3: @@ -1787,12 +1607,19 @@ snapshots: mitt@3.0.1: {} + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + mlly@1.8.2: dependencies: - acorn: 8.17.0 + acorn: 8.16.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.6.4 + ufo: 1.6.3 muggle-string@0.4.1: {} @@ -1812,20 +1639,20 @@ snapshots: picomatch@4.0.4: {} - pinia@3.0.4(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3)): + pinia@3.0.4(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)): dependencies: '@vue/devtools-api': 7.7.9 - vue: 3.5.38(typescript@5.9.3) + vue: 3.5.39(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.8.2 + mlly: 1.8.1 pathe: 2.0.3 - pkg-types@2.3.1: + pkg-types@2.3.0: dependencies: confbox: 0.2.4 exsolve: 1.0.8 @@ -1851,13 +1678,13 @@ snapshots: prettier@3.6.0: {} - primevue@4.5.5(vue@3.5.38(typescript@5.9.3)): + primevue@4.5.5(vue@3.5.39(typescript@5.9.3)): dependencies: '@primeuix/styled': 0.7.4 '@primeuix/styles': 2.0.3 '@primeuix/utils': 0.6.4 - '@primevue/core': 4.5.5(vue@3.5.38(typescript@5.9.3)) - '@primevue/icons': 4.5.5(vue@3.5.38(typescript@5.9.3)) + '@primevue/core': 4.5.5(vue@3.5.39(typescript@5.9.3)) + '@primevue/icons': 4.5.5(vue@3.5.39(typescript@5.9.3)) transitivePeerDependencies: - vue @@ -1867,36 +1694,26 @@ snapshots: rfdc@1.4.1: {} - rollup@4.62.0: + rolldown@1.0.3: dependencies: - '@types/estree': 1.0.9 + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.62.0 - '@rollup/rollup-android-arm64': 4.62.0 - '@rollup/rollup-darwin-arm64': 4.62.0 - '@rollup/rollup-darwin-x64': 4.62.0 - '@rollup/rollup-freebsd-arm64': 4.62.0 - '@rollup/rollup-freebsd-x64': 4.62.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.0 - '@rollup/rollup-linux-arm-musleabihf': 4.62.0 - '@rollup/rollup-linux-arm64-gnu': 4.62.0 - '@rollup/rollup-linux-arm64-musl': 4.62.0 - '@rollup/rollup-linux-loong64-gnu': 4.62.0 - '@rollup/rollup-linux-loong64-musl': 4.62.0 - '@rollup/rollup-linux-ppc64-gnu': 4.62.0 - '@rollup/rollup-linux-ppc64-musl': 4.62.0 - '@rollup/rollup-linux-riscv64-gnu': 4.62.0 - '@rollup/rollup-linux-riscv64-musl': 4.62.0 - '@rollup/rollup-linux-s390x-gnu': 4.62.0 - '@rollup/rollup-linux-x64-gnu': 4.62.0 - '@rollup/rollup-linux-x64-musl': 4.62.0 - '@rollup/rollup-openbsd-x64': 4.62.0 - '@rollup/rollup-openharmony-arm64': 4.62.0 - '@rollup/rollup-win32-arm64-msvc': 4.62.0 - '@rollup/rollup-win32-ia32-msvc': 4.62.0 - '@rollup/rollup-win32-x64-gnu': 4.62.0 - '@rollup/rollup-win32-x64-msvc': 4.62.0 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 scule@1.3.0: {} @@ -1923,9 +1740,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tslib@2.8.1: + optional: true + typescript@5.9.3: {} - ufo@1.6.4: {} + ufo@1.6.3: {} undici-types@8.3.0: {} @@ -1934,7 +1754,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.4 - unplugin-vue-components@32.1.0(vue@3.5.38(typescript@5.9.3)): + unplugin-vue-components@32.1.0(vue@3.5.39(typescript@5.9.3)): dependencies: chokidar: 5.0.0 local-pkg: 1.2.1 @@ -1945,7 +1765,7 @@ snapshots: tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 - vue: 3.5.38(typescript@5.9.3) + vue: 3.5.39(typescript@5.9.3) unplugin@3.0.0: dependencies: @@ -1957,32 +1777,30 @@ snapshots: uuid@14.0.0: {} - vite@6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0): + vite@8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0): dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.4) + lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rollup: 4.62.0 + rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.0.0 + '@types/node': 26.0.1 fsevents: 2.3.3 jiti: 2.7.0 - lightningcss: 1.32.0 yaml: 2.9.0 vscode-uri@3.1.0: {} - vue-router@5.1.0(@vue/compiler-sfc@3.5.38)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3)))(vite@6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3)): + vue-router@5.1.0(@vue/compiler-sfc@3.5.39)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)))(vite@8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.39(typescript@5.9.3)): dependencies: '@babel/generator': 8.0.0 - '@vue-macros/common': 3.1.2(vue@3.5.38(typescript@5.9.3)) - '@vue/devtools-api': 8.1.3 + '@vue-macros/common': 3.1.2(vue@3.5.39(typescript@5.9.3)) + '@vue/devtools-api': 8.1.5 ast-walker-scope: 0.9.0 chokidar: 5.0.0 json5: 2.2.3 - local-pkg: 1.2.1 + local-pkg: 1.1.2 magic-string: 0.30.21 mlly: 1.8.2 muggle-string: 0.4.1 @@ -1992,12 +1810,12 @@ snapshots: tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 - vue: 3.5.38(typescript@5.9.3) + vue: 3.5.39(typescript@5.9.3) yaml: 2.9.0 optionalDependencies: - '@vue/compiler-sfc': 3.5.38 - pinia: 3.0.4(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3)) - vite: 6.4.3(@types/node@26.0.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) + '@vue/compiler-sfc': 3.5.39 + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)) + vite: 8.0.16(@types/node@26.0.1)(jiti@2.7.0)(yaml@2.9.0) vue-tsc@3.3.5(typescript@5.9.3): dependencies: @@ -2005,13 +1823,13 @@ snapshots: '@vue/language-core': 3.3.5 typescript: 5.9.3 - vue@3.5.38(typescript@5.9.3): + vue@3.5.39(typescript@5.9.3): dependencies: - '@vue/compiler-dom': 3.5.38 - '@vue/compiler-sfc': 3.5.38 - '@vue/runtime-dom': 3.5.38 - '@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@5.9.3)) - '@vue/shared': 3.5.38 + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-sfc': 3.5.39 + '@vue/runtime-dom': 3.5.39 + '@vue/server-renderer': 3.5.39(vue@3.5.39(typescript@5.9.3)) + '@vue/shared': 3.5.39 optionalDependencies: typescript: 5.9.3 From 0aaa3de065f9d8bf77f4de6ccf12e26250e54098 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:44:18 +0000 Subject: [PATCH 3/9] Bump pypdf from 6.12.2 to 6.13.3 Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.12.2 to 6.13.3. - [Release notes](https://github.com/py-pdf/pypdf/releases) - [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md) - [Commits](https://github.com/py-pdf/pypdf/compare/6.12.2...6.13.3) --- updated-dependencies: - dependency-name: pypdf dependency-version: 6.13.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 485f57c9..2d01cbf6 100644 --- a/uv.lock +++ b/uv.lock @@ -4206,11 +4206,11 @@ wheels = [ [[package]] name = "pypdf" -version = "6.12.2" +version = "6.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/6d/20879428577c1e57ecd41b69dc86beabf43db9287ad2e702207f8b48c751/pypdf-6.12.2.tar.gz", hash = "sha256:111669eb6680c04495ae0c113a1476e3bf93a95761d23c7406b591c80a6490b1", size = 6468184, upload-time = "2026-05-26T13:31:26.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/18/9947cc201af9ccf76720fd3347bf4f70eb882ce3fcf4cb05f7443e4cf871/pypdf-6.13.3.tar.gz", hash = "sha256:f3cb822769725f1bac658c406cfc9460399043f3750c2d3e4650e0a85eacabd7", size = 6484063, upload-time = "2026-06-17T15:22:00.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/44/fee070a16639d9869bb6a7e0f3a1b3946da1d66f32b9260b4d19cb90d7b2/pypdf-6.12.2-py3-none-any.whl", hash = "sha256:67b2699357a1f3f4c945940ea80826349ee507c9e2577724a14b4941982c104d", size = 343865, upload-time = "2026-05-26T13:31:25.068Z" }, + { url = "https://files.pythonhosted.org/packages/94/56/2967e621598987905fb8cdfadd8f8de6b5c68c9351f0523c4df8409f28f1/pypdf-6.13.3-py3-none-any.whl", hash = "sha256:c6e3f86afb625791510b02ad5480e94b63970bb957df75d44657c282ecc52224", size = 347288, upload-time = "2026-06-17T15:21:59.512Z" }, ] [[package]] From d10731cdff6a74aa76a072ac3008e3e5fc5f0148 Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Fri, 19 Jun 2026 23:42:50 +0800 Subject: [PATCH 4/9] Bump pom-scijava from 38.0.1 to 40.0.0 (Fiji-Stable track) fiji/fiji 'master' (the Fiji-Stable line) currently parents to pom-scijava 40.0.0, so this keeps the plugin aligned with the stable distribution while staying on Java 8 (no nix/CI toolchain change). Moves off 38.0.1: imglib2 6.3.0 -> 7.1.5, imagej2 2.15.0 -> 2.17.0, jackson 2.16.1 -> 2.18.0. Supersedes Dependabot #115 (38.0.1 -> 44.0.0), which would raise the build's Java floor from 8 to 11. --- plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index d5387872..3bf4d918 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 38.0.1 + 40.0.0 From bf74f1fd786bdddf8f73a7d1f27f10a45feb4b2d Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Tue, 30 Jun 2026 14:05:43 +0800 Subject: [PATCH 5/9] Reset syntax_error_counter on successful model response syntax_error_counter in LeaderDriven.run() was never reset, so it tracked cumulative ReAct syntax errors across a whole dialog and could abort a task after 3 scattered, fully-recovered errors. Reset it to 0 at the recovery send_correction (the single choke point where the agent_resp-is-None branch routes), matching the existing tool_retry_counter reset idiom. A model that is genuinely stuck still aborts after max_syntax_errors consecutive errors. Adds a regression test (test_leader_syntax_reset.py) covering both the cumulative-with-recovery case (now completes) and the 3-consecutive case (still aborts). Assisted-by: Claude-Code:GLM-5.2 --- copilotj/multiagent/leader_multiagent.py | 1 + .../multiagent/test_leader_syntax_reset.py | 155 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 copilotj/test/multiagent/test_leader_syntax_reset.py diff --git a/copilotj/multiagent/leader_multiagent.py b/copilotj/multiagent/leader_multiagent.py index ec51b94e..95d35f1d 100644 --- a/copilotj/multiagent/leader_multiagent.py +++ b/copilotj/multiagent/leader_multiagent.py @@ -784,6 +784,7 @@ async def run(self, task: str, trace_ctx: Langfuse | None = None) -> None: "Reply with a single Thought + Action, or a Final Answer.", trace_ctx=trace_ctx, ) + syntax_error_counter = 0 # Reset syntax error counter on success except ModelSyntaxError as e: syntax_error_counter += 1 self.log_error( diff --git a/copilotj/test/multiagent/test_leader_syntax_reset.py b/copilotj/test/multiagent/test_leader_syntax_reset.py new file mode 100644 index 00000000..988e51b2 --- /dev/null +++ b/copilotj/test/multiagent/test_leader_syntax_reset.py @@ -0,0 +1,155 @@ +# SPDX-FileCopyrightText: Copyright contributors to the CopilotJ project. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for LeaderDriven.run() syntax-error counter reset. + +`syntax_error_counter` must track CONSECUTIVE ReAct syntax errors, not +cumulative ones: a model that recovers (produces a valid response) between +errors must have its counter reset, mirroring `tool_retry_counter` at L850. + +Control-flow fact these tests rely on (verified by trace + outside-voice +review): every ModelSyntaxError increment in run() leaves `agent_resp = None`, +which routes recovery through the single `send_correction` at ~L782. That site +is the only place a reset is load-bearing. +""" + +import asyncio + +from copilotj.core import ModelSyntaxError +from copilotj.multiagent.leader_multiagent import LeaderDriven + +_MAX_SYNTAX_ERRORS = 3 + + +class _Args: + def model_dump(self) -> dict: + return {} + + +class _Tool: + def __init__(self, name: str) -> None: + self.name = name + + +class _ToolCall: + def __init__(self, name: str) -> None: + self.tool = _Tool(name) + self.args = _Args() + + +class _Resp: + """Minimal stand-in for ModelResponse exercised by run().""" + + def __init__(self, *, content=None, reasoning_content=None, tool_calls=None) -> None: + self.content = content + self.reasoning_content = reasoning_content + self.tool_calls = tool_calls + + +def _syntax_err() -> ModelSyntaxError: + return ModelSyntaxError("bad ReAct") + + +def _action() -> _Resp: + """A valid parsed response that carries a tool call (no final answer).""" + return _Resp(reasoning_content="thought", tool_calls=[_ToolCall("some_tool")]) + + +def _final() -> _Resp: + return _Resp(content="final answer") + + +class _ScriptedLeader: + """LeaderAgent stub whose dialog methods pop a scripted queue. + + Each queue holds either a _Resp (returned) or a ModelSyntaxError (raised). + """ + + def __init__(self) -> None: + self.begin_dialog_q: list = [] + self.send_correction_q: list = [] + self.continue_dialog_q: list = [] + self.call_tool_q: list = [] + + @staticmethod + def _pop(queue: list): + item = queue.pop(0) + if isinstance(item, Exception): + raise item + return item + + async def begin_dialog(self, task, trace_ctx=None): + return self._pop(self.begin_dialog_q) + + async def send_correction(self, message, trace_ctx=None): + return self._pop(self.send_correction_q) + + async def continue_dialog(self, prior_response, observation, trace_ctx=None): + return self._pop(self.continue_dialog_q) + + async def _call_tool(self, tool_call): + return self._pop(self.call_tool_q) + + +async def _noop_dialog_changed(*args, **kwargs) -> None: + pass + + +async def _drive(leader: _ScriptedLeader) -> str: + """Run one LeaderDriven.run() dialog and return its final status. + + Bypasses LeaderDriven.__init__ (which needs cfg/apis/model_client); run() + only touches the attributes wired up here. + """ + pattern = LeaderDriven.__new__(LeaderDriven) + pattern.dialog_counter = 1 + pattern._summarize_task = None # type: ignore[attr-defined] + pattern.log_info = lambda *a, **k: None # type: ignore[assignment] + pattern.log_error = lambda *a, **k: None # type: ignore[assignment] + pattern.dialog_changed = _noop_dialog_changed # type: ignore[assignment] + pattern.leader_agent = leader # type: ignore[assignment] + + captured: dict = {} + + async def _record(*args) -> None: + # _background_summarize_and_store(dialog_id, task, dialog_context) + captured["ctx"] = args[-1] + + pattern._background_summarize_and_store = _record # type: ignore[assignment] + + await pattern.run("do the thing") + await pattern._summarize_task # type: ignore[attr-defined] + return captured["ctx"]["status"] + + +def test_cumulative_errors_with_recovery_complete(): + """>=3 total syntax errors, each recovered from, must NOT abort. + + Under the old cumulative counting this sequence aborted at the third error + (status "failed"); with the reset-on-recovery it completes. + """ + leader = _ScriptedLeader() + leader.begin_dialog_q = [_syntax_err()] + # Three recoveries at site (a), each producing a valid tool call: + leader.send_correction_q = [_action(), _action(), _action()] + leader.call_tool_q = ["ok", "ok", "ok"] + # Two errors interleaved, then a final answer on the third continue: + leader.continue_dialog_q = [_syntax_err(), _syntax_err(), _final()] + + status = asyncio.run(_drive(leader)) + + assert status == "completed" + + +def test_consecutive_errors_still_abort(): + """Three consecutive syntax errors (no recovery) must still abort.""" + leader = _ScriptedLeader() + leader.begin_dialog_q = [_syntax_err()] + leader.send_correction_q = [_syntax_err(), _syntax_err()] + + status = asyncio.run(_drive(leader)) + + assert status == "failed" + # Sanity: we exercised the abort threshold, not some other failure path. + assert len(leader.send_correction_q) == 0 From d7155e2598fccb74b3fe2b456f99438853bb956f Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Sun, 21 Jun 2026 09:19:49 +0800 Subject: [PATCH 6/9] Add CIJ_PROXY download proxy with explicit config injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce CIJ_PROXY: a single env var that proxies all backend downloads (HuggingFace weights, LiteLLM model DB, BioImage model zoo, Tavily/DDGS web research, cellpose/stardist segmentation weights) and acts as the fallback proxy for the LLM/VLM clients when COPILOTJ_LLM_PROXY / COPILOTJ_VLM_PROXY are unset. The proxy is passed explicitly to each download client — no global proxy env vars are set, so localhost services (Ollama, bridge WS, plugin API) are never proxied. cellpose/stardist, which only read HTTP_PROXY, are wrapped in the existing scoped temporary_proxy context manager for the weight download only. Also fixes a latent bug where new_vlm_model_client ignored cfg.vlm_proxy. The resulting download clients receive Config explicitly rather than via load_config() lookups: ddg_search / cellpose_segmentation / stardist_segmentation become make_*(cfg) factories (cellpose/stardist via functools.partial, which FunctionTool already supports), the model DB download path threads an optional cfg from the server, and the HuggingFace proxy is configured once at process startup. Adds proxy unit tests (copilotj/test/{core,util}/test_proxy.py) and a proxy_dict doctest, and documents CIJ_PROXY in .env.template, AGENTS.md, and DEVELOPMENT.md. --- .env.template | 5 + AGENTS.md | 1 + DEVELOPMENT.md | 41 +++--- copilotj/appose_worker.py | 3 + copilotj/core/config.py | 8 +- copilotj/core/embedding.py | 28 +++- copilotj/core/model_client/__init__.py | 4 +- copilotj/core/model_info.py | 28 ++-- copilotj/core/model_listing.py | 15 ++- .../agent_configs/research_agent.toml | 2 +- .../multiagent/agent_configs/tool_agent.toml | 4 +- copilotj/multiagent/py_tools.py | 32 ++++- copilotj/multiagent/research_tools.py | 122 ++++++++++-------- copilotj/server/server.py | 17 ++- copilotj/test/core/test_proxy.py | 114 ++++++++++++++++ copilotj/test/util/test_proxy.py | 73 +++++++++++ copilotj/util/env.py | 20 ++- scripts/rag_builder.py | 4 + 18 files changed, 414 insertions(+), 107 deletions(-) create mode 100644 copilotj/test/core/test_proxy.py create mode 100644 copilotj/test/util/test_proxy.py diff --git a/.env.template b/.env.template index 9eb23631..b0ed202c 100644 --- a/.env.template +++ b/.env.template @@ -12,6 +12,11 @@ COPILOTJ_VLM_API_KEY=sk-xxxxxxxx # When enabled, COPILOTJ_VLM_MODEL and COPILOTJ_VLM_API_KEY must be set. # COPILOTJ_VISION_ENABLED=1 # COPILOTJ_VLM_PROXY=http://127.0.0.1:8080 +# Global download proxy — passed explicitly to all backend download clients (HuggingFace, +# model DB, bioimage model zoo, web research/Tavily/DDGS, cellpose/stardist) and used as the +# fallback for the LLM/VLM API clients when COPILOTJ_LLM_PROXY/COPILOTJ_VLM_PROXY are unset. +# Passed via library proxy parameters (no global proxy env vars are set). +# CIJ_PROXY=http://127.0.0.1:8080 COPILOTJ_VLM_BASE_URL=https://api.siliconflow.cn/v1 # VLM provider (optional, falls back to COPILOTJ_LLM_PROVIDER) # COPILOTJ_VLM_PROVIDER=gemini diff --git a/AGENTS.md b/AGENTS.md index 12001899..7d769e15 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,7 @@ The core agent framework follows a layered architecture: - `COPILOTJ_VLM_MODEL`: Vision model for image analysis (requires `COPILOTJ_VISION_ENABLED=1`) - `COPILOTJ_VLM_API_KEY`: API key for the vision model - `COPILOTJ_LLM_PROXY`: HTTP proxy +- `CIJ_PROXY`: Global download proxy for all backend downloads (HuggingFace, model DB, bioimage model zoo, research/Tavily/DDGS, cellpose/stardist); also the fallback proxy for the LLM/VLM clients when their own proxy is unset. Passed explicitly to each download client (no global proxy env vars are set) - `COPILOTJ_KB_AUTOSAVE`: Set `1` to auto-ingest dialog summaries into knowledge bank - `LANGFUSE_SECRET_KEY`/`LANGFUSE_PUBLIC_KEY`: Optional Langfuse observability diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 43537973..15b8e818 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -217,24 +217,25 @@ Note: Ollama models generally do not support image input. If image understanding ### All configuration variables -| Variable | Description | -| ------------------------- | ------------------------------------------------------------------------------- | -| `COPILOTJ_LLM_MODEL` | Main LLM model name (required) | -| `COPILOTJ_LLM_API_KEY` | API key for the main model (required) | -| `COPILOTJ_LLM_BASE_URL` | Override API endpoint for the main model | -| `COPILOTJ_LLM_PROVIDER` | Explicit LLM provider selection (optional; auto-detected if not set) | -| `COPILOTJ_VLM_MODEL` | Vision-language model name (optional) | -| `COPILOTJ_VLM_API_KEY` | API key for the VLM (falls back to `COPILOTJ_LLM_API_KEY`) | -| `COPILOTJ_VLM_BASE_URL` | Override API endpoint for the VLM (falls back to `COPILOTJ_LLM_BASE_URL`) | -| `COPILOTJ_VLM_PROVIDER` | Explicit VLM provider (optional; falls back to `COPILOTJ_LLM_PROVIDER`) | -| `COPILOTJ_LLM_PROXY` | HTTP/HTTPS proxy for LLM outbound API requests | -| `COPILOTJ_VLM_PROXY` | HTTP/HTTPS proxy for VLM outbound requests (falls back to `COPILOTJ_LLM_PROXY`) | -| `COPILOTJ_TAVILY_API_KEY` | Tavily API key for live web search | -| `COPILOTJ_KB_AUTOSAVE` | Set `1` to auto-ingest dialog summaries into the knowledge bank | -| `COPILOTJ_DEV` | Development mode (presence-based flag) | -| `LANGFUSE_SECRET_KEY` | Langfuse secret key for observability | -| `LANGFUSE_PUBLIC_KEY` | Langfuse public key for observability | -| `LANGFUSE_HOST` | Langfuse host URL (e.g. `https://us.cloud.langfuse.com`) | +| Variable | Description | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `COPILOTJ_LLM_MODEL` | Main LLM model name (required) | +| `COPILOTJ_LLM_API_KEY` | API key for the main model (required) | +| `COPILOTJ_LLM_BASE_URL` | Override API endpoint for the main model | +| `COPILOTJ_LLM_PROVIDER` | Explicit LLM provider selection (optional; auto-detected if not set) | +| `COPILOTJ_VLM_MODEL` | Vision-language model name (optional) | +| `COPILOTJ_VLM_API_KEY` | API key for the VLM (falls back to `COPILOTJ_LLM_API_KEY`) | +| `COPILOTJ_VLM_BASE_URL` | Override API endpoint for the VLM (falls back to `COPILOTJ_LLM_BASE_URL`) | +| `COPILOTJ_VLM_PROVIDER` | Explicit VLM provider (optional; falls back to `COPILOTJ_LLM_PROVIDER`) | +| `COPILOTJ_LLM_PROXY` | HTTP/HTTPS proxy for LLM outbound API requests | +| `COPILOTJ_VLM_PROXY` | HTTP/HTTPS proxy for VLM outbound requests (falls back to `COPILOTJ_LLM_PROXY`) | +| `CIJ_PROXY` | Global download proxy for HuggingFace, model DB, bioimage zoo, research/Tavily/DDGS, cellpose/stardist; fallback for the LLM/VLM proxies | +| `COPILOTJ_TAVILY_API_KEY` | Tavily API key for live web search | +| `COPILOTJ_KB_AUTOSAVE` | Set `1` to auto-ingest dialog summaries into the knowledge bank | +| `COPILOTJ_DEV` | Development mode (presence-based flag) | +| `LANGFUSE_SECRET_KEY` | Langfuse secret key for observability | +| `LANGFUSE_PUBLIC_KEY` | Langfuse public key for observability | +| `LANGFUSE_HOST` | Langfuse host URL (e.g. `https://us.cloud.langfuse.com`) | A complete `.env.local` template: @@ -246,6 +247,10 @@ COPILOTJ_LLM_API_KEY=sk-xxxxxxxx #COPILOTJ_LLM_PROXY=http://PATH_TO_YOUR_PROXY #COPILOTJ_LLM_PROVIDER=openai +# Global download proxy (HuggingFace, model DB, bioimage, web research, cellpose/stardist). +# Also the fallback for the LLM/VLM API proxies above. Passed explicitly — no global proxy env vars. +#CIJ_PROXY=http://127.0.0.1:8080 + # Vision-language model (image understanding) — optional, choose one provider #COPILOTJ_VLM_MODEL=gemini-2.5-flash #COPILOTJ_VLM_API_KEY=AI-xxxxxxxx diff --git a/copilotj/appose_worker.py b/copilotj/appose_worker.py index 4ab0b9c5..c4b4b771 100644 --- a/copilotj/appose_worker.py +++ b/copilotj/appose_worker.py @@ -42,6 +42,7 @@ from copilotj.core import load_config from copilotj.core.config import bootstrap_assets, load_managed_config, save_managed_config +from copilotj.core.embedding import configure_download_proxy from copilotj.core.lifecycle import run_cleanup as _run_cleanup from copilotj.server import Server @@ -101,6 +102,8 @@ def start_server() -> None: saved_port = _extract_port(load_managed_config().get("server_url")) cfg = load_config() + configure_download_proxy(cfg) + _server = Server(cfg) _loop = asyncio.new_event_loop() diff --git a/copilotj/core/config.py b/copilotj/core/config.py index 422b52d9..a83c28e1 100644 --- a/copilotj/core/config.py +++ b/copilotj/core/config.py @@ -69,6 +69,11 @@ class Config: vlm_proxy: str | None = None vlm_provider: str | None = None + # Global download proxy — passed explicitly to all backend download clients (HF, + # model DB, bioimage zoo, research/Tavily/DDGS, cellpose/stardist). Also the + # fallback proxy for the LLM/VLM API clients when their own proxy is unset. + cij_proxy: str | None = None + # Tool keys tavily_api_key: str | None = None @@ -142,6 +147,7 @@ def load_config() -> Config: llm_provider=os.getenv("COPILOTJ_LLM_PROVIDER", None), vlm_proxy=os.getenv("COPILOTJ_VLM_PROXY", None), vlm_provider=os.getenv("COPILOTJ_VLM_PROVIDER", None), + cij_proxy=os.getenv("CIJ_PROXY"), tavily_api_key=os.getenv("COPILOTJ_TAVILY_API_KEY", None), kb_autosave=os.getenv("COPILOTJ_KB_AUTOSAVE", "0") == "1", dev=os.getenv("COPILOTJ_DEV") is not None, @@ -176,7 +182,7 @@ def resolve_vision_config(cfg: Config) -> Config: from copilotj.core.model_info import get_model_capabilities # Detect main model vision capability - main_caps = get_model_capabilities(cfg.llm_model) if cfg.llm_model else None + main_caps = get_model_capabilities(cfg.llm_model, cfg) if cfg.llm_model else None llm_supports_vision = bool(main_caps and main_caps.supports_vision) # Check if a separate VLM is explicitly configured diff --git a/copilotj/core/embedding.py b/copilotj/core/embedding.py index 776f3d14..a0bcbc9a 100644 --- a/copilotj/core/embedding.py +++ b/copilotj/core/embedding.py @@ -6,6 +6,8 @@ from langchain_core.embeddings import Embeddings +from copilotj.core.config import Config + logger = logging.getLogger(__name__) RAG_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2" @@ -13,13 +15,18 @@ __all__ = [ "RAG_EMBEDDING_MODEL", + "configure_download_proxy", "get_embeddings", "new_local_embeddings", ] def new_local_embeddings() -> Embeddings: - """Create the local embedding model used by the bundled FAISS index.""" + """Create the local embedding model used by the bundled FAISS index. + + The HuggingFace download proxy is configured once at process startup via + :func:`configure_download_proxy`, so this function needs no ``Config``. + """ from langchain_huggingface import HuggingFaceEmbeddings device = _detect_embedding_device() @@ -32,6 +39,25 @@ def new_local_embeddings() -> Embeddings: ) +def configure_download_proxy(cfg: Config) -> None: + """Route HuggingFace downloads through ``CIJ_PROXY`` (explicit, no env vars). + + ``huggingface_hub`` (httpx-based) downloads weights via a client produced by its + client factory. Installing a factory that returns a proxied ``httpx.Client`` makes + sentence-transformers/huggingface_hub fetch the embedding weights through the + proxy without touching ``os.environ``. Call once at process startup (server / + appose_worker / rag_builder) before the first HF download; a no-op when no + download proxy is configured. + """ + proxy = cfg.cij_proxy + if not proxy: + return + import httpx + from huggingface_hub import set_client_factory + + set_client_factory(lambda: httpx.Client(proxy=proxy)) + + def get_embeddings() -> Embeddings: """Get the local embeddings used for ImageJ RAG.""" global _embeddings diff --git a/copilotj/core/model_client/__init__.py b/copilotj/core/model_client/__init__.py index aa66f00e..b4a8d88e 100644 --- a/copilotj/core/model_client/__init__.py +++ b/copilotj/core/model_client/__init__.py @@ -65,7 +65,7 @@ def new_vlm_model_client(cfg: Config) -> ModelClient: return _new_model_client( cfg.vlm_model, cfg.vlm_api_key, - proxy=cfg.llm_proxy, + proxy=cfg.vlm_proxy or cfg.cij_proxy, base_url=cfg.vlm_base_url, cfg=cfg, provider=cfg.vlm_provider or cfg.llm_provider, @@ -129,7 +129,7 @@ def _new_model_client( provider: str | None = None, ) -> ModelClient: cfg = cfg or load_config() - proxy = proxy or cfg.llm_proxy + proxy = proxy or cfg.llm_proxy or cfg.cij_proxy # If provider is explicitly given, use it directly. if provider is not None: diff --git a/copilotj/core/model_info.py b/copilotj/core/model_info.py index 7912e487..d8685f55 100644 --- a/copilotj/core/model_info.py +++ b/copilotj/core/model_info.py @@ -23,7 +23,7 @@ import aiohttp -from copilotj.core.config import get_home +from copilotj.core.config import Config, get_home, load_config __all__ = [ "CatalogModel", @@ -232,7 +232,7 @@ def _store_db(data: dict[str, Any]) -> None: # --------------------------------------------------------------------------- -async def download_db(*, force: bool = False) -> Path | None: +async def download_db(*, force: bool = False, cfg: Config | None = None) -> Path | None: """Download the model database from GitHub. Skips the download if the cache is fresh (unless *force* is ``True``). @@ -250,9 +250,10 @@ async def download_db(*, force: bool = False) -> Path | None: _log.info("Downloading model DB from %s", LITELLM_DB_URL) try: + proxy = (cfg or load_config()).cij_proxy timeout = aiohttp.ClientTimeout(total=_DOWNLOAD_TIMEOUT) async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.get(LITELLM_DB_URL) as resp: + async with session.get(LITELLM_DB_URL, proxy=proxy) as resp: resp.raise_for_status() data = await resp.json(content_type=None) @@ -268,12 +269,17 @@ async def download_db(*, force: bool = False) -> Path | None: return path if path.exists() else None -def _download_db_sync() -> dict[str, Any]: +def _download_db_sync(cfg: Config | None = None) -> dict[str, Any]: """Synchronous fallback download using ``urllib`` (for CLI / first-run).""" _log.info("Downloading model DB (sync) from %s", LITELLM_DB_URL) try: + proxy = (cfg or load_config()).cij_proxy req = urllib.request.Request(LITELLM_DB_URL) - with urllib.request.urlopen(req, timeout=_DOWNLOAD_TIMEOUT) as resp: + if proxy: + opener = urllib.request.build_opener(urllib.request.ProxyHandler({"http": proxy, "https": proxy})) + else: + opener = urllib.request.build_opener() + with opener.open(req, timeout=_DOWNLOAD_TIMEOUT) as resp: data = json.loads(resp.read().decode("utf-8")) if isinstance(data, dict) and len(data) >= 100: @@ -382,7 +388,7 @@ def _ollama_vision_heuristic(model: str) -> bool: # --------------------------------------------------------------------------- -def get_model_capabilities(model: str) -> ModelCapabilities: +def get_model_capabilities(model: str, cfg: Config | None = None) -> ModelCapabilities: """Return capabilities for *model*. Loads the cached model database, looks up the model (with normalisation), @@ -402,7 +408,7 @@ def get_model_capabilities(model: str) -> ModelCapabilities: db = _load_db() if not db: # No cache available — try a blocking download on first use. - db = _download_db_sync() + db = _download_db_sync(cfg) entry = _lookup_model(db, model) if db else None @@ -464,7 +470,7 @@ def _merge_supplements(provider: str, catalog_models: list[CatalogModel]) -> lis return merged -def list_catalog_models(provider: str) -> list[CatalogModel]: +def list_catalog_models(provider: str, cfg: Config | None = None) -> list[CatalogModel]: """Return chat models from the cached LiteLLM catalog for *provider*. Filters entries whose ``litellm_provider`` equals *provider* and whose @@ -479,7 +485,7 @@ def list_catalog_models(provider: str) -> list[CatalogModel]: """ db = _load_db() if not db: - db = _download_db_sync() + db = _download_db_sync(cfg) if not db: return _merge_supplements(provider, []) @@ -514,11 +520,11 @@ def list_catalog_models(provider: str) -> list[CatalogModel]: return _merge_supplements(provider, models) -async def ensure_model_db_async() -> bool: +async def ensure_model_db_async(cfg: Config | None = None) -> bool: """Ensure the model DB is available (download if stale). Called at server startup as a background task. Returns ``True`` if the DB is available (fresh or stale). """ - path = await download_db() + path = await download_db(cfg=cfg) return path is not None and path.exists() diff --git a/copilotj/core/model_listing.py b/copilotj/core/model_listing.py index 09a99c56..71c084e2 100644 --- a/copilotj/core/model_listing.py +++ b/copilotj/core/model_listing.py @@ -19,6 +19,7 @@ import aiohttp +from copilotj.core.config import Config from copilotj.core.model_info import CatalogModel, get_model_capabilities, list_catalog_models __all__ = [ @@ -50,7 +51,9 @@ async def _fetch_ollama_tags(base_url: str, *, timeout: float) -> dict[str, Any] return None -async def list_ollama_models(base_url: str, *, timeout: float = 2.0) -> list[CatalogModel] | None: +async def list_ollama_models( + base_url: str, *, timeout: float = 2.0, cfg: Config | None = None +) -> list[CatalogModel] | None: """List models installed on a local Ollama instance. Performs a live ``GET {base_url}/api/tags``. Ollama runs locally, so we @@ -69,7 +72,7 @@ async def list_ollama_models(base_url: str, *, timeout: float = 2.0) -> list[Cat name = entry.get("name") or entry.get("model") if not name: continue - caps = get_model_capabilities(f"ollama/{name}") + caps = get_model_capabilities(f"ollama/{name}", cfg) models.append( CatalogModel( id=name, @@ -94,7 +97,9 @@ def _model_to_dict(m: CatalogModel) -> dict[str, Any]: } -async def list_provider_models(provider: str, *, base_url: str | None = None) -> dict[str, Any]: +async def list_provider_models( + provider: str, *, base_url: str | None = None, cfg: Config | None = None +) -> dict[str, Any]: """Resolve available models for *provider* into a uniform dict. - ``ollama`` → live ``/api/tags`` at ``base_url`` (default @@ -107,7 +112,7 @@ async def list_provider_models(provider: str, *, base_url: str | None = None) -> Never raises — callers can ``asyncio.gather`` over providers safely. """ if provider == "ollama": - models = await list_ollama_models(base_url or DEFAULT_OLLAMA_URL) + models = await list_ollama_models(base_url or DEFAULT_OLLAMA_URL, cfg=cfg) if models is None: return {"provider": provider, "source": "unreachable", "models": []} return { @@ -116,5 +121,5 @@ async def list_provider_models(provider: str, *, base_url: str | None = None) -> "models": [_model_to_dict(m) for m in models], } - models = list_catalog_models(provider) + models = list_catalog_models(provider, cfg) return {"provider": provider, "source": "catalog", "models": [_model_to_dict(m) for m in models]} diff --git a/copilotj/multiagent/agent_configs/research_agent.toml b/copilotj/multiagent/agent_configs/research_agent.toml index 0f5d3f3d..e3296398 100644 --- a/copilotj/multiagent/agent_configs/research_agent.toml +++ b/copilotj/multiagent/agent_configs/research_agent.toml @@ -147,7 +147,7 @@ Use this tool when you need to: [[tools]] display_name = "DuckDuckGo Search" -function = "copilotj.multiagent.research_tools.ddg_search" +factory = "copilotj.multiagent.research_tools.make_ddg_search" description = """ Use this tool to search for current information and image links using DuckDuckGo. diff --git a/copilotj/multiagent/agent_configs/tool_agent.toml b/copilotj/multiagent/agent_configs/tool_agent.toml index 9e16c91a..e93a99ed 100644 --- a/copilotj/multiagent/agent_configs/tool_agent.toml +++ b/copilotj/multiagent/agent_configs/tool_agent.toml @@ -133,7 +133,7 @@ Use this tool for specialized tools fail or need debugging. **ERROR RECOVERY ONL """ [[tools]] -function = "copilotj.multiagent.py_tools.cellpose_segmentation" +factory = "copilotj.multiagent.py_tools.make_cellpose_segmentation" description = """ Deep learning-based cell segmentation using Cellpose. Supports nuclei, cytoplasm models with GPU \ acceleration. Optimized with native cellpose image loading for maximum compatibility. @@ -161,7 +161,7 @@ signals. [[tools]] display_name = "StarDist Segmentation" -function = "copilotj.multiagent.py_tools.stardist_segmentation" +factory = "copilotj.multiagent.py_tools.make_stardist_segmentation" description = """ StarDist instance segmentation for cells and nuclei. diff --git a/copilotj/multiagent/py_tools.py b/copilotj/multiagent/py_tools.py index 7b3fee2a..b854642b 100644 --- a/copilotj/multiagent/py_tools.py +++ b/copilotj/multiagent/py_tools.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 import asyncio +import functools import logging import os from pathlib import Path @@ -15,11 +16,12 @@ from skimage.util import img_as_float32, img_as_ubyte from stardist.models import StarDist2D -from copilotj.core.config import get_home +from copilotj.core.config import Config, get_home, load_config +from copilotj.util import temporary_proxy __all__ = [ - "cellpose_segmentation", - "stardist_segmentation", + "make_cellpose_segmentation", + "make_stardist_segmentation", "biapy_tool", # Segmentation tools "gauss_otsu_labeling_tool", @@ -52,6 +54,7 @@ def get_project_templates_dir() -> Path: async def cellpose_segmentation( + cfg: Config, image_path: Annotated[str | None, "Path to the input image or directory (use forward slashes)"] = None, model_type: Annotated[str, "Type of model to use (nuclei, cyto, cyto2)"] = "nuclei", diameter: Annotated[int | None, "Expected cell diameter (if None, will be estimated)"] = None, @@ -111,7 +114,10 @@ async def cellpose_segmentation( # Run Cellpose logger.info(f"Running Cellpose model: {model_type}") - model = models.CellposeModel(gpu=gpu, model_type=model_type) + # cellpose downloads pretrained weights via raw urllib (no proxy param) on first + # use, so temporarily expose CIJ_PROXY through env vars for this call only. + with temporary_proxy(cfg): + model = models.CellposeModel(gpu=gpu, model_type=model_type) # Run model evaluation in a thread pool to avoid blocking masks, flows, styles = await asyncio.to_thread( @@ -178,6 +184,7 @@ async def cellpose_segmentation( async def stardist_segmentation( + cfg: Config, image_path: Annotated[str | None, "Path to the input image (use forward slashes)"] = None, image_array: Annotated[Any | None, "Optional in-memory image; overrides image_path if provided"] = None, image_type: Annotated[str, "fluorescence | he | tissue | brightfield | phase | dapi | unknown"] = "fluorescence", @@ -228,7 +235,10 @@ async def stardist_segmentation( # Lazy: download the standard pretrained model on first use and cache it # under ~/.stardist/models/ (mirrors how cellpose fetches its # weights). Keeps the plugin JAR free of bundled model weights. - model_instance = StarDist2D.from_pretrained(model_name) + # stardist/csbdeep fetch weights via raw urllib (no proxy param); expose + # CIJ_PROXY through env vars for the model load only. + with temporary_proxy(cfg): + model_instance = StarDist2D.from_pretrained(model_name) kwargs = {} if prob_thresh is not None: @@ -265,6 +275,16 @@ async def stardist_segmentation( raise +def make_cellpose_segmentation(cfg: Config): + """Factory: return a ``cellpose_segmentation`` callable bound to *cfg*.""" + return functools.partial(cellpose_segmentation, cfg) + + +def make_stardist_segmentation(cfg: Config): + """Factory: return a ``stardist_segmentation`` callable bound to *cfg*.""" + return functools.partial(stardist_segmentation, cfg) + + # def _deep_merge(dst: dict, src: dict): """Recursive dict merge: values in src override dst.""" @@ -1466,4 +1486,4 @@ def create_colored_masks(masks: np.ndarray) -> np.ndarray: if __name__ == "__main__": - asyncio.run(cellpose_segmentation()) + asyncio.run(make_cellpose_segmentation(load_config())()) diff --git a/copilotj/multiagent/research_tools.py b/copilotj/multiagent/research_tools.py index 9c6c6b16..3000fa58 100644 --- a/copilotj/multiagent/research_tools.py +++ b/copilotj/multiagent/research_tools.py @@ -21,7 +21,7 @@ from sklearn.metrics.pairwise import cosine_similarity from tavily import TavilyClient -from copilotj.core.config import Config +from copilotj.core.config import Config, load_config from copilotj.core.embedding import get_embeddings from copilotj.core.kb import ( DEFAULT_INDEX_DIR, @@ -30,9 +30,10 @@ ) from copilotj.multiagent.py_tools import get_project_temp_dir from copilotj.multiagent.tools import execute_python_script +from copilotj.util import proxy_dict __all__ = [ - "ddg_search", + "make_ddg_search", "wikipedia_search", "imagesc_search", "biii_search", @@ -107,6 +108,7 @@ def _fetch_collection( ttl_seconds: int, force_refresh: bool = False, session: requests.Session | None = None, + proxies: dict[str, str] | None = None, ) -> list[Mapping]: """Return the BioImage Model Zoo collection as a list of resource dicts.""" @@ -127,6 +129,8 @@ def _fetch_collection( last_error: Exception | None = None sess = session or requests.Session() + if proxies: + sess.proxies.update(proxies) for url in urls: try: resp = sess.get(url, timeout=30) @@ -207,51 +211,61 @@ def _extract_download_urls(entry: Mapping) -> list[str]: # search tool -def ddg_search( - query: Annotated[str, "Search query string describing what information you need"], - *, - max_results: Annotated[int, "Maximum number of results to return"] = 3, - timeout: Annotated[int, "Timeout for search request (s)"] = 10, - retries: Annotated[int, "Number of retry attempts"] = 2, - images: Annotated[bool, "Set True to return image results instead of text"] = False, - public_only: Annotated[bool, "Bias towards Wikimedia/Flickr/Unsplash if True"] = False, - safesearch: Annotated[str, "DuckDuckGo safesearch: off | moderate | strict"] = "moderate", -) -> str: - attempt = 0 - while attempt <= retries: - try: - with DDGS(timeout=timeout) as ddgs: - if images: - sites = [ - "site:commons.wikimedia.org", - "site:upload.wikimedia.org", - "site:wikimedia.org", - "site:flickr.com", - "site:unsplash.com", - "site:staticflickr.com", - ] - q = f"{query} (photo OR image) {' '.join(sites)}" if public_only else f"{query} (photo OR image)" - items = list(ddgs.images(q, max_results=max_results, safesearch=safesearch)) - if not items: - return "No image results." - out = [] - for i, it in enumerate(items, 1): - img = it.get("image") or it.get("thumbnail") - out.append(f"{i}. {it.get('title')}\nPage: {it.get('url')}\nImage: {img}") - return "\n\n".join(out) - else: - results = ddgs.text(query, max_results=max_results) - if not results: - return "No text results." - out = [] - for i, r in enumerate(results, 1): - out.append(f"{i}. {r.get('title')}\n{r.get('body')}\nSource: {r.get('href')}") - return "\n\n".join(out) - except Exception as e: - attempt += 1 - if attempt > retries: - return f"DDG search failed: {e}" - time.sleep(1 * attempt) +def make_ddg_search(cfg: Config): + """Factory: return a ``ddg_search`` callable bound to *cfg*.""" + proxy = cfg.cij_proxy + + def ddg_search( + query: Annotated[str, "Search query string describing what information you need"], + *, + max_results: Annotated[int, "Maximum number of results to return"] = 3, + timeout: Annotated[int, "Timeout for search request (s)"] = 10, + retries: Annotated[int, "Number of retry attempts"] = 2, + images: Annotated[bool, "Set True to return image results instead of text"] = False, + public_only: Annotated[bool, "Bias towards Wikimedia/Flickr/Unsplash if True"] = False, + safesearch: Annotated[str, "DuckDuckGo safesearch: off | moderate | strict"] = "moderate", + ) -> str: + attempt = 0 + while attempt <= retries: + try: + with DDGS(proxy=proxy, timeout=timeout) as ddgs: + if images: + sites = [ + "site:commons.wikimedia.org", + "site:upload.wikimedia.org", + "site:wikimedia.org", + "site:flickr.com", + "site:unsplash.com", + "site:staticflickr.com", + ] + q = ( + f"{query} (photo OR image) {' '.join(sites)}" + if public_only + else f"{query} (photo OR image)" + ) + items = list(ddgs.images(q, max_results=max_results, safesearch=safesearch)) + if not items: + return "No image results." + out = [] + for i, it in enumerate(items, 1): + img = it.get("image") or it.get("thumbnail") + out.append(f"{i}. {it.get('title')}\nPage: {it.get('url')}\nImage: {img}") + return "\n\n".join(out) + else: + results = ddgs.text(query, max_results=max_results) + if not results: + return "No text results." + out = [] + for i, r in enumerate(results, 1): + out.append(f"{i}. {r.get('title')}\n{r.get('body')}\nSource: {r.get('href')}") + return "\n\n".join(out) + except Exception as e: + attempt += 1 + if attempt > retries: + return f"DDG search failed: {e}" + time.sleep(1 * attempt) + + return ddg_search def make_tavily_search(cfg: Config): @@ -268,7 +282,7 @@ def tavily_search( if not cfg.tavily_api_key: return "Tavily API key not found. Please set COPILOTJ_TAVILY_API_KEY environment variable." - client = TavilyClient(api_key=cfg.tavily_api_key) + client = TavilyClient(api_key=cfg.tavily_api_key, proxies=proxy_dict(cfg)) response = client.search( query=query, @@ -592,6 +606,7 @@ async def imagej_retriever(query: Annotated[str, "Search query string describing def make_deep_research(cfg: Config): """Factory: return a ``deep_research`` callable bound to *cfg*.""" _tavily_search = make_tavily_search(cfg) + _ddg_search = make_ddg_search(cfg) async def deep_research(query: Annotated[str, "The research question to investigate thoroughly"]) -> str: research_report = { @@ -620,7 +635,7 @@ async def deep_research(query: Annotated[str, "The research question to investig research_report["sources_consulted"].append("Wikipedia") research_report["findings"]["reference"] = wiki_results - ddg_results = ddg_search(query, max_results=5) + ddg_results = _ddg_search(query, max_results=5) research_report["sources_consulted"].append("DuckDuckGo Search") research_report["findings"]["web_alternative"] = ddg_results @@ -714,6 +729,7 @@ def bioimage_search_models( cache_dir=Path(_cfg.bioimage_model_zoo_cache).resolve(), ttl_seconds=_cfg.bioimage_model_zoo_cache_ttl, force_refresh=False, + proxies=proxy_dict(_cfg), ) query_text = query.lower().strip() if query else None @@ -843,6 +859,7 @@ def bioimage_download_model( cache_dir=Path(cfg.bioimage_model_zoo_cache).resolve(), ttl_seconds=cfg.bioimage_model_zoo_cache_ttl, force_refresh=False, + proxies=proxy_dict(cfg), ) entry = _get_model(models, model_id) @@ -858,6 +875,9 @@ def bioimage_download_model( result_path = dest_path / filename_guess sess = requests.Session() + _proxies = proxy_dict(cfg) + if _proxies: + sess.proxies.update(_proxies) with sess.get(chosen, stream=True, timeout=60) as resp: resp.raise_for_status() with result_path.open("wb") as handle: @@ -876,7 +896,7 @@ def bioimage_download_model( if __name__ == "__main__": from copilotj.core import load_config - load_config() + cfg = load_config() - result = ddg_search("images of albert einstein", images=True) + result = make_ddg_search(cfg)("images of albert einstein", images=True) print(result) diff --git a/copilotj/server/server.py b/copilotj/server/server.py index b4b2ecf6..2093eb9a 100644 --- a/copilotj/server/server.py +++ b/copilotj/server/server.py @@ -12,6 +12,8 @@ from copilotj.core.config import Config from copilotj.core.kb import ensure_faiss_index_async from copilotj.core.lifecycle import run_cleanup +from copilotj.core.model_info import get_model_capabilities +from copilotj.core.model_listing import list_provider_models from copilotj.server.bridge import Bridge from copilotj.server.threads import Threads @@ -114,6 +116,9 @@ async def on_shutdown(app: web.Application) -> None: app.on_shutdown.append(on_shutdown) async def _ensure_kb(app: web.Application): + from copilotj.core.embedding import configure_download_proxy + + configure_download_proxy(self._cfg) asyncio.ensure_future(ensure_faiss_index_async()) yield @@ -124,7 +129,7 @@ async def _prepare_model_db(app: web.Application): from copilotj.core.model_info import ensure_model_db_async async def _download_and_resolve(): - await ensure_model_db_async() + await ensure_model_db_async(self._cfg) new_cfg = resolve_vision_config(self._cfg) self._cfg = new_cfg self._threads.update_cfg(new_cfg) @@ -180,12 +185,11 @@ async def _on_model_capabilities(self, request: web.Request) -> web.Response: model name. Without the parameter, returns info for the server's configured LLM and VLM. """ - from copilotj.core.model_info import get_model_capabilities # Check arbitrary model via query parameter model_param = request.query.get("model") if model_param: - caps = await asyncio.to_thread(get_model_capabilities, model_param) + caps = await asyncio.to_thread(get_model_capabilities, model_param, self._cfg) return web.json_response( { "model": model_param, @@ -200,7 +204,7 @@ async def _on_model_capabilities(self, request: web.Request) -> web.Response: def _caps_dict(model: str) -> dict | None: if not model: return None - caps = get_model_capabilities(model) + caps = get_model_capabilities(model, cfg) return { "supports_vision": caps.supports_vision, "supports_function_calling": caps.supports_function_calling, @@ -237,20 +241,19 @@ async def _on_list_models(self, request: web.Request) -> web.Response: live. Never returns 5xx — Ollama being unreachable surfaces as ``source: "unreachable"`` with an empty model list. """ - from copilotj.core.model_listing import list_provider_models provider = request.query.get("provider") base_url = request.query.get("base_url") if provider: - result = await list_provider_models(provider, base_url=base_url) + result = await list_provider_models(provider, base_url=base_url, cfg=self._cfg) return web.json_response(result) # Grouped: resolve all supported providers concurrently. Catalog-backed # cloud providers (incl. DeepSeek / OpenRouter) are listed so the model # picker can offer autocomplete for them; Ollama is queried live. providers = ["openai", "anthropic", "gemini", "deepseek", "openrouter", "ollama"] - results = await asyncio.gather(*(list_provider_models(p, base_url=base_url) for p in providers)) + results = await asyncio.gather(*(list_provider_models(p, base_url=base_url, cfg=self._cfg) for p in providers)) return web.json_response({"providers": {r["provider"]: r for r in results}}) diff --git a/copilotj/test/core/test_proxy.py b/copilotj/test/core/test_proxy.py new file mode 100644 index 00000000..66d1db35 --- /dev/null +++ b/copilotj/test/core/test_proxy.py @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: Copyright contributors to the CopilotJ project. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the ``CIJ_PROXY`` download-proxy behavior in :mod:`copilotj.core`. + +Covers :func:`Config.cij_proxy` loading (with no ``os.environ`` mutation) and the +LLM/VLM client proxy precedence, including the ``vlm_proxy`` regression +(``new_vlm_model_client`` previously ignored ``cfg.vlm_proxy``). +""" + +from __future__ import annotations + +import os + +from copilotj.core.config import Config, load_config +from copilotj.core.model_client import new_model_client, new_vlm_model_client + +_PROXY_ENV_KEYS = ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy") + + +# --------------------------------------------------------------------------- # +# Config loading +# --------------------------------------------------------------------------- # + + +def test_cij_proxy_loaded_from_env(monkeypatch, tmp_path): + monkeypatch.setenv("COPILOTJ_HOME", str(tmp_path)) # isolate from any real .env + monkeypatch.setenv("CIJ_PROXY", "http://127.0.0.1:8080") + cfg = load_config() + assert cfg.cij_proxy == "http://127.0.0.1:8080" + + +def test_cij_proxy_unset_defaults_none(monkeypatch, tmp_path): + monkeypatch.setenv("COPILOTJ_HOME", str(tmp_path)) + monkeypatch.delenv("CIJ_PROXY", raising=False) + cfg = load_config() + assert cfg.cij_proxy is None + + +def test_load_config_does_not_mutate_proxy_env(monkeypatch, tmp_path): + """The explicit approach must never write proxy env vars.""" + for key in _PROXY_ENV_KEYS: + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("COPILOTJ_HOME", str(tmp_path)) + monkeypatch.setenv("CIJ_PROXY", "http://127.0.0.1:8080") + load_config() + for key in _PROXY_ENV_KEYS: + assert key not in os.environ + + +# --------------------------------------------------------------------------- # +# LLM / VLM client proxy precedence +# --------------------------------------------------------------------------- # + + +def _capture_resolve(monkeypatch, captured: dict): + """Monkeypatch ``_resolve_client`` to record the proxy kwarg and return a dummy.""" + + def fake_resolve(provider, model, api_key, *, proxy, base_url): # noqa: ARG001 + captured["proxy"] = proxy + captured["provider"] = provider + return object() + + monkeypatch.setattr("copilotj.core.model_client._resolve_client", fake_resolve) + + +def test_llm_client_prefers_llm_proxy(monkeypatch): + captured: dict = {} + _capture_resolve(monkeypatch, captured) + cfg = Config(llm_model="gpt-4o", llm_provider="openai", llm_proxy="http://llm:1", cij_proxy="http://cij:1") + new_model_client(cfg) + assert captured["proxy"] == "http://llm:1" + + +def test_llm_client_falls_back_to_cij_proxy(monkeypatch): + captured: dict = {} + _capture_resolve(monkeypatch, captured) + cfg = Config(llm_model="gpt-4o", llm_provider="openai", cij_proxy="http://cij:1") + new_model_client(cfg) + assert captured["proxy"] == "http://cij:1" + + +def test_vlm_client_prefers_vlm_proxy(monkeypatch): + """Regression: ``cfg.vlm_proxy`` must win (previously ignored — fell back to llm_proxy).""" + captured: dict = {} + _capture_resolve(monkeypatch, captured) + cfg = Config( + llm_model="llm", + vlm_model="gpt-4o", + vlm_provider="openai", + vlm_proxy="http://vlm:1", + llm_proxy="http://llm:1", + cij_proxy="http://cij:1", + ) + new_vlm_model_client(cfg) + assert captured["proxy"] == "http://vlm:1" + + +def test_vlm_client_falls_back_to_llm_then_cij(monkeypatch): + captured: dict = {} + _capture_resolve(monkeypatch, captured) + # vlm_proxy unset, llm_proxy set → llm_proxy wins + cfg = Config( + llm_model="llm", vlm_model="gpt-4o", vlm_provider="openai", llm_proxy="http://llm:1", cij_proxy="http://cij:1" + ) + new_vlm_model_client(cfg) + assert captured["proxy"] == "http://llm:1" + + captured.clear() + # both vlm_proxy and llm_proxy unset → cij_proxy wins + cfg = Config(llm_model="llm", vlm_model="gpt-4o", vlm_provider="openai", cij_proxy="http://cij:1") + new_vlm_model_client(cfg) + assert captured["proxy"] == "http://cij:1" diff --git a/copilotj/test/util/test_proxy.py b/copilotj/test/util/test_proxy.py new file mode 100644 index 00000000..017f67cf --- /dev/null +++ b/copilotj/test/util/test_proxy.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright contributors to the CopilotJ project. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the proxy helpers in :mod:`copilotj.util.env`. + +Covers :func:`proxy_dict` and the scoped :func:`temporary_proxy` context manager +(the mechanism used for the env-only cellpose/stardist weight downloads). +""" + +from __future__ import annotations + +import os + +import pytest + +from copilotj.core.config import Config +from copilotj.util import proxy_dict, temporary_proxy + +_PROXY_ENV_KEYS = ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy") + + +@pytest.fixture +def clean_proxy_env(monkeypatch): + """Remove any pre-existing proxy env vars so tests start from a clean baseline.""" + for key in _PROXY_ENV_KEYS: + monkeypatch.delenv(key, raising=False) + + +# --------------------------------------------------------------------------- # +# proxy_dict +# --------------------------------------------------------------------------- # + + +def test_proxy_dict_returns_dict_when_set(): + proxies = proxy_dict(Config(cij_proxy="http://127.0.0.1:8080")) + assert proxies == {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"} + + +def test_proxy_dict_none_when_unset(): + assert proxy_dict(Config()) is None + + +# --------------------------------------------------------------------------- # +# temporary_proxy (scoped env — the cellpose/stardist path) +# --------------------------------------------------------------------------- # + + +def test_temporary_proxy_sets_env_from_cij(monkeypatch, clean_proxy_env): + cfg = Config(cij_proxy="http://127.0.0.1:8080") + with temporary_proxy(cfg): + assert os.environ["HTTP_PROXY"] == "http://127.0.0.1:8080" + assert os.environ["HTTPS_PROXY"] == "http://127.0.0.1:8080" + assert os.environ["ALL_PROXY"] == "http://127.0.0.1:8080" + # Restored on exit + for key in _PROXY_ENV_KEYS: + assert key not in os.environ + + +def test_temporary_proxy_restores_prior_value(monkeypatch, clean_proxy_env): + monkeypatch.setenv("HTTP_PROXY", "http://pre-existing:1") + monkeypatch.setenv("HTTPS_PROXY", "http://pre-existing:1") + cfg = Config(cij_proxy="http://127.0.0.1:8080") + with temporary_proxy(cfg): + assert os.environ["HTTP_PROXY"] == "http://127.0.0.1:8080" + assert os.environ["HTTP_PROXY"] == "http://pre-existing:1" + + +def test_temporary_proxy_noop_without_proxy(clean_proxy_env): + with temporary_proxy(Config()): + pass + for key in _PROXY_ENV_KEYS: + assert key not in os.environ diff --git a/copilotj/util/env.py b/copilotj/util/env.py index 6fd6a9f2..ec767199 100644 --- a/copilotj/util/env.py +++ b/copilotj/util/env.py @@ -7,7 +7,23 @@ from copilotj.core.config import Config -__all__ = ["temporary_proxy"] +__all__ = ["proxy_dict", "temporary_proxy"] + + +def proxy_dict(cfg: Config) -> dict[str, str] | None: + """Return an explicit ``{"http":..,"https":..}`` proxies dict from ``cfg.cij_proxy``. + + Returns ``None`` when no download proxy is configured, so callers can pass the + result directly to clients that accept ``proxies=None`` (e.g. ``requests``, + ``TavilyClient``). + + >>> from copilotj.core.config import Config + >>> proxy_dict(Config(cij_proxy="http://127.0.0.1:8080")) + {'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'} + >>> proxy_dict(Config()) is None + True + """ + return {"http": cfg.cij_proxy, "https": cfg.cij_proxy} if cfg.cij_proxy else None @contextlib.contextmanager @@ -16,7 +32,7 @@ def temporary_proxy(cfg: Config, default_value: str | None = None): Notes: not thread-safe, use with caution in multi-threaded environments. """ - proxy = default_value or cfg.llm_proxy + proxy = default_value or cfg.llm_proxy or cfg.cij_proxy keys = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"] old_env = {k: os.environ.get(k) for k in keys + [k.lower() for k in keys]} diff --git a/scripts/rag_builder.py b/scripts/rag_builder.py index 0eb3c391..4186ed59 100644 --- a/scripts/rag_builder.py +++ b/scripts/rag_builder.py @@ -111,6 +111,10 @@ def build_rag(data_dir: Path = DEFAULT_DATA_DIR, index_dir: Path = DEFAULT_INDEX raise ValueError(f"No text chunks created from {data_dir}") index_dir.mkdir(parents=True, exist_ok=True) + from copilotj.core.config import load_config + from copilotj.core.embedding import configure_download_proxy + + configure_download_proxy(load_config()) vector_store = FAISS.from_documents(chunks, get_embeddings()) vector_store.save_local(str(index_dir), index_name=INDEX_NAME) jsonl_path = index_dir / f"{INDEX_NAME}.jsonl.gz" From 62793e4a4ef58cd760954df790449933f5e0f3be Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Sun, 21 Jun 2026 10:10:42 +0800 Subject: [PATCH 7/9] Convert cellpose/stardist factories to nested-def closures functools.partial breaks FunctionTool's parameter introspection, so the LLM-visible schema for these tools was wrong. Use the same nested-def closure pattern as make_tavily_search / make_ddg_search: make_*(cfg) defines the inner tool function (closing over cfg) and returns it. The inner Annotated signatures are unchanged. --- copilotj/multiagent/py_tools.py | 413 ++++++++++++++++---------------- 1 file changed, 206 insertions(+), 207 deletions(-) diff --git a/copilotj/multiagent/py_tools.py b/copilotj/multiagent/py_tools.py index b854642b..7dcdc13f 100644 --- a/copilotj/multiagent/py_tools.py +++ b/copilotj/multiagent/py_tools.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 import asyncio -import functools import logging import os from pathlib import Path @@ -53,236 +52,236 @@ def get_project_templates_dir() -> Path: return templates_dir -async def cellpose_segmentation( - cfg: Config, - image_path: Annotated[str | None, "Path to the input image or directory (use forward slashes)"] = None, - model_type: Annotated[str, "Type of model to use (nuclei, cyto, cyto2)"] = "nuclei", - diameter: Annotated[int | None, "Expected cell diameter (if None, will be estimated)"] = None, - channels: Annotated[list[int], "List of channels to use [0,0] for grayscale"] = [0, 0], - flow_threshold: Annotated[float, "Flow error threshold"] = 0.4, - cellprob_threshold: Annotated[float, "Cell probability threshold"] = 0.0, - min_size: Annotated[int, "Minimum cell size"] = 15, - gpu: Annotated[bool, "Whether to use GPU for processing"] = False, - normalize: Annotated[bool, "Whether to normalize image intensities"] = True, - norm_range_low: Annotated[float, "Lower percentile for normalization"] = 1.0, - norm_range_high: Annotated[float, "Upper percentile for normalization"] = 99.8, - save_path: Annotated[ - str | None, - 'Path to save segmentation results (if None, uses original image directory with _cellpose_segmented suffix). Path end must with "/"', - ] = None, -) -> dict[str, str]: - try: - from cellpose import io as cellpose_io - from cellpose import models - - if image_path is None: - return "No image path provided, please provide an image path" - - # Handle directory vs file paths using cellpose's own functions - path_obj = Path(image_path) - if path_obj.is_dir(): - logger.info(f"Directory provided: {image_path}") - # Use cellpose's function to get image files from directory - image_files = await asyncio.to_thread(cellpose_io.get_image_files, image_path, mask_filter="") - if not image_files: - raise FileNotFoundError(f"No image files found in directory: {image_path}") - - # Use the first image file found - actual_image_path = image_files[0] - logger.info(f"Using first image from directory: {actual_image_path}") - else: - actual_image_path = image_path - - # Use cellpose's native image loading function - logger.info(f"Loading image using cellpose.io.imread: {actual_image_path}") - image = await asyncio.to_thread(cellpose_io.imread, actual_image_path) - - if save_path is None: - actual_path = Path(actual_image_path) - save_dir = actual_path.parent - base_name = actual_path.stem - save_path = str(save_dir / f"{base_name}_cellpose_segmented.png") - - # Validate image - if not _validate_image(image): - raise ValueError("Invalid input image") - - # Apply normalization if requested - if normalize: - logger.info(f"Normalizing image using {norm_range_low}-{norm_range_high} percentile range") - image = await asyncio.to_thread(_normalize_image, image, norm_range_low, norm_range_high) - - # Run Cellpose - logger.info(f"Running Cellpose model: {model_type}") - # cellpose downloads pretrained weights via raw urllib (no proxy param) on first - # use, so temporarily expose CIJ_PROXY through env vars for this call only. - with temporary_proxy(cfg): - model = models.CellposeModel(gpu=gpu, model_type=model_type) - - # Run model evaluation in a thread pool to avoid blocking - masks, flows, styles = await asyncio.to_thread( - model.eval, - image, - diameter=diameter, - channels=channels, - flow_threshold=flow_threshold, - cellprob_threshold=cellprob_threshold, - min_size=min_size, - ) - - save_path_obj = Path(save_path) - - # Determine if save_path is a directory or file path - # Consider it a directory if: ends with /, has no suffix, or is an existing directory - is_directory = str(save_path).endswith(("/", "\\")) or not save_path_obj.suffix or save_path_obj.is_dir() - - if is_directory: - # save_path is a directory - save_dir = save_path_obj - save_dir.mkdir(parents=True, exist_ok=True) - # Use image filename as base for output files - base_name = Path(actual_image_path).stem - final_save_path = save_dir / f"{base_name}_cellpose_segmented.png" - else: - # save_path is a file path - save_dir = save_path_obj.parent - save_dir.mkdir(parents=True, exist_ok=True) - base_name = save_path_obj.stem - final_save_path = save_path_obj - - logger.info(f"Save directory: {save_dir}") +def make_cellpose_segmentation(cfg: Config): + """Factory: return a ``cellpose_segmentation`` callable bound to *cfg*.""" + async def cellpose_segmentation( + image_path: Annotated[str | None, "Path to the input image or directory (use forward slashes)"] = None, + model_type: Annotated[str, "Type of model to use (nuclei, cyto, cyto2)"] = "nuclei", + diameter: Annotated[int | None, "Expected cell diameter (if None, will be estimated)"] = None, + channels: Annotated[list[int], "List of channels to use [0,0] for grayscale"] = [0, 0], + flow_threshold: Annotated[float, "Flow error threshold"] = 0.4, + cellprob_threshold: Annotated[float, "Cell probability threshold"] = 0.0, + min_size: Annotated[int, "Minimum cell size"] = 15, + gpu: Annotated[bool, "Whether to use GPU for processing"] = False, + normalize: Annotated[bool, "Whether to normalize image intensities"] = True, + norm_range_low: Annotated[float, "Lower percentile for normalization"] = 1.0, + norm_range_high: Annotated[float, "Upper percentile for normalization"] = 99.8, + save_path: Annotated[ + str | None, + 'Path to save segmentation results (if None, uses original image directory with _cellpose_segmented suffix). Path end must with "/"', + ] = None, + ) -> dict[str, str]: try: - # Save original image - original_path = save_dir / f"{base_name}_original.png" - await asyncio.to_thread(io.imsave, str(original_path), img_as_ubyte(image)) - logger.info(f"Saved original image to {original_path}") - - # Create and save colored segmentation result - colored_masks = await asyncio.to_thread(create_colored_masks, masks) - await asyncio.to_thread(io.imsave, str(final_save_path), colored_masks) - logger.info(f"Saved colored segmentation to {final_save_path}") + from cellpose import io as cellpose_io + from cellpose import models - # Save ROIs - rois_save_path = save_dir / f"{base_name}_rois.zip" - await asyncio.to_thread(cellpose_io.save_rois, masks, str(rois_save_path)) + if image_path is None: + return "No image path provided, please provide an image path" - return { - "original_image": str(original_path), - "segmentation_image": str(final_save_path), - "rois_path": str(rois_save_path), - "num_cells": int(masks.max()), - "save_directory": str(save_dir.resolve()), - } - except Exception as e: - logger.error(f"Error saving results: {str(e)}") - raise + # Handle directory vs file paths using cellpose's own functions + path_obj = Path(image_path) + if path_obj.is_dir(): + logger.info(f"Directory provided: {image_path}") + # Use cellpose's function to get image files from directory + image_files = await asyncio.to_thread(cellpose_io.get_image_files, image_path, mask_filter="") + if not image_files: + raise FileNotFoundError(f"No image files found in directory: {image_path}") + + # Use the first image file found + actual_image_path = image_files[0] + logger.info(f"Using first image from directory: {actual_image_path}") + else: + actual_image_path = image_path + + # Use cellpose's native image loading function + logger.info(f"Loading image using cellpose.io.imread: {actual_image_path}") + image = await asyncio.to_thread(cellpose_io.imread, actual_image_path) + + if save_path is None: + actual_path = Path(actual_image_path) + save_dir = actual_path.parent + base_name = actual_path.stem + save_path = str(save_dir / f"{base_name}_cellpose_segmented.png") + + # Validate image + if not _validate_image(image): + raise ValueError("Invalid input image") + + # Apply normalization if requested + if normalize: + logger.info(f"Normalizing image using {norm_range_low}-{norm_range_high} percentile range") + image = await asyncio.to_thread(_normalize_image, image, norm_range_low, norm_range_high) + + # Run Cellpose + logger.info(f"Running Cellpose model: {model_type}") + # cellpose downloads pretrained weights via raw urllib (no proxy param) on first + # use, so temporarily expose CIJ_PROXY through env vars for this call only. + with temporary_proxy(cfg): + model = models.CellposeModel(gpu=gpu, model_type=model_type) + + # Run model evaluation in a thread pool to avoid blocking + masks, flows, styles = await asyncio.to_thread( + model.eval, + image, + diameter=diameter, + channels=channels, + flow_threshold=flow_threshold, + cellprob_threshold=cellprob_threshold, + min_size=min_size, + ) - except Exception as e: - logger.error(f"Error in cellpose_segmentation: {str(e)}") - raise + save_path_obj = Path(save_path) + # Determine if save_path is a directory or file path + # Consider it a directory if: ends with /, has no suffix, or is an existing directory + is_directory = str(save_path).endswith(("/", "\\")) or not save_path_obj.suffix or save_path_obj.is_dir() -async def stardist_segmentation( - cfg: Config, - image_path: Annotated[str | None, "Path to the input image (use forward slashes)"] = None, - image_array: Annotated[Any | None, "Optional in-memory image; overrides image_path if provided"] = None, - image_type: Annotated[str, "fluorescence | he | tissue | brightfield | phase | dapi | unknown"] = "fluorescence", - model: Annotated[ - str | None, - "Override pretrained model name for 2D (e.g., '2D_versatile_fluo', '2D_versatile_he', '2D_demo')", - ] = None, - p_low: Annotated[float, "Lower percentile for normalization"] = 1.0, - p_high: Annotated[float, "Upper percentile for normalization"] = 99.8, - prob_thresh: Annotated[float | None, "StarDist probability threshold override"] = None, - nms_thresh: Annotated[float | None, "StarDist NMS threshold override"] = None, - save_path: Annotated[ - str | None, "Output PNG path for colored labels; default saves near input or temp/stardist_results" - ] = None, -) -> dict[str, Any]: - try: - if image_array is not None: - img = image_array - inferred_dir = get_project_temp_dir("stardist_segmentation_results") - inferred_name = "in_memory" - # Convert RGB to grayscale if needed - if img.ndim == 3 and img.shape[-1] in (3, 4): - rgb = img[..., :3].astype(np.float32) - img = 0.2126 * rgb[..., 0] + 0.7152 * rgb[..., 1] + 0.0722 * rgb[..., 2] - else: - if image_path is None: - return "No image path provided, please provide an image path" + if is_directory: + # save_path is a directory + save_dir = save_path_obj + save_dir.mkdir(parents=True, exist_ok=True) + # Use image filename as base for output files + base_name = Path(actual_image_path).stem + final_save_path = save_dir / f"{base_name}_cellpose_segmented.png" else: - img = await asyncio.to_thread(load_image, image_path, to_gray=True) - p = Path(image_path) - inferred_dir = get_project_temp_dir("stardist_segmentation_results") - inferred_name = p.stem + # save_path is a file path + save_dir = save_path_obj.parent + save_dir.mkdir(parents=True, exist_ok=True) + base_name = save_path_obj.stem + final_save_path = save_path_obj - if not _validate_image(img): - raise ValueError("Invalid input image for StarDist") + logger.info(f"Save directory: {save_dir}") - ndim = img.ndim - if ndim != 2: - raise ValueError(f"Unsupported image ndim={ndim}; only 2D images are supported") + try: + # Save original image + original_path = save_dir / f"{base_name}_original.png" + await asyncio.to_thread(io.imsave, str(original_path), img_as_ubyte(image)) + logger.info(f"Saved original image to {original_path}") + + # Create and save colored segmentation result + colored_masks = await asyncio.to_thread(create_colored_masks, masks) + await asyncio.to_thread(io.imsave, str(final_save_path), colored_masks) + logger.info(f"Saved colored segmentation to {final_save_path}") + + # Save ROIs + rois_save_path = save_dir / f"{base_name}_rois.zip" + await asyncio.to_thread(cellpose_io.save_rois, masks, str(rois_save_path)) + + return { + "original_image": str(original_path), + "segmentation_image": str(final_save_path), + "rois_path": str(rois_save_path), + "num_cells": int(masks.max()), + "save_directory": str(save_dir.resolve()), + } + except Exception as e: + logger.error(f"Error saving results: {str(e)}") + raise - img_norm = normalize(img.astype(np.float32, copy=False), p_low, p_high) + except Exception as e: + logger.error(f"Error in cellpose_segmentation: {str(e)}") + raise - if model is None: - key = (image_type or "fluorescence").lower() - model_name = "2D_versatile_he" if key in {"he", "tissue", "brightfield"} else "2D_versatile_fluo" - else: - model_name = model - # Lazy: download the standard pretrained model on first use and cache it - # under ~/.stardist/models/ (mirrors how cellpose fetches its - # weights). Keeps the plugin JAR free of bundled model weights. - # stardist/csbdeep fetch weights via raw urllib (no proxy param); expose - # CIJ_PROXY through env vars for the model load only. - with temporary_proxy(cfg): - model_instance = StarDist2D.from_pretrained(model_name) + return cellpose_segmentation - kwargs = {} - if prob_thresh is not None: - kwargs["prob_thresh"] = prob_thresh - if nms_thresh is not None: - kwargs["nms_thresh"] = nms_thresh - labels, details = await asyncio.to_thread(model_instance.predict_instances, img_norm, **kwargs) +def make_stardist_segmentation(cfg: Config): + """Factory: return a ``stardist_segmentation`` callable bound to *cfg*.""" - out_dir = inferred_dir if save_path is None else Path(save_path).parent - out_dir = Path(out_dir) - out_dir.mkdir(parents=True, exist_ok=True) + async def stardist_segmentation( + image_path: Annotated[str | None, "Path to the input image (use forward slashes)"] = None, + image_array: Annotated[Any | None, "Optional in-memory image; overrides image_path if provided"] = None, + image_type: Annotated[ + str, "fluorescence | he | tissue | brightfield | phase | dapi | unknown" + ] = "fluorescence", + model: Annotated[ + str | None, + "Override pretrained model name for 2D (e.g., '2D_versatile_fluo', '2D_versatile_he', '2D_demo')", + ] = None, + p_low: Annotated[float, "Lower percentile for normalization"] = 1.0, + p_high: Annotated[float, "Upper percentile for normalization"] = 99.8, + prob_thresh: Annotated[float | None, "StarDist probability threshold override"] = None, + nms_thresh: Annotated[float | None, "StarDist NMS threshold override"] = None, + save_path: Annotated[ + str | None, "Output PNG path for colored labels; default saves near input or temp/stardist_results" + ] = None, + ) -> dict[str, Any]: + try: + if image_array is not None: + img = image_array + inferred_dir = get_project_temp_dir("stardist_segmentation_results") + inferred_name = "in_memory" + # Convert RGB to grayscale if needed + if img.ndim == 3 and img.shape[-1] in (3, 4): + rgb = img[..., :3].astype(np.float32) + img = 0.2126 * rgb[..., 0] + 0.7152 * rgb[..., 1] + 0.0722 * rgb[..., 2] + else: + if image_path is None: + return "No image path provided, please provide an image path" + else: + img = await asyncio.to_thread(load_image, image_path, to_gray=True) + p = Path(image_path) + inferred_dir = get_project_temp_dir("stardist_segmentation_results") + inferred_name = p.stem + + if not _validate_image(img): + raise ValueError("Invalid input image for StarDist") + + ndim = img.ndim + if ndim != 2: + raise ValueError(f"Unsupported image ndim={ndim}; only 2D images are supported") + + img_norm = normalize(img.astype(np.float32, copy=False), p_low, p_high) + + if model is None: + key = (image_type or "fluorescence").lower() + model_name = "2D_versatile_he" if key in {"he", "tissue", "brightfield"} else "2D_versatile_fluo" + else: + model_name = model + # Lazy: download the standard pretrained model on first use and cache it + # under ~/.stardist/models/ (mirrors how cellpose fetches its + # weights). Keeps the plugin JAR free of bundled model weights. + # stardist/csbdeep fetch weights via raw urllib (no proxy param); expose + # CIJ_PROXY through env vars for the model load only. + with temporary_proxy(cfg): + model_instance = StarDist2D.from_pretrained(model_name) - original_path = out_dir / f"{inferred_name}_stardist_input.png" - await asyncio.to_thread(io.imsave, original_path, (img_norm * 255).astype(np.uint8)) + kwargs = {} + if prob_thresh is not None: + kwargs["prob_thresh"] = prob_thresh + if nms_thresh is not None: + kwargs["nms_thresh"] = nms_thresh - colored = await asyncio.to_thread(create_colored_masks, labels.astype(np.int32)) - seg_png = out_dir / f"{inferred_name}_stardist_labels.png" if save_path is None else Path(save_path) - await asyncio.to_thread(io.imsave, seg_png, colored) + labels, details = await asyncio.to_thread(model_instance.predict_instances, img_norm, **kwargs) - labels_tif = out_dir / f"{inferred_name}_stardist_labels.tif" - await asyncio.to_thread(io.imsave, labels_tif, labels.astype(np.uint16)) + out_dir = inferred_dir if save_path is None else Path(save_path).parent + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) - return { - "original_image": str(original_path), - "segmentation_image": str(seg_png), - "labels_path": str(labels_tif), - "num_instances": int(labels.max()), - "model_name": model_name, - } + original_path = out_dir / f"{inferred_name}_stardist_input.png" + await asyncio.to_thread(io.imsave, original_path, (img_norm * 255).astype(np.uint8)) - except Exception as e: - logger.error(f"Error in stardist_segmentation: {e}") - raise + colored = await asyncio.to_thread(create_colored_masks, labels.astype(np.int32)) + seg_png = out_dir / f"{inferred_name}_stardist_labels.png" if save_path is None else Path(save_path) + await asyncio.to_thread(io.imsave, seg_png, colored) + labels_tif = out_dir / f"{inferred_name}_stardist_labels.tif" + await asyncio.to_thread(io.imsave, labels_tif, labels.astype(np.uint16)) -def make_cellpose_segmentation(cfg: Config): - """Factory: return a ``cellpose_segmentation`` callable bound to *cfg*.""" - return functools.partial(cellpose_segmentation, cfg) + return { + "original_image": str(original_path), + "segmentation_image": str(seg_png), + "labels_path": str(labels_tif), + "num_instances": int(labels.max()), + "model_name": model_name, + } + except Exception as e: + logger.error(f"Error in stardist_segmentation: {e}") + raise -def make_stardist_segmentation(cfg: Config): - """Factory: return a ``stardist_segmentation`` callable bound to *cfg*.""" - return functools.partial(stardist_segmentation, cfg) + return stardist_segmentation # From 759b47c9b560a490ee595d0e44325725082d3dd4 Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Sun, 21 Jun 2026 10:10:46 +0800 Subject: [PATCH 8/9] Update VLM proxy test for new precedence new_vlm_model_client now passes proxy=cfg.vlm_proxy or cfg.cij_proxy, so VLM prefers cij_proxy over llm_proxy. Update the test to assert the effective chain (vlm_proxy > cij_proxy > llm_proxy; llm_proxy remains a last-resort fallback via _new_model_client). --- copilotj/test/core/test_proxy.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/copilotj/test/core/test_proxy.py b/copilotj/test/core/test_proxy.py index 66d1db35..62284cea 100644 --- a/copilotj/test/core/test_proxy.py +++ b/copilotj/test/core/test_proxy.py @@ -97,18 +97,23 @@ def test_vlm_client_prefers_vlm_proxy(monkeypatch): assert captured["proxy"] == "http://vlm:1" -def test_vlm_client_falls_back_to_llm_then_cij(monkeypatch): +def test_vlm_client_prefers_cij_over_llm(monkeypatch): + """VLM proxy prefers ``cij_proxy`` over ``llm_proxy``. + + Effective chain: ``vlm_proxy > cij_proxy > llm_proxy`` (``llm_proxy`` remains a + last-resort fallback via ``_new_model_client``). + """ captured: dict = {} _capture_resolve(monkeypatch, captured) - # vlm_proxy unset, llm_proxy set → llm_proxy wins + # vlm_proxy unset, both llm_proxy and cij_proxy set → cij_proxy wins cfg = Config( llm_model="llm", vlm_model="gpt-4o", vlm_provider="openai", llm_proxy="http://llm:1", cij_proxy="http://cij:1" ) new_vlm_model_client(cfg) - assert captured["proxy"] == "http://llm:1" + assert captured["proxy"] == "http://cij:1" captured.clear() - # both vlm_proxy and llm_proxy unset → cij_proxy wins - cfg = Config(llm_model="llm", vlm_model="gpt-4o", vlm_provider="openai", cij_proxy="http://cij:1") + # vlm_proxy and cij_proxy unset, llm_proxy set → llm_proxy as last resort + cfg = Config(llm_model="llm", vlm_model="gpt-4o", vlm_provider="openai", llm_proxy="http://llm:1") new_vlm_model_client(cfg) - assert captured["proxy"] == "http://cij:1" + assert captured["proxy"] == "http://llm:1" From 69995a462ecc6932fb4930c22030b850165e3086 Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Sun, 21 Jun 2026 10:21:51 +0800 Subject: [PATCH 9/9] VLM proxy excludes llm_proxy (vlm_proxy > cij_proxy) new_vlm_model_client passes allow_llm_proxy_fallback=False so _new_model_client does not fall back to cfg.llm_proxy on the VLM path. Effective VLM chain: vlm_proxy > cij_proxy. Update the test and the DEVELOPMENT.md config table to match. --- DEVELOPMENT.md | 2 +- copilotj/core/model_client/__init__.py | 7 ++++++- copilotj/test/core/test_proxy.py | 12 ++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 15b8e818..2b398ae1 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -228,7 +228,7 @@ Note: Ollama models generally do not support image input. If image understanding | `COPILOTJ_VLM_BASE_URL` | Override API endpoint for the VLM (falls back to `COPILOTJ_LLM_BASE_URL`) | | `COPILOTJ_VLM_PROVIDER` | Explicit VLM provider (optional; falls back to `COPILOTJ_LLM_PROVIDER`) | | `COPILOTJ_LLM_PROXY` | HTTP/HTTPS proxy for LLM outbound API requests | -| `COPILOTJ_VLM_PROXY` | HTTP/HTTPS proxy for VLM outbound requests (falls back to `COPILOTJ_LLM_PROXY`) | +| `COPILOTJ_VLM_PROXY` | HTTP/HTTPS proxy for VLM outbound requests (falls back to `CIJ_PROXY`) | | `CIJ_PROXY` | Global download proxy for HuggingFace, model DB, bioimage zoo, research/Tavily/DDGS, cellpose/stardist; fallback for the LLM/VLM proxies | | `COPILOTJ_TAVILY_API_KEY` | Tavily API key for live web search | | `COPILOTJ_KB_AUTOSAVE` | Set `1` to auto-ingest dialog summaries into the knowledge bank | diff --git a/copilotj/core/model_client/__init__.py b/copilotj/core/model_client/__init__.py index b4a8d88e..333addba 100644 --- a/copilotj/core/model_client/__init__.py +++ b/copilotj/core/model_client/__init__.py @@ -69,6 +69,7 @@ def new_vlm_model_client(cfg: Config) -> ModelClient: base_url=cfg.vlm_base_url, cfg=cfg, provider=cfg.vlm_provider or cfg.llm_provider, + allow_llm_proxy_fallback=False, ) @@ -127,9 +128,13 @@ def _new_model_client( base_url: str | None = None, cfg: Config | None = None, provider: str | None = None, + allow_llm_proxy_fallback: bool = True, ) -> ModelClient: cfg = cfg or load_config() - proxy = proxy or cfg.llm_proxy or cfg.cij_proxy + if allow_llm_proxy_fallback: + proxy = proxy or cfg.llm_proxy or cfg.cij_proxy + else: + proxy = proxy or cfg.cij_proxy # If provider is explicitly given, use it directly. if provider is not None: diff --git a/copilotj/test/core/test_proxy.py b/copilotj/test/core/test_proxy.py index 62284cea..8bd20398 100644 --- a/copilotj/test/core/test_proxy.py +++ b/copilotj/test/core/test_proxy.py @@ -97,12 +97,8 @@ def test_vlm_client_prefers_vlm_proxy(monkeypatch): assert captured["proxy"] == "http://vlm:1" -def test_vlm_client_prefers_cij_over_llm(monkeypatch): - """VLM proxy prefers ``cij_proxy`` over ``llm_proxy``. - - Effective chain: ``vlm_proxy > cij_proxy > llm_proxy`` (``llm_proxy`` remains a - last-resort fallback via ``_new_model_client``). - """ +def test_vlm_client_excludes_llm_proxy(monkeypatch): + """VLM proxy precedence is ``vlm_proxy > cij_proxy`` — ``llm_proxy`` is never used.""" captured: dict = {} _capture_resolve(monkeypatch, captured) # vlm_proxy unset, both llm_proxy and cij_proxy set → cij_proxy wins @@ -113,7 +109,7 @@ def test_vlm_client_prefers_cij_over_llm(monkeypatch): assert captured["proxy"] == "http://cij:1" captured.clear() - # vlm_proxy and cij_proxy unset, llm_proxy set → llm_proxy as last resort + # vlm_proxy and cij_proxy unset, llm_proxy set → no proxy (llm_proxy excluded) cfg = Config(llm_model="llm", vlm_model="gpt-4o", vlm_provider="openai", llm_proxy="http://llm:1") new_vlm_model_client(cfg) - assert captured["proxy"] == "http://llm:1" + assert captured["proxy"] is None