Merge branch 'main' into bigrefactor
# Conflicts: # package-lock.json # packages/agent/package.json
This commit is contained in:
2
.github/APPROVED_CONTRIBUTORS
vendored
2
.github/APPROVED_CONTRIBUTORS
vendored
@@ -198,3 +198,5 @@ louis030195 pr
|
||||
technocidal pr
|
||||
|
||||
pandada8 pr
|
||||
|
||||
npupko issue
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@ packages/*/dist-firefox/
|
||||
.vscode/
|
||||
.zed/
|
||||
.idea/
|
||||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
447
package-lock.json
generated
447
package-lock.json
generated
@@ -16,7 +16,6 @@
|
||||
"packages/coding-agent/examples/extensions/sandbox"
|
||||
],
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.5",
|
||||
"@mariozechner/pi-coding-agent": "^0.30.2",
|
||||
"get-east-asian-width": "^1.4.0"
|
||||
},
|
||||
@@ -27,6 +26,7 @@
|
||||
"@typescript/native-preview": "7.0.0-dev.20260120.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"shx": "^0.4.0",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.9.2"
|
||||
@@ -55,15 +55,6 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sandbox-runtime/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sandbox-runtime/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
@@ -923,6 +914,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -940,6 +934,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -957,6 +954,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -974,6 +974,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1660,6 +1663,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1676,6 +1682,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1692,6 +1701,9 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1708,6 +1720,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1724,6 +1739,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1765,19 +1783,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/jiti": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz",
|
||||
"integrity": "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"std-env": "^3.10.0",
|
||||
"yoctocolors": "^2.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/mini-lit": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/mini-lit/-/mini-lit-0.2.1.tgz",
|
||||
@@ -1968,6 +1973,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1988,6 +1996,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2008,6 +2019,9 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2028,6 +2042,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2048,6 +2065,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2279,6 +2299,9 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2300,6 +2323,9 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2321,6 +2347,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2342,6 +2371,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2363,6 +2395,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2384,6 +2419,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2554,9 +2592,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
|
||||
"integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
|
||||
"integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2567,9 +2605,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
|
||||
"integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz",
|
||||
"integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2580,9 +2618,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
|
||||
"integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz",
|
||||
"integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2593,9 +2631,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
|
||||
"integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz",
|
||||
"integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2606,9 +2644,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
|
||||
"integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz",
|
||||
"integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2619,9 +2657,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
|
||||
"integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz",
|
||||
"integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2632,12 +2670,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
|
||||
"integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz",
|
||||
"integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2645,12 +2686,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
|
||||
"integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz",
|
||||
"integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2658,12 +2702,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2671,12 +2718,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2684,12 +2734,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2697,12 +2750,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2710,12 +2766,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2723,12 +2782,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2736,12 +2798,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2749,12 +2814,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2762,12 +2830,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2775,12 +2846,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2788,12 +2862,15 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2801,9 +2878,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
|
||||
"integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz",
|
||||
"integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2814,9 +2891,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
|
||||
"integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz",
|
||||
"integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2827,9 +2904,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
|
||||
"integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz",
|
||||
"integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2840,9 +2917,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
|
||||
"integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz",
|
||||
"integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2853,9 +2930,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2866,9 +2943,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
|
||||
"integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz",
|
||||
"integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3666,6 +3743,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3682,6 +3762,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3698,6 +3781,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3714,6 +3800,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -4582,12 +4671,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
@@ -5110,9 +5199,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
|
||||
"integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.7.tgz",
|
||||
"integrity": "sha512-Yh7/7rQuMXICNr0oMYDR2yHP6oUvmQsTToFeOWj/kIDhAwQ+c4Ol/lbcwOmEM5OHYQmh6S6EQSQ1sljCKP36bQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5670,9 +5759,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -5795,6 +5884,15 @@
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/katex/node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/koffi": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.1.tgz",
|
||||
@@ -5951,6 +6049,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5971,6 +6072,9 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5991,6 +6095,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6011,6 +6118,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -6115,9 +6225,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.3.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
|
||||
"integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
|
||||
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -6657,9 +6767,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
|
||||
"integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -6932,9 +7042,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
|
||||
"integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz",
|
||||
"integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -6947,31 +7057,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.2",
|
||||
"@rollup/rollup-android-arm64": "4.60.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.2",
|
||||
"@rollup/rollup-darwin-x64": "4.60.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.2",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.2",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.2",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.2",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.2",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.2",
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.3",
|
||||
"@rollup/rollup-android-arm64": "4.60.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.3",
|
||||
"@rollup/rollup-darwin-x64": "4.60.3",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.3",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.3",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.3",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.3",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.3",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.3",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.3",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -7247,6 +7357,7 @@
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
@@ -8143,22 +8254,10 @@
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz",
|
||||
"integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
@@ -8175,10 +8274,10 @@
|
||||
},
|
||||
"packages/agent": {
|
||||
"name": "@mariozechner/pi-agent-core",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.73.0",
|
||||
"ignore": "^7.0.5",
|
||||
"typebox": "^1.1.24",
|
||||
"yaml": "^2.8.2"
|
||||
@@ -8211,7 +8310,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.91.1",
|
||||
@@ -8257,13 +8356,12 @@
|
||||
},
|
||||
"packages/coding-agent": {
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.2",
|
||||
"@mariozechner/pi-agent-core": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-tui": "^0.72.1",
|
||||
"@mariozechner/pi-agent-core": "^0.73.0",
|
||||
"@mariozechner/pi-ai": "^0.73.0",
|
||||
"@mariozechner/pi-tui": "^0.73.0",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"chalk": "^5.5.0",
|
||||
"cli-highlight": "^2.1.11",
|
||||
@@ -8273,6 +8371,7 @@
|
||||
"glob": "^13.0.1",
|
||||
"hosted-git-info": "^9.0.2",
|
||||
"ignore": "^7.0.5",
|
||||
"jiti": "^2.7.0",
|
||||
"marked": "^15.0.12",
|
||||
"minimatch": "^10.2.3",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
@@ -8304,25 +8403,25 @@
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/custom-provider-anthropic": {
|
||||
"name": "pi-extension-custom-provider-anthropic",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": {
|
||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||
"version": "0.72.1"
|
||||
"version": "0.73.0"
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/sandbox": {
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/with-deps": {
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -8358,7 +8457,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@mariozechner/pi-tui",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
@@ -8380,12 +8479,12 @@
|
||||
},
|
||||
"packages/web-ui": {
|
||||
"name": "@mariozechner/pi-web-ui",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lmstudio/sdk": "^1.5.0",
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-tui": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.73.0",
|
||||
"@mariozechner/pi-tui": "^0.73.0",
|
||||
"docx-preview": "^0.3.7",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide": "^0.544.0",
|
||||
@@ -8407,7 +8506,7 @@
|
||||
},
|
||||
"packages/web-ui/example": {
|
||||
"name": "pi-web-ui-example",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"dependencies": {
|
||||
"@mariozechner/mini-lit": "^0.2.0",
|
||||
"@mariozechner/pi-ai": "file:../../ai",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@typescript/native-preview": "7.0.0-dev.20260120.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.9.2",
|
||||
"shx": "^0.4.0"
|
||||
@@ -48,7 +49,6 @@
|
||||
},
|
||||
"version": "0.0.3",
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.5",
|
||||
"@mariozechner/pi-coding-agent": "^0.30.2",
|
||||
"get-east-asian-width": "^1.4.0"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-agent-core",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
@@ -17,7 +17,7 @@
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.73.0",
|
||||
"ignore": "^7.0.5",
|
||||
"typebox": "^1.1.24",
|
||||
"yaml": "^2.8.2"
|
||||
|
||||
@@ -2,13 +2,27 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI Responses reasoning text streaming for LM Studio and other compatible providers that emit `response.reasoning_text.delta` events ([#4191](https://github.com/badlogic/pi-mono/pull/4191) by [@yaanfpv](https://github.com/yaanfpv)).
|
||||
- Fixed OpenAI Codex OAuth refresh failures writing directly to stderr while the TUI is active ([#4141](https://github.com/badlogic/pi-mono/issues/4141)).
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Switched the built-in `xiaomi` provider endpoint from Token Plan AMS (`https://token-plan-ams.xiaomimimo.com/anthropic`) to API billing (`https://api.xiaomimimo.com/anthropic`). `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users still on Token Plan must move to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var.
|
||||
- Switched the built-in `xiaomi` provider endpoint from Token Plan AMS (`https://token-plan-ams.xiaomimimo.com/anthropic`) to API billing (`https://api.xiaomimimo.com/anthropic`). `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users still on Token Plan must move to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
|
||||
### Added
|
||||
|
||||
- Added Xiaomi MiMo Token Plan regional providers with per-region env vars: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), and `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`).
|
||||
- Added Xiaomi MiMo Token Plan regional providers with per-region env vars: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), and `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`) ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
- Added `registerSessionResourceCleanup()` and `cleanupSessionResources()` so providers can register cleanup hooks for session-scoped resources.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed generated OpenAI-compatible model metadata for Qwen 3.5/3.6 and MiniMax M2.7 to match models.dev and OpenCode Go ([#4110](https://github.com/badlogic/pi-mono/pull/4110) by [@jsynowiec](https://github.com/jsynowiec)).
|
||||
- Fixed Bedrock Converse thinking effort mapping to preserve native `xhigh` for Claude Opus 4.7.
|
||||
- Fixed OpenAI Codex Responses WebSocket transport to fall back to SSE when setup fails before streaming starts, and attach transport diagnostics to the assistant message ([#4133](https://github.com/badlogic/pi-mono/issues/4133)).
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -857,15 +857,17 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||
const kimiModels = data["kimi-for-coding"].models as Record<string, ModelsDevModel>;
|
||||
const hasCanonicalModel = Object.prototype.hasOwnProperty.call(kimiModels, "kimi-for-coding");
|
||||
|
||||
const kimiAliases = new Set(["k2p5", "k2p6"]);
|
||||
|
||||
for (const [modelId, model] of Object.entries(kimiModels)) {
|
||||
const m = model as ModelsDevModel;
|
||||
if (m.tool_call !== true) continue;
|
||||
// models.dev still exposes deprecated "k2p5" in some snapshots.
|
||||
// Normalize to the canonical model id and drop duplicates when canonical exists.
|
||||
if (modelId === "k2p5" && hasCanonicalModel) continue;
|
||||
// models.dev may expose versioned aliases (e.g. k2p5/k2p6).
|
||||
// Normalize aliases to the canonical model id and drop duplicates when canonical exists.
|
||||
if (kimiAliases.has(modelId) && hasCanonicalModel) continue;
|
||||
|
||||
const normalizedId = modelId === "k2p5" ? "kimi-for-coding" : modelId;
|
||||
const normalizedName = modelId === "k2p5" ? "Kimi For Coding" : m.name || normalizedId;
|
||||
const normalizedId = kimiAliases.has(modelId) ? "kimi-for-coding" : modelId;
|
||||
const normalizedName = kimiAliases.has(modelId) ? "Kimi For Coding" : m.name || normalizedId;
|
||||
|
||||
models.push({
|
||||
id: normalizedId,
|
||||
|
||||
@@ -34,6 +34,8 @@ export type {
|
||||
OAuthProviderId,
|
||||
OAuthProviderInfo,
|
||||
OAuthProviderInterface,
|
||||
OAuthSelectOption,
|
||||
OAuthSelectPrompt,
|
||||
} from "./utils/oauth/types.js";
|
||||
export * from "./utils/overflow.js";
|
||||
export * from "./utils/typebox-helpers.js";
|
||||
|
||||
@@ -5819,24 +5819,6 @@ export const MODELS = {
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
"kimi-coding": {
|
||||
"k2p6": {
|
||||
id: "k2p6",
|
||||
name: "Kimi K2.6",
|
||||
api: "anthropic-messages",
|
||||
provider: "kimi-coding",
|
||||
baseUrl: "https://api.kimi.com/coding",
|
||||
headers: {"User-Agent":"KimiCLI/1.5"},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 32768,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"kimi-for-coding": {
|
||||
id: "kimi-for-coding",
|
||||
name: "Kimi For Coding",
|
||||
@@ -9207,8 +9189,8 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.21,
|
||||
output: 0.7899999999999999,
|
||||
input: 0.27,
|
||||
output: 0.95,
|
||||
cacheRead: 0.13,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -9279,13 +9261,13 @@ export const MODELS = {
|
||||
thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"},
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.435,
|
||||
output: 0.87,
|
||||
cacheRead: 0.003625,
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 384000,
|
||||
contextWindow: 131000,
|
||||
maxTokens: 131000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"essentialai/rnj-1-instruct": {
|
||||
id: "essentialai/rnj-1-instruct",
|
||||
@@ -9318,7 +9300,7 @@ export const MODELS = {
|
||||
cacheRead: 0.024999999999999998,
|
||||
cacheWrite: 0.08333333333333334,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"google/gemini-2.0-flash-lite-001": {
|
||||
@@ -11807,13 +11789,13 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.08,
|
||||
output: 0.28,
|
||||
input: 0.09,
|
||||
output: 0.44999999999999996,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 40960,
|
||||
maxTokens: 16384,
|
||||
maxTokens: 20000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3-30b-a3b-instruct-2507": {
|
||||
id: "qwen/qwen3-30b-a3b-instruct-2507",
|
||||
@@ -12232,13 +12214,13 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.1625,
|
||||
output: 1.3,
|
||||
cacheRead: 0,
|
||||
input: 0.15,
|
||||
output: 1,
|
||||
cacheRead: 0.049999999999999996,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.5-397b-a17b": {
|
||||
id: "qwen/qwen3.5-397b-a17b",
|
||||
@@ -12342,6 +12324,23 @@ export const MODELS = {
|
||||
contextWindow: 262144,
|
||||
maxTokens: 81920,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.6-35b-a3b": {
|
||||
id: "qwen/qwen3.6-35b-a3b",
|
||||
name: "Qwen: Qwen3.6 35B A3B",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 1,
|
||||
cacheRead: 0.049999999999999996,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.6-flash": {
|
||||
id: "qwen/qwen3.6-flash",
|
||||
name: "Qwen: Qwen3.6 Flash",
|
||||
@@ -14573,23 +14572,6 @@ export const MODELS = {
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"moonshotai/kimi-k2-0905": {
|
||||
id: "moonshotai/kimi-k2-0905",
|
||||
name: "Kimi K2 0905",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.6,
|
||||
output: 2.5,
|
||||
cacheRead: 0.3,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"moonshotai/kimi-k2-thinking": {
|
||||
id: "moonshotai/kimi-k2-thinking",
|
||||
name: "Kimi K2 Thinking",
|
||||
@@ -16813,7 +16795,7 @@ export const MODELS = {
|
||||
} satisfies Model<"openai-completions">,
|
||||
"glm-5v-turbo": {
|
||||
id: "glm-5v-turbo",
|
||||
name: "glm-5v-turbo",
|
||||
name: "GLM-5V-Turbo",
|
||||
api: "openai-completions",
|
||||
provider: "zai",
|
||||
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
||||
|
||||
@@ -352,7 +352,7 @@ function buildRequestBody(
|
||||
model: model.id,
|
||||
store: false,
|
||||
stream: true,
|
||||
instructions: context.systemPrompt,
|
||||
instructions: context.systemPrompt || "You are a helpful assistant.",
|
||||
input: messages,
|
||||
text: { verbosity: options?.textVerbosity || "low" },
|
||||
include: ["reasoning.encrypted_content"],
|
||||
|
||||
@@ -354,6 +354,16 @@ export async function processResponsesStream<TApi extends Api>(
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (event.type === "response.reasoning_text.delta") {
|
||||
if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
|
||||
currentBlock.thinking += event.delta;
|
||||
stream.push({
|
||||
type: "thinking_delta",
|
||||
contentIndex: blockIndex(),
|
||||
delta: event.delta,
|
||||
partial: output,
|
||||
});
|
||||
}
|
||||
} else if (event.type === "response.content_part.added") {
|
||||
if (currentItem?.type === "message") {
|
||||
currentItem.content = currentItem.content || [];
|
||||
@@ -429,7 +439,9 @@ export async function processResponsesStream<TApi extends Api>(
|
||||
const item = event.item;
|
||||
|
||||
if (item.type === "reasoning" && currentBlock?.type === "thinking") {
|
||||
currentBlock.thinking = item.summary?.map((s) => s.text).join("\n\n") || "";
|
||||
const summaryText = item.summary?.map((s) => s.text).join("\n\n") || "";
|
||||
const contentText = item.content?.map((c) => c.text).join("\n\n") || "";
|
||||
currentBlock.thinking = summaryText || contentText || currentBlock.thinking;
|
||||
currentBlock.thinkingSignature = JSON.stringify(item);
|
||||
stream.push({
|
||||
type: "thinking_end",
|
||||
|
||||
@@ -30,7 +30,7 @@ const SCOPE = "openid profile email offline_access";
|
||||
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
||||
|
||||
type TokenSuccess = { type: "success"; access: string; refresh: string; expires: number };
|
||||
type TokenFailure = { type: "failed" };
|
||||
type TokenFailure = { type: "failed"; message: string; status?: number };
|
||||
type TokenResult = TokenSuccess | TokenFailure;
|
||||
|
||||
type JwtPayload = {
|
||||
@@ -108,8 +108,11 @@ async function exchangeAuthorizationCode(
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
console.error("[openai-codex] code->token failed:", response.status, text);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
status: response.status,
|
||||
message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`,
|
||||
};
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
@@ -119,8 +122,10 @@ async function exchangeAuthorizationCode(
|
||||
};
|
||||
|
||||
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
||||
console.error("[openai-codex] token response missing fields:", json);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -145,8 +150,11 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
console.error("[openai-codex] Token refresh failed:", response.status, text);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
status: response.status,
|
||||
message: `OpenAI Codex token refresh failed (${response.status}): ${text || response.statusText}`,
|
||||
};
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
@@ -156,8 +164,10 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
||||
};
|
||||
|
||||
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
||||
console.error("[openai-codex] Token refresh response missing fields:", json);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
message: `OpenAI Codex token refresh response missing fields: ${JSON.stringify(json)}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -167,8 +177,10 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
||||
expires: Date.now() + json.expires_in * 1000,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[openai-codex] Token refresh error:", error);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
message: `OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,12 +270,7 @@ function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
|
||||
waitForCode: () => waitForCodePromise,
|
||||
});
|
||||
})
|
||||
.on("error", (err: NodeJS.ErrnoException) => {
|
||||
console.error(
|
||||
`[openai-codex] Failed to bind http://${CALLBACK_HOST}:1455 (`,
|
||||
err.code,
|
||||
") Falling back to manual paste.",
|
||||
);
|
||||
.on("error", (_err: NodeJS.ErrnoException) => {
|
||||
settleWait?.(null);
|
||||
resolve({
|
||||
close: () => {
|
||||
@@ -386,7 +393,7 @@ export async function loginOpenAICodex(options: {
|
||||
|
||||
const tokenResult = await exchangeAuthorizationCode(code, verifier);
|
||||
if (tokenResult.type !== "success") {
|
||||
throw new Error("Token exchange failed");
|
||||
throw new Error(tokenResult.message);
|
||||
}
|
||||
|
||||
const accountId = getAccountId(tokenResult.access);
|
||||
@@ -411,7 +418,7 @@ export async function loginOpenAICodex(options: {
|
||||
export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {
|
||||
const result = await refreshAccessToken(refreshToken);
|
||||
if (result.type !== "success") {
|
||||
throw new Error("Failed to refresh OpenAI Codex token");
|
||||
throw new Error(result.message);
|
||||
}
|
||||
|
||||
const accountId = getAccountId(result.access);
|
||||
|
||||
@@ -23,11 +23,23 @@ export type OAuthAuthInfo = {
|
||||
instructions?: string;
|
||||
};
|
||||
|
||||
export type OAuthSelectOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type OAuthSelectPrompt = {
|
||||
message: string;
|
||||
options: OAuthSelectOption[];
|
||||
};
|
||||
|
||||
export interface OAuthLoginCallbacks {
|
||||
onAuth: (info: OAuthAuthInfo) => void;
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||
onProgress?: (message: string) => void;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
/** Show an interactive selector and return the selected option id, or undefined on cancel. */
|
||||
onSelect?: (prompt: OAuthSelectPrompt) => Promise<string | undefined>;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
|
||||
32
packages/ai/test/openai-codex-oauth.test.ts
Normal file
32
packages/ai/test/openai-codex-oauth.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { refreshOpenAICodexToken } from "../src/utils/oauth/openai-codex.js";
|
||||
|
||||
describe("OpenAI Codex OAuth", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("does not write token refresh failures to stderr", async () => {
|
||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (): Promise<Response> => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Could not validate your token. Please try signing in again.",
|
||||
type: "invalid_request_error",
|
||||
},
|
||||
}),
|
||||
{ status: 401, statusText: "Unauthorized", headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(refreshOpenAICodexToken("invalid-refresh-token")).rejects.toThrow(
|
||||
/OpenAI Codex token refresh failed \(401\).*Could not validate your token/,
|
||||
);
|
||||
expect(consoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,13 +2,27 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)).
|
||||
- Fixed pending tool results not updating in the live TUI after toggling thinking block visibility while the tool is running ([#4167](https://github.com/badlogic/pi-mono/issues/4167)).
|
||||
- Fixed `/copy` reporting success on Linux without writing the clipboard on Wayland-only compositors (Hyprland, Niri, ...) by skipping the X11-only native addon on Linux and routing through `wl-copy`/`xclip`/`xsel` instead ([#4177](https://github.com/badlogic/pi-mono/issues/4177)).
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
### New Features
|
||||
|
||||
- **Xiaomi MiMo API billing and regional Token Plan providers** - `xiaomi` now uses API billing, with separate `xiaomi-token-plan-{cn,ams,sgp}` providers. See [docs/providers.md#api-keys](docs/providers.md#api-keys) and [README.md#providers--models](README.md#providers--models). ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode))
|
||||
- **Incremental bash output streaming** - Bash tool output now appears while commands run instead of only after completion. ([#4145](https://github.com/badlogic/pi-mono/issues/4145))
|
||||
- **Compact read rendering** - Interactive `read` output for Pi docs, context files, and skills is collapsed by default and shows selected line ranges.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Switched the built-in `xiaomi` provider from Token Plan AMS to Xiaomi's API billing endpoint, and renamed its `/login` display from "Xiaomi MiMo Token Plan" to "Xiaomi MiMo". `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users on Token Plan should switch to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var.
|
||||
- Switched the built-in `xiaomi` provider from Token Plan AMS to Xiaomi's API billing endpoint, and renamed its `/login` display from "Xiaomi MiMo Token Plan" to "Xiaomi MiMo". `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users on Token Plan should switch to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
|
||||
### Added
|
||||
|
||||
- Added three Xiaomi MiMo Token Plan regional providers visible in `/login`: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`). Each defaults to `mimo-v2.5-pro`.
|
||||
- Added three Xiaomi MiMo Token Plan regional providers visible in `/login`: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`). Each defaults to `mimo-v2.5-pro` ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -16,7 +30,14 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed generated OpenAI-compatible model metadata for Qwen 3.5/3.6 and MiniMax M2.7, so those models work through the built-in provider catalog ([#4110](https://github.com/badlogic/pi-mono/pull/4110) by [@jsynowiec](https://github.com/jsynowiec)).
|
||||
- Fixed Bedrock Claude Opus 4.7 `xhigh` thinking requests by preserving the provider's native effort value.
|
||||
- Fixed OpenAI Codex WebSocket transport to fall back to SSE when setup fails before streaming starts, and surface transport diagnostics in the assistant message ([#4133](https://github.com/badlogic/pi-mono/issues/4133)).
|
||||
- Fixed OpenAI Codex WebSocket transport keeping `--print` and JSON mode processes alive after the response by closing cached WebSocket sessions during session shutdown ([#4103](https://github.com/badlogic/pi-mono/issues/4103)).
|
||||
- Fixed compact `read` tool calls to render directly and include selected line ranges in interactive output.
|
||||
- Fixed interactive sessions to exit when terminal input is lost instead of continuing in a broken state.
|
||||
- Fixed bash tool output to stream incrementally while commands run instead of waiting for command completion ([#4145](https://github.com/badlogic/pi-mono/issues/4145)).
|
||||
- Fixed selector and autocomplete fuzzy ranking to prioritize exact matches.
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider-anthropic",
|
||||
"private": true,
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||
"private": true,
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"private": true,
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"private": true,
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"type": "module",
|
||||
"piConfig": {
|
||||
@@ -38,10 +38,9 @@
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.2",
|
||||
"@mariozechner/pi-agent-core": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-tui": "^0.72.1",
|
||||
"@mariozechner/pi-agent-core": "^0.73.0",
|
||||
"@mariozechner/pi-ai": "^0.73.0",
|
||||
"@mariozechner/pi-tui": "^0.73.0",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"chalk": "^5.5.0",
|
||||
"cli-highlight": "^2.1.11",
|
||||
@@ -51,6 +50,7 @@
|
||||
"glob": "^13.0.1",
|
||||
"hosted-git-info": "^9.0.2",
|
||||
"ignore": "^7.0.5",
|
||||
"jiti": "^2.7.0",
|
||||
"marked": "^15.0.12",
|
||||
"minimatch": "^10.2.3",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
||||
@@ -122,6 +122,11 @@ export function parseArgs(args: string[]): Args {
|
||||
}
|
||||
} else if (arg === "--print" || arg === "-p") {
|
||||
result.print = true;
|
||||
const next = args[i + 1];
|
||||
if (next !== undefined && !next.startsWith("@") && (!next.startsWith("-") || next.startsWith("---"))) {
|
||||
result.messages.push(next);
|
||||
i++;
|
||||
}
|
||||
} else if (arg === "--export" && i + 1 < args.length) {
|
||||
result.export = args[++i];
|
||||
} else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) {
|
||||
|
||||
@@ -204,6 +204,10 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.tree-role-skill {
|
||||
color: var(--customMessageLabel);
|
||||
}
|
||||
|
||||
.tree-role-assistant {
|
||||
color: var(--success);
|
||||
}
|
||||
@@ -383,7 +387,8 @@
|
||||
}
|
||||
|
||||
.user-message:hover .copy-link-btn,
|
||||
.assistant-message:hover .copy-link-btn {
|
||||
.assistant-message:hover .copy-link-btn,
|
||||
.skill-user-entry:hover .copy-link-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -786,6 +791,45 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Skill invocation - matches compaction style (clickable, collapsed by default) */
|
||||
.skill-invocation {
|
||||
background: var(--customMessageBg);
|
||||
border-radius: 4px;
|
||||
padding: var(--line-height);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.skill-invocation-label {
|
||||
color: var(--customMessageLabel);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.skill-invocation-collapsed {
|
||||
color: var(--customMessageText);
|
||||
}
|
||||
|
||||
.skill-invocation-content {
|
||||
display: none;
|
||||
color: var(--customMessageText);
|
||||
margin-top: var(--line-height);
|
||||
}
|
||||
|
||||
.skill-invocation.expanded .skill-invocation-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.skill-invocation.expanded .skill-invocation-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.skill-invocation + .user-message {
|
||||
margin-top: var(--line-height);
|
||||
}
|
||||
|
||||
.skill-user-entry {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Branch summary */
|
||||
.branch-summary {
|
||||
background: var(--customMessageBg);
|
||||
|
||||
@@ -313,6 +313,22 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a skill block from message text.
|
||||
* Returns null if the text doesn't contain a skill block.
|
||||
* Matches the format: <skill name="..." location="...">\n...\n</skill>\n\nuser message
|
||||
*/
|
||||
function parseSkillBlock(text) {
|
||||
const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
name: match[1],
|
||||
location: match[2],
|
||||
content: match[3],
|
||||
userMessage: match[4]?.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function getSearchableText(entry, label) {
|
||||
const parts = [];
|
||||
if (label) parts.push(label);
|
||||
@@ -613,7 +629,16 @@
|
||||
case 'message': {
|
||||
const msg = entry.message;
|
||||
if (msg.role === 'user') {
|
||||
const content = truncate(normalize(extractContent(msg.content)));
|
||||
const rawContent = extractContent(msg.content);
|
||||
const skillBlock = parseSkillBlock(rawContent);
|
||||
if (skillBlock) {
|
||||
let treeHtml = labelHtml + `<span class="tree-role-skill">skill:</span> ${escapeHtml(skillBlock.name)}`;
|
||||
if (skillBlock.userMessage) {
|
||||
treeHtml += ` · <span class="tree-role-user">user:</span> ${escapeHtml(truncate(normalize(skillBlock.userMessage)))}`;
|
||||
}
|
||||
return treeHtml;
|
||||
}
|
||||
const content = truncate(normalize(rawContent));
|
||||
return labelHtml + `<span class="tree-role-user">user:</span> ${escapeHtml(content)}`;
|
||||
}
|
||||
if (msg.role === 'assistant') {
|
||||
@@ -1140,8 +1165,46 @@
|
||||
const msg = entry.message;
|
||||
|
||||
if (msg.role === 'user') {
|
||||
let html = `<div class="user-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
||||
const content = msg.content;
|
||||
const text = typeof content === 'string' ? content :
|
||||
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
||||
const skillBlock = parseSkillBlock(text);
|
||||
|
||||
if (skillBlock) {
|
||||
// Collect images from content array
|
||||
const images = Array.isArray(content) ? content.filter(c => c.type === 'image') : [];
|
||||
const hasUserContent = skillBlock.userMessage || images.length > 0;
|
||||
let html = `<div class="skill-user-entry" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
||||
|
||||
// Skill invocation (collapsed by default, click to expand)
|
||||
html += `<div class="skill-invocation" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
||||
<div class="skill-invocation-label">[skill] ${escapeHtml(skillBlock.name)}</div>
|
||||
<div class="skill-invocation-collapsed">${escapeHtml(skillBlock.name)} (click to expand)</div>
|
||||
<div class="skill-invocation-content markdown-content">${safeMarkedParse(skillBlock.content)}</div>
|
||||
</div>`;
|
||||
|
||||
// User message (separate block if present)
|
||||
if (hasUserContent) {
|
||||
html += '<div class="user-message">';
|
||||
if (images.length > 0) {
|
||||
html += '<div class="message-images">';
|
||||
for (const img of images) {
|
||||
html += `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="message-image" />`;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
if (skillBlock.userMessage) {
|
||||
html += `<div class="markdown-content">${safeMarkedParse(skillBlock.userMessage)}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// No skill block - normal user message
|
||||
let html = `<div class="user-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
const images = content.filter(c => c.type === 'image');
|
||||
@@ -1154,8 +1217,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const text = typeof content === 'string' ? content :
|
||||
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
||||
if (text.trim()) {
|
||||
html += `<div class="markdown-content">${safeMarkedParse(text)}</div>`;
|
||||
}
|
||||
@@ -1716,6 +1777,9 @@
|
||||
document.querySelectorAll('.compaction').forEach(el => {
|
||||
el.classList.toggle('expanded', toolOutputsExpanded);
|
||||
});
|
||||
document.querySelectorAll('.skill-invocation').forEach(el => {
|
||||
el.classList.toggle('expanded', toolOutputsExpanded);
|
||||
});
|
||||
};
|
||||
|
||||
const attachHeaderHandlers = () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Extension loader - loads TypeScript extension modules using jiti.
|
||||
*
|
||||
* Uses @mariozechner/jiti fork with virtualModules support for compiled Bun binaries.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
@@ -9,12 +8,12 @@ import { createRequire } from "node:module";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "@mariozechner/jiti";
|
||||
import * as _bundledPiAgentCore from "@mariozechner/pi-agent-core";
|
||||
import * as _bundledPiAi from "@mariozechner/pi-ai";
|
||||
import * as _bundledPiAiOauth from "@mariozechner/pi-ai/oauth";
|
||||
import type { KeyId } from "@mariozechner/pi-tui";
|
||||
import * as _bundledPiTui from "@mariozechner/pi-tui";
|
||||
import { createJiti } from "jiti/static";
|
||||
// Static imports of packages that extensions may use.
|
||||
// These MUST be static so Bun bundles them into the compiled binary.
|
||||
// The virtualModules option then makes them available to extensions.
|
||||
|
||||
@@ -213,6 +213,13 @@ function formatValidationPath(error: TLocalizedValidationError): string {
|
||||
return path || "root";
|
||||
}
|
||||
|
||||
/** Strip `//` line comments and trailing commas from JSON, leaving string literals untouched. */
|
||||
function stripJsonComments(input: string): string {
|
||||
return input
|
||||
.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
|
||||
.replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : ""));
|
||||
}
|
||||
|
||||
/** Provider override config (baseUrl, compat) without request auth/headers */
|
||||
interface ProviderOverride {
|
||||
baseUrl?: string;
|
||||
@@ -450,7 +457,7 @@ export class ModelRegistry {
|
||||
|
||||
try {
|
||||
const content = readFileSync(modelsJsonPath, "utf-8");
|
||||
const parsed = JSON.parse(content) as unknown;
|
||||
const parsed = JSON.parse(stripJsonComments(content)) as unknown;
|
||||
|
||||
if (!validateModelsConfig.Check(parsed)) {
|
||||
const errors =
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { createWriteStream, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { Container, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { spawn } from "child_process";
|
||||
@@ -18,17 +15,10 @@ import {
|
||||
untrackDetachedChildPid,
|
||||
} from "../../utils/shell.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { OutputAccumulator } from "./output-accumulator.js";
|
||||
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";
|
||||
|
||||
/**
|
||||
* Generate a unique temp file path for bash output.
|
||||
*/
|
||||
function getTempFilePath(): string {
|
||||
const id = randomBytes(8).toString("hex");
|
||||
return join(tmpdir(), `pi-bash-${id}.log`);
|
||||
}
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.js";
|
||||
|
||||
const bashSchema = Type.Object({
|
||||
command: Type.String({ description: "Bash command to execute" }),
|
||||
@@ -161,6 +151,7 @@ export interface BashToolOptions {
|
||||
}
|
||||
|
||||
const BASH_PREVIEW_LINES = 5;
|
||||
const BASH_UPDATE_THROTTLE_MS = 100;
|
||||
|
||||
type BashRenderState = {
|
||||
startedAt: number | undefined;
|
||||
@@ -292,118 +283,119 @@ export function createBashToolDefinition(
|
||||
) {
|
||||
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
||||
const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
|
||||
const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" });
|
||||
let updateTimer: NodeJS.Timeout | undefined;
|
||||
let updateDirty = false;
|
||||
let lastUpdateAt = 0;
|
||||
|
||||
const emitOutputUpdate = () => {
|
||||
if (!onUpdate || !updateDirty) return;
|
||||
updateDirty = false;
|
||||
lastUpdateAt = Date.now();
|
||||
const snapshot = output.snapshot({ persistIfTruncated: true });
|
||||
onUpdate({
|
||||
content: [{ type: "text", text: snapshot.content || "" }],
|
||||
details: {
|
||||
truncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,
|
||||
fullOutputPath: snapshot.fullOutputPath,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clearUpdateTimer = () => {
|
||||
if (updateTimer) {
|
||||
clearTimeout(updateTimer);
|
||||
updateTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleOutputUpdate = () => {
|
||||
if (!onUpdate) return;
|
||||
updateDirty = true;
|
||||
const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);
|
||||
if (delay <= 0) {
|
||||
clearUpdateTimer();
|
||||
emitOutputUpdate();
|
||||
return;
|
||||
}
|
||||
updateTimer ??= setTimeout(() => {
|
||||
updateTimer = undefined;
|
||||
emitOutputUpdate();
|
||||
}, delay);
|
||||
};
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate({ content: [], details: undefined });
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let tempFilePath: string | undefined;
|
||||
let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
|
||||
let totalBytes = 0;
|
||||
const chunks: Buffer[] = [];
|
||||
let chunksBytes = 0;
|
||||
const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
|
||||
|
||||
const ensureTempFile = () => {
|
||||
if (tempFilePath) return;
|
||||
tempFilePath = getTempFilePath();
|
||||
tempFileStream = createWriteStream(tempFilePath);
|
||||
for (const chunk of chunks) tempFileStream.write(chunk);
|
||||
};
|
||||
const handleData = (data: Buffer) => {
|
||||
output.append(data);
|
||||
scheduleOutputUpdate();
|
||||
};
|
||||
|
||||
const handleData = (data: Buffer) => {
|
||||
totalBytes += data.length;
|
||||
// Start writing to a temp file once output exceeds the in-memory threshold.
|
||||
if (totalBytes > DEFAULT_MAX_BYTES) {
|
||||
ensureTempFile();
|
||||
}
|
||||
// Write to temp file if we have one.
|
||||
if (tempFileStream) tempFileStream.write(data);
|
||||
// Keep a rolling buffer of recent output for tail truncation.
|
||||
chunks.push(data);
|
||||
chunksBytes += data.length;
|
||||
// Trim old chunks if the rolling buffer grows too large.
|
||||
while (chunksBytes > maxChunksBytes && chunks.length > 1) {
|
||||
const removed = chunks.shift()!;
|
||||
chunksBytes -= removed.length;
|
||||
}
|
||||
// Stream partial output using the rolling tail buffer.
|
||||
if (onUpdate) {
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
const fullText = fullBuffer.toString("utf-8");
|
||||
const truncation = truncateTail(fullText);
|
||||
if (truncation.truncated) {
|
||||
ensureTempFile();
|
||||
}
|
||||
onUpdate({
|
||||
content: [{ type: "text", text: truncation.content || "" }],
|
||||
details: {
|
||||
truncation: truncation.truncated ? truncation : undefined,
|
||||
fullOutputPath: tempFilePath,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const finishOutput = async () => {
|
||||
output.finish();
|
||||
clearUpdateTimer();
|
||||
emitOutputUpdate();
|
||||
const snapshot = output.snapshot({ persistIfTruncated: true });
|
||||
await output.closeTempFile();
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
ops.exec(spawnContext.command, spawnContext.cwd, {
|
||||
onData: handleData,
|
||||
signal,
|
||||
timeout,
|
||||
env: spawnContext.env,
|
||||
})
|
||||
.then(({ exitCode }) => {
|
||||
// Combine the rolling buffer chunks.
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
const fullOutput = fullBuffer.toString("utf-8");
|
||||
// Apply tail truncation for the final display payload.
|
||||
const truncation = truncateTail(fullOutput);
|
||||
if (truncation.truncated) {
|
||||
ensureTempFile();
|
||||
}
|
||||
// Close temp file stream before building the final result.
|
||||
if (tempFileStream) tempFileStream.end();
|
||||
let outputText = truncation.content || "(no output)";
|
||||
let details: BashToolDetails | undefined;
|
||||
if (truncation.truncated) {
|
||||
// Build truncation details and an actionable notice.
|
||||
details = { truncation, fullOutputPath: tempFilePath };
|
||||
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
||||
const endLine = truncation.totalLines;
|
||||
if (truncation.lastLinePartial) {
|
||||
// Edge case: the last line alone is larger than the byte limit.
|
||||
const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
|
||||
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
|
||||
} else if (truncation.truncatedBy === "lines") {
|
||||
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
|
||||
} else {
|
||||
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
|
||||
}
|
||||
}
|
||||
if (exitCode !== 0 && exitCode !== null) {
|
||||
outputText += `\n\nCommand exited with code ${exitCode}`;
|
||||
reject(new Error(outputText));
|
||||
} else {
|
||||
resolve({ content: [{ type: "text", text: outputText }], details });
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
// Close temp file stream and include buffered output in the error message.
|
||||
if (tempFileStream) tempFileStream.end();
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
let output = fullBuffer.toString("utf-8");
|
||||
if (err.message === "aborted") {
|
||||
if (output) output += "\n\n";
|
||||
output += "Command aborted";
|
||||
reject(new Error(output));
|
||||
} else if (err.message.startsWith("timeout:")) {
|
||||
const timeoutSecs = err.message.split(":")[1];
|
||||
if (output) output += "\n\n";
|
||||
output += `Command timed out after ${timeoutSecs} seconds`;
|
||||
reject(new Error(output));
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
const formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = "(no output)") => {
|
||||
const truncation = snapshot.truncation;
|
||||
let text = snapshot.content || emptyText;
|
||||
let details: BashToolDetails | undefined;
|
||||
if (truncation.truncated) {
|
||||
details = { truncation, fullOutputPath: snapshot.fullOutputPath };
|
||||
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
||||
const endLine = truncation.totalLines;
|
||||
if (truncation.lastLinePartial) {
|
||||
const lastLineSize = formatSize(output.getLastLineBytes());
|
||||
text += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;
|
||||
} else if (truncation.truncatedBy === "lines") {
|
||||
text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;
|
||||
} else {
|
||||
text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;
|
||||
}
|
||||
}
|
||||
return { text, details };
|
||||
};
|
||||
|
||||
const appendStatus = (text: string, status: string) => `${text ? `${text}\n\n` : ""}${status}`;
|
||||
|
||||
try {
|
||||
let exitCode: number | null;
|
||||
try {
|
||||
const result = await ops.exec(spawnContext.command, spawnContext.cwd, {
|
||||
onData: handleData,
|
||||
signal,
|
||||
timeout,
|
||||
env: spawnContext.env,
|
||||
});
|
||||
});
|
||||
exitCode = result.exitCode;
|
||||
} catch (err) {
|
||||
const snapshot = await finishOutput();
|
||||
const { text } = formatOutput(snapshot, "");
|
||||
if (err instanceof Error && err.message === "aborted") {
|
||||
throw new Error(appendStatus(text, "Command aborted"));
|
||||
}
|
||||
if (err instanceof Error && err.message.startsWith("timeout:")) {
|
||||
const timeoutSecs = err.message.split(":")[1];
|
||||
throw new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const snapshot = await finishOutput();
|
||||
const { text: outputText, details } = formatOutput(snapshot);
|
||||
if (exitCode !== 0 && exitCode !== null) {
|
||||
throw new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));
|
||||
}
|
||||
return { content: [{ type: "text", text: outputText }], details };
|
||||
} finally {
|
||||
clearUpdateTimer();
|
||||
}
|
||||
},
|
||||
renderCall(args, _theme, context) {
|
||||
const state = context.state;
|
||||
|
||||
216
packages/coding-agent/src/core/tools/output-accumulator.ts
Normal file
216
packages/coding-agent/src/core/tools/output-accumulator.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { createWriteStream, type WriteStream } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.js";
|
||||
|
||||
export interface OutputAccumulatorOptions {
|
||||
maxLines?: number;
|
||||
maxBytes?: number;
|
||||
tempFilePrefix?: string;
|
||||
}
|
||||
|
||||
export interface OutputSnapshot {
|
||||
content: string;
|
||||
truncation: TruncationResult;
|
||||
fullOutputPath?: string;
|
||||
}
|
||||
|
||||
function defaultTempFilePath(prefix: string): string {
|
||||
const id = randomBytes(8).toString("hex");
|
||||
return join(tmpdir(), `${prefix}-${id}.log`);
|
||||
}
|
||||
|
||||
function byteLength(text: string): number {
|
||||
return Buffer.byteLength(text, "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementally tracks streaming output with bounded memory.
|
||||
*
|
||||
* Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded
|
||||
* tail for display snapshots, and opens a temp file when the full output needs
|
||||
* to be preserved.
|
||||
*/
|
||||
export class OutputAccumulator {
|
||||
private readonly maxLines: number;
|
||||
private readonly maxBytes: number;
|
||||
private readonly maxRollingBytes: number;
|
||||
private readonly tempFilePrefix: string;
|
||||
private readonly decoder = new TextDecoder();
|
||||
|
||||
private rawChunks: Buffer[] = [];
|
||||
private tailText = "";
|
||||
private tailBytes = 0;
|
||||
private tailStartsAtLineBoundary = true;
|
||||
private totalRawBytes = 0;
|
||||
private totalDecodedBytes = 0;
|
||||
private totalLines = 1;
|
||||
private currentLineBytes = 0;
|
||||
private finished = false;
|
||||
|
||||
private tempFilePath: string | undefined;
|
||||
private tempFileStream: WriteStream | undefined;
|
||||
|
||||
constructor(options: OutputAccumulatorOptions = {}) {
|
||||
this.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
||||
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||
this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
|
||||
this.tempFilePrefix = options.tempFilePrefix ?? "pi-output";
|
||||
}
|
||||
|
||||
append(data: Buffer): void {
|
||||
if (this.finished) {
|
||||
throw new Error("Cannot append to a finished output accumulator");
|
||||
}
|
||||
|
||||
this.totalRawBytes += data.length;
|
||||
this.appendDecodedText(this.decoder.decode(data, { stream: true }));
|
||||
|
||||
if (this.tempFileStream || this.shouldUseTempFile()) {
|
||||
this.ensureTempFile();
|
||||
this.tempFileStream?.write(data);
|
||||
} else if (data.length > 0) {
|
||||
this.rawChunks.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
finish(): void {
|
||||
if (this.finished) {
|
||||
return;
|
||||
}
|
||||
this.finished = true;
|
||||
this.appendDecodedText(this.decoder.decode());
|
||||
if (this.shouldUseTempFile()) {
|
||||
this.ensureTempFile();
|
||||
}
|
||||
}
|
||||
|
||||
snapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot {
|
||||
const tailTruncation = truncateTail(this.getSnapshotText(), {
|
||||
maxLines: this.maxLines,
|
||||
maxBytes: this.maxBytes,
|
||||
});
|
||||
const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;
|
||||
const truncatedBy = truncated
|
||||
? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines"))
|
||||
: null;
|
||||
const truncation: TruncationResult = {
|
||||
...tailTruncation,
|
||||
truncated,
|
||||
truncatedBy,
|
||||
totalLines: this.totalLines,
|
||||
totalBytes: this.totalDecodedBytes,
|
||||
maxLines: this.maxLines,
|
||||
maxBytes: this.maxBytes,
|
||||
};
|
||||
|
||||
if (options.persistIfTruncated && truncation.truncated) {
|
||||
this.ensureTempFile();
|
||||
}
|
||||
|
||||
return {
|
||||
content: truncation.content,
|
||||
truncation,
|
||||
fullOutputPath: this.tempFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
async closeTempFile(): Promise<void> {
|
||||
if (!this.tempFileStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = this.tempFileStream;
|
||||
this.tempFileStream = undefined;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onError = (error: Error) => {
|
||||
stream.off("finish", onFinish);
|
||||
reject(error);
|
||||
};
|
||||
const onFinish = () => {
|
||||
stream.off("error", onError);
|
||||
resolve();
|
||||
};
|
||||
stream.once("error", onError);
|
||||
stream.once("finish", onFinish);
|
||||
stream.end();
|
||||
});
|
||||
}
|
||||
|
||||
getLastLineBytes(): number {
|
||||
return this.currentLineBytes;
|
||||
}
|
||||
|
||||
private appendDecodedText(text: string): void {
|
||||
if (text.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes = byteLength(text);
|
||||
this.totalDecodedBytes += bytes;
|
||||
this.tailText += text;
|
||||
this.tailBytes += bytes;
|
||||
if (this.tailBytes > this.maxRollingBytes * 2) {
|
||||
this.trimTail();
|
||||
}
|
||||
|
||||
let newlines = 0;
|
||||
let lastNewline = -1;
|
||||
for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
|
||||
newlines++;
|
||||
lastNewline = i;
|
||||
}
|
||||
if (newlines === 0) {
|
||||
this.currentLineBytes += bytes;
|
||||
} else {
|
||||
this.totalLines += newlines;
|
||||
this.currentLineBytes = byteLength(text.slice(lastNewline + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private trimTail(): void {
|
||||
const buffer = Buffer.from(this.tailText, "utf-8");
|
||||
if (buffer.length <= this.maxRollingBytes) {
|
||||
this.tailBytes = buffer.length;
|
||||
return;
|
||||
}
|
||||
|
||||
let start = buffer.length - this.maxRollingBytes;
|
||||
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {
|
||||
start++;
|
||||
}
|
||||
|
||||
this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a;
|
||||
this.tailText = buffer.subarray(start).toString("utf-8");
|
||||
this.tailBytes = byteLength(this.tailText);
|
||||
}
|
||||
|
||||
private getSnapshotText(): string {
|
||||
if (this.tailStartsAtLineBoundary) {
|
||||
return this.tailText;
|
||||
}
|
||||
|
||||
const firstNewline = this.tailText.indexOf("\n");
|
||||
return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
|
||||
}
|
||||
|
||||
private shouldUseTempFile(): boolean {
|
||||
return (
|
||||
this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines
|
||||
);
|
||||
}
|
||||
|
||||
private ensureTempFile(): void {
|
||||
if (this.tempFilePath) {
|
||||
return;
|
||||
}
|
||||
this.tempFilePath = defaultTempFilePath(this.tempFilePrefix);
|
||||
this.tempFileStream = createWriteStream(this.tempFilePath);
|
||||
for (const chunk of this.rawChunks) {
|
||||
this.tempFileStream.write(chunk);
|
||||
}
|
||||
this.rawChunks = [];
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
showAuth(url: string, instructions?: string): void {
|
||||
this.contentContainer.clear();
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
|
||||
const linkedUrl = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`;
|
||||
this.contentContainer.addChild(new Text(theme.fg("accent", linkedUrl), 1, 0));
|
||||
|
||||
const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
|
||||
const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type Message,
|
||||
type Model,
|
||||
type OAuthProviderId,
|
||||
type OAuthSelectPrompt,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import type {
|
||||
AutocompleteItem,
|
||||
@@ -2632,6 +2633,7 @@ export class InteractiveMode {
|
||||
|
||||
switch (event.type) {
|
||||
case "agent_start":
|
||||
this.pendingTools.clear();
|
||||
if (this.settingsManager.getShowTerminalProgress()) {
|
||||
this.ui.terminal.setProgress(true);
|
||||
}
|
||||
@@ -3099,6 +3101,7 @@ export class InteractiveMode {
|
||||
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
||||
): void {
|
||||
this.pendingTools.clear();
|
||||
const renderedPendingTools = new Map<string, ToolExecutionComponent>();
|
||||
|
||||
if (options.updateFooter) {
|
||||
this.footer.invalidate();
|
||||
@@ -3140,16 +3143,16 @@ export class InteractiveMode {
|
||||
}
|
||||
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
||||
} else {
|
||||
this.pendingTools.set(content.id, component);
|
||||
renderedPendingTools.set(content.id, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (message.role === "toolResult") {
|
||||
// Match tool results to pending tool components
|
||||
const component = this.pendingTools.get(message.toolCallId);
|
||||
const component = renderedPendingTools.get(message.toolCallId);
|
||||
if (component) {
|
||||
component.updateResult(message);
|
||||
this.pendingTools.delete(message.toolCallId);
|
||||
renderedPendingTools.delete(message.toolCallId);
|
||||
}
|
||||
} else {
|
||||
// All other messages use standard rendering
|
||||
@@ -3157,7 +3160,9 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
this.pendingTools.clear();
|
||||
for (const [toolCallId, component] of renderedPendingTools) {
|
||||
this.pendingTools.set(toolCallId, component);
|
||||
}
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
@@ -4645,6 +4650,34 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
private showOAuthLoginSelect(dialog: LoginDialogComponent, prompt: OAuthSelectPrompt): Promise<string | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const restoreDialog = () => {
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(dialog);
|
||||
this.ui.setFocus(dialog);
|
||||
this.ui.requestRender();
|
||||
};
|
||||
const labels = prompt.options.map((option) => option.label);
|
||||
const selector = new ExtensionSelectorComponent(
|
||||
prompt.message,
|
||||
labels,
|
||||
(optionLabel) => {
|
||||
restoreDialog();
|
||||
resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
|
||||
},
|
||||
() => {
|
||||
restoreDialog();
|
||||
resolve(undefined);
|
||||
},
|
||||
);
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(selector);
|
||||
this.ui.setFocus(selector);
|
||||
this.ui.requestRender();
|
||||
});
|
||||
}
|
||||
|
||||
private async showLoginDialog(providerId: string, providerName: string): Promise<void> {
|
||||
const providerInfo = this.session.modelRegistry.authStorage
|
||||
.getOAuthProviders()
|
||||
@@ -4722,6 +4755,8 @@ export class InteractiveMode {
|
||||
dialog.showProgress(message);
|
||||
},
|
||||
|
||||
onSelect: (prompt: OAuthSelectPrompt) => this.showOAuthLoginSelect(dialog, prompt),
|
||||
|
||||
onManualCodeInput: () => manualCodePromise,
|
||||
|
||||
signal: dialog.signal,
|
||||
|
||||
@@ -35,11 +35,20 @@ function emitOsc52(text: string): boolean {
|
||||
export async function copyToClipboard(text: string): Promise<void> {
|
||||
let copied = false;
|
||||
|
||||
const p = platform();
|
||||
|
||||
// Prefer direct clipboard writes. Emitting OSC 52 first can make terminals
|
||||
// write the same native clipboard concurrently with the addon, and very large
|
||||
// OSC 52 payloads can desynchronize terminal rendering.
|
||||
//
|
||||
// On Linux, skip the native addon. The underlying `clipboard-rs` crate is
|
||||
// X11-only and does not retain selection ownership after `set_text`
|
||||
// resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even
|
||||
// some X11 sessions the call resolves successfully without populating the
|
||||
// clipboard. The platform tools below (wl-copy, xclip, xsel) properly
|
||||
// daemonize and keep ownership.
|
||||
try {
|
||||
if (clipboard) {
|
||||
if (clipboard && p !== "linux") {
|
||||
await clipboard.setText(text);
|
||||
copied = true;
|
||||
}
|
||||
@@ -52,7 +61,6 @@ export async function copyToClipboard(text: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = platform();
|
||||
const options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] };
|
||||
|
||||
if (!copied) {
|
||||
|
||||
@@ -43,6 +43,21 @@ describe("parseArgs", () => {
|
||||
const result = parseArgs(["-p"]);
|
||||
expect(result.print).toBe(true);
|
||||
});
|
||||
|
||||
test("parses prompt after -p even when it starts with YAML frontmatter", () => {
|
||||
const prompt = "---\ntitle: hello\n---\nSay hi.";
|
||||
const result = parseArgs(["-p", prompt]);
|
||||
expect(result.print).toBe(true);
|
||||
expect(result.messages).toEqual([prompt]);
|
||||
expect(result.unknownFlags.size).toBe(0);
|
||||
});
|
||||
|
||||
test("does not consume options after -p as prompts", () => {
|
||||
const result = parseArgs(["-p", "--provider", "openai", "Say hi."]);
|
||||
expect(result.print).toBe(true);
|
||||
expect(result.provider).toBe("openai");
|
||||
expect(result.messages).toEqual(["Say hi."]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("--continue flag", () => {
|
||||
|
||||
40
packages/coding-agent/test/export-html-skill-block.test.ts
Normal file
40
packages/coding-agent/test/export-html-skill-block.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("export HTML skill block rendering", () => {
|
||||
const templateJs = readFileSync(new URL("../src/core/export-html/template.js", import.meta.url), "utf-8");
|
||||
|
||||
it("strips skill wrapper XML from user message rendering", () => {
|
||||
// Skill commands store a structural wrapper in the raw user message:
|
||||
// <skill name="..." location="...">\n...\n</skill>\n\nactual prompt
|
||||
// The export renderer must detect that wrapper and render only the user-visible prompt,
|
||||
// not the Pi-generated <skill>...</skill> XML tags.
|
||||
expect(templateJs).toMatch(/parseSkillBlock/);
|
||||
expect(templateJs).toMatch(/skillBlock\.userMessage/);
|
||||
});
|
||||
|
||||
it("renders skill invocation and user message as separate sibling blocks", () => {
|
||||
// The skill block and user message should render as separate entry-level elements,
|
||||
// matching the TUI layout where SkillInvocationMessageComponent and
|
||||
// UserMessageComponent are siblings, not nested.
|
||||
expect(templateJs).toMatch(/skill-invocation/);
|
||||
|
||||
// When a skill block has a userMessage, the user-message div must be emitted
|
||||
// as a separate block after the skill-invocation div, containing the user-authored text.
|
||||
// Verify the code checks hasUserContent so the user-message div is only omitted
|
||||
// when the skill block has no user prompt and no images.
|
||||
expect(templateJs).toMatch(/hasUserContent/);
|
||||
});
|
||||
|
||||
it("renders skill content as markdown, not raw text", () => {
|
||||
// The skill block body is markdown (from the SKILL.md file).
|
||||
// It should be rendered through safeMarkedParse, not escaped as raw text.
|
||||
expect(templateJs).toMatch(/safeMarkedParse\(skillBlock\.content\)/);
|
||||
});
|
||||
|
||||
it("shows skill name and user message in the sidebar tree", () => {
|
||||
// The sidebar tree should display both the skill name and the user prompt,
|
||||
// not just one or the other.
|
||||
expect(templateJs).toMatch(/tree-role-skill/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { AssistantMessage, ToolResultMessage, Usage } from "@mariozechner/pi-ai";
|
||||
import { Container, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { beforeAll, describe, expect, test, vi } from "vitest";
|
||||
import type { AgentSessionEvent } from "../../../src/core/agent-session.js";
|
||||
import type { SessionContext } from "../../../src/core/session-manager.js";
|
||||
import type { ToolExecutionComponent } from "../../../src/modes/interactive/components/tool-execution.js";
|
||||
import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.js";
|
||||
import { initTheme } from "../../../src/modes/interactive/theme/theme.js";
|
||||
|
||||
const TOOL_CALL_ID = "tool-4167";
|
||||
const TOOL_NAME = "slow_tool";
|
||||
|
||||
const EMPTY_USAGE: Usage = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
type RenderSessionContextThis = {
|
||||
pendingTools: Map<string, ToolExecutionComponent>;
|
||||
chatContainer: Container;
|
||||
footer: { invalidate(): void };
|
||||
ui: TUI;
|
||||
settingsManager: {
|
||||
getShowImages(): boolean;
|
||||
getImageWidthCells(): number;
|
||||
};
|
||||
sessionManager: { getCwd(): string };
|
||||
session: { retryAttempt: number };
|
||||
toolOutputExpanded: boolean;
|
||||
isInitialized: boolean;
|
||||
updateEditorBorderColor(): void;
|
||||
getRegisteredToolDefinition(toolName: string): undefined;
|
||||
addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
|
||||
};
|
||||
|
||||
type RenderSessionContext = (
|
||||
this: RenderSessionContextThis,
|
||||
sessionContext: SessionContext,
|
||||
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
||||
) => void;
|
||||
|
||||
type HandleEvent = (this: RenderSessionContextThis, event: AgentSessionEvent) => Promise<void>;
|
||||
|
||||
function createFakeInteractiveModeThis(): RenderSessionContextThis {
|
||||
const chatContainer = new Container();
|
||||
return {
|
||||
pendingTools: new Map<string, ToolExecutionComponent>(),
|
||||
chatContainer,
|
||||
footer: { invalidate: vi.fn() },
|
||||
ui: { requestRender: vi.fn() } as unknown as TUI,
|
||||
settingsManager: {
|
||||
getShowImages: () => false,
|
||||
getImageWidthCells: () => 60,
|
||||
},
|
||||
sessionManager: { getCwd: () => process.cwd() },
|
||||
session: { retryAttempt: 0 },
|
||||
toolOutputExpanded: false,
|
||||
isInitialized: true,
|
||||
updateEditorBorderColor: vi.fn(),
|
||||
getRegisteredToolDefinition: (_toolName: string) => undefined,
|
||||
addMessageToChat(message: AgentMessage) {
|
||||
chatContainer.addChild(new Text(message.role, 0, 0));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createAssistantToolCallMessage(): AssistantMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: TOOL_CALL_ID,
|
||||
name: TOOL_NAME,
|
||||
arguments: { delayMs: 10_000 },
|
||||
},
|
||||
],
|
||||
api: "test-api",
|
||||
provider: "test-provider",
|
||||
model: "test-model",
|
||||
usage: EMPTY_USAGE,
|
||||
stopReason: "toolUse",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function createToolResultMessage(text: string): ToolResultMessage {
|
||||
return {
|
||||
role: "toolResult",
|
||||
toolCallId: TOOL_CALL_ID,
|
||||
toolName: TOOL_NAME,
|
||||
content: [{ type: "text", text }],
|
||||
isError: false,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionContext(messages: AgentMessage[]): SessionContext {
|
||||
return {
|
||||
messages,
|
||||
thinkingLevel: "off",
|
||||
model: null,
|
||||
};
|
||||
}
|
||||
|
||||
function renderChat(container: Container): string {
|
||||
return stripAnsi(container.render(120).join("\n"));
|
||||
}
|
||||
|
||||
describe("InteractiveMode.renderSessionContext", () => {
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
});
|
||||
|
||||
test("keeps unresolved rendered tool calls registered for live completion events", async () => {
|
||||
const fakeThis = createFakeInteractiveModeThis();
|
||||
const renderSessionContext = (
|
||||
InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext }
|
||||
).renderSessionContext;
|
||||
const handleEvent = (InteractiveMode.prototype as unknown as { handleEvent: HandleEvent }).handleEvent;
|
||||
|
||||
renderSessionContext.call(fakeThis, createSessionContext([createAssistantToolCallMessage()]));
|
||||
|
||||
expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(true);
|
||||
|
||||
await handleEvent.call(fakeThis, {
|
||||
type: "tool_execution_end",
|
||||
toolCallId: TOOL_CALL_ID,
|
||||
toolName: TOOL_NAME,
|
||||
result: { content: [{ type: "text", text: "FINAL_RESULT" }], details: undefined },
|
||||
isError: false,
|
||||
});
|
||||
|
||||
expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(false);
|
||||
expect(renderChat(fakeThis.chatContainer)).toContain("FINAL_RESULT");
|
||||
});
|
||||
|
||||
test("does not keep completed historical tool calls registered as pending", () => {
|
||||
const fakeThis = createFakeInteractiveModeThis();
|
||||
const renderSessionContext = (
|
||||
InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext }
|
||||
).renderSessionContext;
|
||||
|
||||
renderSessionContext.call(
|
||||
fakeThis,
|
||||
createSessionContext([createAssistantToolCallMessage(), createToolResultMessage("HISTORICAL_RESULT")]),
|
||||
);
|
||||
|
||||
expect(fakeThis.pendingTools.size).toBe(0);
|
||||
expect(renderChat(fakeThis.chatContainer)).toContain("HISTORICAL_RESULT");
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { executeBashWithOperations } from "../src/core/bash-executor.js";
|
||||
import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
|
||||
import { type BashOperations, createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
|
||||
import { computeEditsDiff } from "../src/core/tools/edit-diff.js";
|
||||
import {
|
||||
createEditTool,
|
||||
@@ -449,6 +449,42 @@ describe("Coding Agent Tools", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should include full output path for truncated timeout and abort errors", async () => {
|
||||
for (const testCase of [
|
||||
{ error: "timeout:5", expected: "Command timed out after 5 seconds" },
|
||||
{ error: "aborted", expected: "Command aborted" },
|
||||
]) {
|
||||
const operations: BashOperations = {
|
||||
exec: async (_command, _cwd, { onData }) => {
|
||||
for (let i = 1; i <= 3000; i++) {
|
||||
onData(Buffer.from(`${i}\n`, "utf-8"));
|
||||
}
|
||||
throw new Error(testCase.error);
|
||||
},
|
||||
};
|
||||
const bash = createBashTool(testDir, { operations });
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await bash.execute(`test-call-${testCase.error}`, { command: "chatty-fail" });
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
const message = (error as Error).message;
|
||||
expect(message).toContain(testCase.expected);
|
||||
expect(message).toMatch(/\[Showing lines \d+-\d+ of \d+\. Full output: /);
|
||||
expect(message).not.toContain("Full output: undefined");
|
||||
const fullOutputPath = message.match(/Full output: ([^\]\n]+)/)?.[1];
|
||||
expect(fullOutputPath).toBeDefined();
|
||||
expect(existsSync(fullOutputPath!)).toBe(true);
|
||||
const fullOutput = readFileSync(fullOutputPath!, "utf-8");
|
||||
expect(fullOutput).toContain("1\n2\n3");
|
||||
expect(fullOutput).toContain("2998\n2999\n3000");
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw error when cwd does not exist", async () => {
|
||||
const nonexistentCwd = "/this/directory/definitely/does/not/exist/12345";
|
||||
|
||||
@@ -517,6 +553,42 @@ describe("Coding Agent Tools", () => {
|
||||
expect(getTextOutput(result).trim()).toBe("no-prefix");
|
||||
});
|
||||
|
||||
it("should coalesce streaming updates for chatty output", async () => {
|
||||
const operations: BashOperations = {
|
||||
exec: async (_command, _cwd, { onData }) => {
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
onData(Buffer.from(`line ${i}\n`, "utf-8"));
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
},
|
||||
};
|
||||
const updates: Array<{ content: Array<{ type: string; text?: string }>; details?: unknown }> = [];
|
||||
const bash = createBashTool(testDir, { operations });
|
||||
|
||||
const result = await bash.execute("test-call-chatty-updates", { command: "chatty" }, undefined, (update) =>
|
||||
updates.push(update),
|
||||
);
|
||||
|
||||
expect(updates.length).toBeLessThan(25);
|
||||
expect(getTextOutput(result)).toContain("line 4999");
|
||||
});
|
||||
|
||||
it("should decode UTF-8 characters split across output chunks", async () => {
|
||||
const euro = Buffer.from("€\n", "utf-8");
|
||||
const operations: BashOperations = {
|
||||
exec: async (_command, _cwd, { onData }) => {
|
||||
onData(euro.subarray(0, 1));
|
||||
onData(euro.subarray(1));
|
||||
return { exitCode: 0 };
|
||||
},
|
||||
};
|
||||
const bash = createBashTool(testDir, { operations });
|
||||
|
||||
const result = await bash.execute("test-call-split-utf8", { command: "split-utf8" });
|
||||
|
||||
expect(getTextOutput(result).trim()).toBe("€");
|
||||
});
|
||||
|
||||
it("should expose local bash operations for extension reuse", async () => {
|
||||
const ops = createLocalBashOperations();
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed wrapped OSC 8 hyperlinks to preserve BEL terminators so OAuth login URLs remain clickable on every wrapped line.
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed fuzzy ranking to prioritize exact matches in selector and autocomplete results.
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
## [0.72.0] - 2026-05-01
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-tui",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -60,6 +60,10 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch {
|
||||
return { matches: false, score: 0 };
|
||||
}
|
||||
|
||||
if (normalizedQuery === textLower) {
|
||||
score -= 100;
|
||||
}
|
||||
|
||||
return { matches: true, score };
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,10 @@ export function detectCapabilities(): TerminalCapabilities {
|
||||
// sequences differently). Force hyperlinks off whenever we detect them, even
|
||||
// when the outer terminal would otherwise support OSC 8. Image protocols are
|
||||
// also unreliable under tmux/screen, so leave `images: null` for safety.
|
||||
const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen");
|
||||
// cmux currently also gets this conservative fallback due to image corruption:
|
||||
// https://github.com/badlogic/pi-mono/issues/4208
|
||||
const inTmuxOrScreen =
|
||||
!!process.env.TMUX || !!process.env.CMUX_WORKSPACE_ID || term.startsWith("tmux") || term.startsWith("screen");
|
||||
if (inTmuxOrScreen) {
|
||||
const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
|
||||
return { images: null, trueColor, hyperlinks: false };
|
||||
|
||||
@@ -312,6 +312,42 @@ export function extractAnsiCode(str: string, pos: number): { code: string; lengt
|
||||
return null;
|
||||
}
|
||||
|
||||
type Osc8Terminator = "\x07" | "\x1b\\";
|
||||
|
||||
interface ActiveHyperlink {
|
||||
params: string;
|
||||
url: string;
|
||||
terminator: Osc8Terminator;
|
||||
}
|
||||
|
||||
function parseOsc8Hyperlink(ansiCode: string): ActiveHyperlink | null | undefined {
|
||||
if (!ansiCode.startsWith("\x1b]8;")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const terminator: Osc8Terminator = ansiCode.endsWith("\x07") ? "\x07" : "\x1b\\";
|
||||
const body = ansiCode.slice(4, terminator === "\x07" ? -1 : -2);
|
||||
const separatorIndex = body.indexOf(";");
|
||||
if (separatorIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = body.slice(0, separatorIndex);
|
||||
const url = body.slice(separatorIndex + 1);
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return { params, url, terminator };
|
||||
}
|
||||
|
||||
function formatOsc8Hyperlink(hyperlink: ActiveHyperlink): string {
|
||||
return `\x1b]8;${hyperlink.params};${hyperlink.url}${hyperlink.terminator}`;
|
||||
}
|
||||
|
||||
function formatOsc8Close(terminator: Osc8Terminator): string {
|
||||
return `\x1b]8;;${terminator}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track active ANSI SGR codes to preserve styling across line breaks.
|
||||
*/
|
||||
@@ -327,13 +363,16 @@ class AnsiCodeTracker {
|
||||
private strikethrough = false;
|
||||
private fgColor: string | null = null; // Stores the full code like "31" or "38;5;240"
|
||||
private bgColor: string | null = null; // Stores the full code like "41" or "48;5;240"
|
||||
private activeHyperlink: string | null = null; // Active OSC 8 hyperlink URL, or null
|
||||
private activeHyperlink: ActiveHyperlink | null = null;
|
||||
|
||||
process(ansiCode: string): void {
|
||||
// OSC 8 hyperlink: \x1b]8;;<url>\x1b\\ (open) or \x1b]8;;\x1b\\ (close)
|
||||
if (ansiCode.startsWith("\x1b]8;")) {
|
||||
const m = ansiCode.match(/^\x1b\]8;[^;]*;([^\x1b\x07]*)/);
|
||||
this.activeHyperlink = m?.[1] ? m[1] : null;
|
||||
// OSC 8 hyperlink: \x1b]8;;<url>\x1b\\ (open) or \x1b]8;;\x1b\\ (close).
|
||||
// Preserve the original terminator because some terminals only make BEL-terminated
|
||||
// links clickable. OAuth login URLs use BEL, so reopening wrapped lines with ST
|
||||
// made only the first physical line clickable in those terminals.
|
||||
const hyperlink = parseOsc8Hyperlink(ansiCode);
|
||||
if (hyperlink !== undefined) {
|
||||
this.activeHyperlink = hyperlink;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -495,7 +534,7 @@ class AnsiCodeTracker {
|
||||
|
||||
let result = codes.length > 0 ? `\x1b[${codes.join(";")}m` : "";
|
||||
if (this.activeHyperlink) {
|
||||
result += `\x1b]8;;${this.activeHyperlink}\x1b\\`;
|
||||
result += formatOsc8Hyperlink(this.activeHyperlink);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -528,7 +567,7 @@ class AnsiCodeTracker {
|
||||
result += "\x1b[24m"; // Underline off only
|
||||
}
|
||||
if (this.activeHyperlink) {
|
||||
result += "\x1b]8;;\x1b\\"; // Close hyperlink; re-opened at line start via getActiveCodes()
|
||||
result += formatOsc8Close(this.activeHyperlink.terminator); // Re-opened at line start via getActiveCodes()
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,13 @@ describe("fuzzyFilter", () => {
|
||||
assert.strictEqual(result[0], "app");
|
||||
});
|
||||
|
||||
it("prioritizes exact matches over longer prefix matches", () => {
|
||||
const items = ["clone", "cl"];
|
||||
const result = fuzzyFilter(items, "cl", (x: string) => x);
|
||||
|
||||
assert.deepStrictEqual(result, ["cl", "clone"]);
|
||||
});
|
||||
|
||||
it("works with custom getText function", () => {
|
||||
const items = [
|
||||
{ name: "foo", id: 1 },
|
||||
|
||||
@@ -191,6 +191,21 @@ describe("wrapTextWithAnsi with OSC 8 hyperlinks", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves BEL terminators when wrapping OAuth-style hyperlinks", () => {
|
||||
const url = `https://example.com/oauth/${"a".repeat(32)}`;
|
||||
const input = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`;
|
||||
const lines = wrapTextWithAnsi(input, 20);
|
||||
|
||||
assert.ok(lines.length > 1);
|
||||
for (const line of lines) {
|
||||
assert.ok(line.includes(`\x1b]8;;${url}\x07`), `Line "${line}" does not reopen the hyperlink with BEL`);
|
||||
assert.ok(!line.includes(`\x1b]8;;${url}\x1b\\`), `Line "${line}" reopens the hyperlink with ST`);
|
||||
}
|
||||
for (const line of lines.slice(0, -1)) {
|
||||
assert.ok(line.endsWith("\x1b]8;;\x07"), `Line "${line}" does not close the hyperlink with BEL`);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not emit OSC 8 sequences on lines that are outside the hyperlink", () => {
|
||||
const url = "https://example.com";
|
||||
const input = `before \x1b]8;;${url}\x1b\\link\x1b]8;;\x1b\\ after`;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
## [0.72.0] - 2026-05-01
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pi-web-ui-example",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-web-ui",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.0",
|
||||
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@lmstudio/sdk": "^1.5.0",
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-tui": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.73.0",
|
||||
"@mariozechner/pi-tui": "^0.73.0",
|
||||
"typebox": "^1.1.24",
|
||||
"docx-preview": "^0.3.7",
|
||||
"jszip": "^3.10.1",
|
||||
|
||||
232
scripts/tool-stats.ts
Executable file
232
scripts/tool-stats.ts
Executable file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
interface TextContent { type: "text"; text: string }
|
||||
interface ImageContent { type: "image"; data: string; mimeType?: string }
|
||||
interface ToolCallContent { type: "toolCall"; id: string; name: string; arguments?: Record<string, unknown> }
|
||||
type Content = TextContent | ImageContent | ToolCallContent | { type: string; [key: string]: unknown };
|
||||
interface Message { role?: string; content?: string | Content[]; toolCallId?: string; toolName?: string; details?: unknown }
|
||||
interface Entry { type?: string; message?: Message }
|
||||
interface ToolStats { calls: number; results: number; estimatedTokens: number; samples: number[]; errors: number }
|
||||
interface BashCommandStats { calls: number; estimatedTokens: number; samples: number[] }
|
||||
interface ToolCallInfo { toolName: string; bashCommand?: string }
|
||||
|
||||
const BUCKETS = [0, 50, 100, 250, 500, 1000, 2000, 4000, 8000, 16000, 32000, Number.POSITIVE_INFINITY];
|
||||
|
||||
function parseArgs(): { sessionsDir: string; output: string } {
|
||||
let sessionsDir = join(homedir(), ".pi", "agent", "sessions");
|
||||
let output = join(tmpdir(), "pi-tool-stats.html");
|
||||
const args = process.argv.slice(2);
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--sessions-dir" && args[i + 1]) sessionsDir = resolve(args[++i]);
|
||||
else if ((arg === "--output" || arg === "-o") && args[i + 1]) output = resolve(args[++i]);
|
||||
else if (arg === "--help" || arg === "-h") {
|
||||
console.log(`Usage: scripts/tool-stats.ts [--sessions-dir <dir>] [--output <file.html>]`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
return { sessionsDir, output };
|
||||
}
|
||||
|
||||
function jsonlFiles(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...jsonlFiles(path));
|
||||
else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(path);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getStats<T>(map: Map<string, T>, key: string, create: () => T): T {
|
||||
let stats = map.get(key);
|
||||
if (!stats) {
|
||||
stats = create();
|
||||
map.set(key, stats);
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
function createToolStats(): ToolStats {
|
||||
return { calls: 0, results: 0, estimatedTokens: 0, samples: [], errors: 0 };
|
||||
}
|
||||
|
||||
function createBashStats(): BashCommandStats {
|
||||
return { calls: 0, estimatedTokens: 0, samples: [] };
|
||||
}
|
||||
|
||||
function estimateTokenCount(text: string): number {
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
function contentText(content: Message["content"]): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content.map((block) => {
|
||||
if (block.type === "text" && "text" in block && typeof block.text === "string") return block.text;
|
||||
if (block.type === "image" && "data" in block && typeof block.data === "string") return block.data;
|
||||
return JSON.stringify(block);
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function getBashCommand(args: Record<string, unknown> | undefined): string | undefined {
|
||||
const command = args?.command;
|
||||
return typeof command === "string" ? command : undefined;
|
||||
}
|
||||
|
||||
function commandKey(command: string): string {
|
||||
const first = command.split(/\n|&&|\|\||;|\|/)[0]?.trim() ?? command.trim();
|
||||
const match = first.match(/^(?:\w+=\S+\s+)*(?:sudo\s+)?([^\s]+)(?:\s+([^\s]+))?/);
|
||||
if (!match) return "unknown";
|
||||
const bin = match[1] ?? "unknown";
|
||||
const sub = match[2] && !match[2].startsWith("-") ? ` ${match[2]}` : "";
|
||||
return `${bin}${sub}`;
|
||||
}
|
||||
|
||||
function bucketCounts(samples: number[]): number[] {
|
||||
const counts = new Array(BUCKETS.length - 1).fill(0) as number[];
|
||||
for (const sample of samples) {
|
||||
const index = BUCKETS.findIndex((max, i) => i > 0 && sample <= max) - 1;
|
||||
counts[Math.max(0, index)]++;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function bucketLabels(): string[] {
|
||||
return BUCKETS.slice(0, -1).map((min, i) => {
|
||||
const max = BUCKETS[i + 1];
|
||||
return Number.isFinite(max) ? `${min}-${max}` : `${min}+`;
|
||||
});
|
||||
}
|
||||
|
||||
const { sessionsDir, output } = parseArgs();
|
||||
if (!existsSync(sessionsDir)) throw new Error(`Sessions directory not found: ${sessionsDir}`);
|
||||
|
||||
const tools = new Map<string, ToolStats>();
|
||||
const bashCommands = new Map<string, BashCommandStats>();
|
||||
const callsById = new Map<string, ToolCallInfo>();
|
||||
let parseErrors = 0;
|
||||
const files = jsonlFiles(sessionsDir);
|
||||
|
||||
for (const file of files) {
|
||||
for (const line of readFileSync(file, "utf8").split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
let entry: Entry;
|
||||
try { entry = JSON.parse(line) as Entry; } catch { parseErrors++; continue; }
|
||||
if (entry.type !== "message") continue;
|
||||
const message = entry.message;
|
||||
if (!message) continue;
|
||||
if (message.role === "assistant" && Array.isArray(message.content)) {
|
||||
for (const block of message.content) {
|
||||
if (block.type !== "toolCall" || !("name" in block) || typeof block.name !== "string") continue;
|
||||
const stats = getStats(tools, block.name, createToolStats);
|
||||
stats.calls++;
|
||||
const bashCommand = block.name === "bash" ? getBashCommand(block.arguments) : undefined;
|
||||
callsById.set(block.id, { toolName: block.name, bashCommand });
|
||||
if (bashCommand) getStats(bashCommands, commandKey(bashCommand), createBashStats).calls++;
|
||||
}
|
||||
} else if (message.role === "toolResult" && message.toolName) {
|
||||
const text = contentText(message.content);
|
||||
const tokens = estimateTokenCount(text);
|
||||
const stats = getStats(tools, message.toolName, createToolStats);
|
||||
stats.results++;
|
||||
stats.estimatedTokens += tokens;
|
||||
stats.samples.push(tokens);
|
||||
if ("isError" in message && message.isError === true) stats.errors++;
|
||||
const call = message.toolCallId ? callsById.get(message.toolCallId) : undefined;
|
||||
if (call?.bashCommand) {
|
||||
const bash = getStats(bashCommands, commandKey(call.bashCommand), createBashStats);
|
||||
bash.estimatedTokens += tokens;
|
||||
bash.samples.push(tokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolRows = [...tools.entries()].map(([name, s]) => ({ name, ...s, avg: s.results ? s.estimatedTokens / s.results : 0, histogram: bucketCounts(s.samples) })).sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
||||
const bashRows = [...bashCommands.entries()].map(([name, s]) => ({ name, ...s, avg: s.samples.length ? s.estimatedTokens / s.samples.length : 0, histogram: bucketCounts(s.samples) })).sort((a, b) => b.estimatedTokens - a.estimatedTokens).slice(0, 50);
|
||||
const data = { generatedAt: new Date().toISOString(), sessionsDir, files: files.length, parseErrors, bucketLabels: bucketLabels(), tools: toolRows, bashCommands: bashRows };
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Pi Tool Stats</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 p-6">
|
||||
<main class="max-w-7xl mx-auto space-y-6">
|
||||
<h1 class="text-3xl font-bold">Pi Tool Stats</h1>
|
||||
<p class="text-zinc-400">${data.files} session files from <code>${sessionsDir}</code>. Generated ${data.generatedAt}.</p>
|
||||
<section class="grid md:grid-cols-2 gap-6">
|
||||
<div class="bg-zinc-900 rounded p-4"><h2 class="font-semibold mb-3">Estimated result tokens by tool</h2><canvas id="tokens"></canvas></div>
|
||||
<div class="bg-zinc-900 rounded p-4"><h2 class="font-semibold mb-3">Tool calls</h2><canvas id="calls"></canvas></div>
|
||||
</section>
|
||||
<section class="grid md:grid-cols-2 gap-6">
|
||||
<div class="bg-zinc-900 rounded p-4">
|
||||
<div class="flex items-center justify-between gap-4 mb-3">
|
||||
<h2 class="font-semibold">Tool result token histogram</h2>
|
||||
<select id="toolSelect" class="bg-zinc-800 rounded px-2 py-1 text-sm"></select>
|
||||
</div>
|
||||
<p id="toolSummary" class="text-sm text-zinc-400 mb-3"></p>
|
||||
<canvas id="toolHistogram" height="120"></canvas>
|
||||
</div>
|
||||
<div class="bg-zinc-900 rounded p-4">
|
||||
<div class="flex items-center justify-between gap-4 mb-3">
|
||||
<h2 class="font-semibold">Bash result token histogram</h2>
|
||||
<select id="bashSelect" class="bg-zinc-800 rounded px-2 py-1 text-sm"></select>
|
||||
</div>
|
||||
<p id="bashSummary" class="text-sm text-zinc-400 mb-3"></p>
|
||||
<canvas id="bashHistogram" height="120"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<section class="bg-zinc-900 rounded p-4"><h2 class="font-semibold mb-3">Tools</h2><div id="tools"></div></section>
|
||||
<section class="bg-zinc-900 rounded p-4">
|
||||
<h2 class="font-semibold mb-3">Bash common commands (best effort)</h2>
|
||||
<div id="bash" class="mt-4"></div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
const data=${JSON.stringify(data)};
|
||||
function fmt(n){return Math.round(n).toLocaleString()}
|
||||
function esc(s){return String(s).replace(/[&<>\"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))}
|
||||
function table(rows,el){
|
||||
document.getElementById(el).innerHTML='<table class="w-full text-sm"><thead class="text-zinc-400"><tr><th class="text-left p-2">Name</th><th class="text-right p-2">Calls</th><th class="text-right p-2">Results</th><th class="text-right p-2">Est. tokens</th><th class="text-right p-2">Avg/result</th><th class="text-left p-2 w-64">Histogram</th></tr></thead><tbody>'+rows.map((r,i)=>'<tr class="border-t border-zinc-800 hover:bg-zinc-800/50 cursor-pointer" data-row="'+i+'"><td class="p-2 font-mono">'+esc(r.name)+'</td><td class="p-2 text-right">'+fmt(r.calls)+'</td><td class="p-2 text-right">'+fmt(r.results??r.samples.length)+'</td><td class="p-2 text-right">'+fmt(r.estimatedTokens)+'</td><td class="p-2 text-right">'+fmt(r.avg)+'</td><td class="p-2"><canvas id="'+el+'Hist'+i+'" height="34"></canvas></td></tr>').join('')+'</tbody></table>';
|
||||
rows.forEach((r,i)=>new Chart(document.getElementById(el+'Hist'+i),{type:'bar',data:{labels:data.bucketLabels,datasets:[{data:r.histogram,label:r.name,backgroundColor:'#60a5fa'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{callbacks:{title:(items)=>data.bucketLabels[items[0].dataIndex]+' tokens'}}},scales:{x:{display:false},y:{display:false}}}}));
|
||||
document.querySelectorAll('#'+el+' tr[data-row]').forEach(row=>row.addEventListener('click',()=>document.getElementById(el==='tools'?'toolSelect':'bashSelect').value=row.dataset.row,document.getElementById(el==='tools'?'toolSelect':'bashSelect').dispatchEvent(new Event('change'))));
|
||||
}
|
||||
function fillSelect(id,rows){document.getElementById(id).innerHTML=rows.map((r,i)=>'<option value="'+i+'">'+esc(r.name)+'</option>').join('')}
|
||||
function summary(r){return fmt(r.calls)+' calls, '+fmt(r.results??r.samples.length)+' results, '+fmt(r.estimatedTokens)+' estimated result tokens, '+fmt(r.avg)+' avg/result'}
|
||||
function singleHistogram(canvasId,summaryId,selectId,rows){
|
||||
let chart;
|
||||
function update(){
|
||||
const row=rows[Number(document.getElementById(selectId).value)]??rows[0];
|
||||
document.getElementById(summaryId).textContent=row?summary(row):'No data';
|
||||
if(chart)chart.destroy();
|
||||
chart=new Chart(document.getElementById(canvasId),{type:'bar',data:{labels:data.bucketLabels,datasets:[{label:row?.name??'',data:row?.histogram??[],backgroundColor:'#60a5fa'}]},options:{plugins:{legend:{display:false},tooltip:{callbacks:{title:(items)=>data.bucketLabels[items[0].dataIndex]+' tokens',label:(item)=>fmt(item.raw)+' results'}}},scales:{y:{beginAtZero:true,title:{display:true,text:'result count'}},x:{title:{display:true,text:'estimated tokens/result'}}}}});
|
||||
}
|
||||
document.getElementById(selectId).addEventListener('change',update);
|
||||
update();
|
||||
}
|
||||
table(data.tools,'tools');
|
||||
table(data.bashCommands,'bash');
|
||||
fillSelect('toolSelect',data.tools);
|
||||
fillSelect('bashSelect',data.bashCommands);
|
||||
singleHistogram('toolHistogram','toolSummary','toolSelect',data.tools);
|
||||
singleHistogram('bashHistogram','bashSummary','bashSelect',data.bashCommands);
|
||||
new Chart(document.getElementById('tokens'),{type:'bar',data:{labels:data.tools.map(r=>r.name),datasets:[{label:'estimated tokens',data:data.tools.map(r=>r.estimatedTokens)}]},options:{plugins:{legend:{display:false}}}});
|
||||
new Chart(document.getElementById('calls'),{type:'bar',data:{labels:data.tools.map(r=>r.name),datasets:[{label:'calls',data:data.tools.map(r=>r.calls)}]},options:{plugins:{legend:{display:false}}}});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
mkdirSync(resolve(output, ".."), { recursive: true });
|
||||
writeFileSync(output, html);
|
||||
console.log(`Wrote ${output}`);
|
||||
spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? ["/c", "start", output] : [output], { detached: true, stdio: "ignore" }).unref();
|
||||
Reference in New Issue
Block a user