diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index fbbea170..5dcdd534 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -198,3 +198,5 @@ louis030195 pr technocidal pr pandada8 pr + +npupko issue diff --git a/.gitignore b/.gitignore index e2e244ec..12a09a8f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ packages/*/dist-firefox/ .vscode/ .zed/ .idea/ +.claude/ *.swp *.swo *~ diff --git a/package-lock.json b/package-lock.json index 4ff3d711..8f0c371f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 332259c3..6079c785 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index e983a617..972c0c51 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.73.0] - 2026-05-04 + ## [0.72.1] - 2026-05-02 ### Changed diff --git a/packages/agent/package.json b/packages/agent/package.json index e788a0cd..b42162b3 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -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" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1be5a737..aecc4c55 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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 diff --git a/packages/ai/package.json b/packages/ai/package.json index eb75ba23..f05b48cf 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -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", diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index e9c17322..ab4cdd19 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -857,15 +857,17 @@ async function loadModelsDevData(): Promise[]> { const kimiModels = data["kimi-for-coding"].models as Record; 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, diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 9f996059..2502008a 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -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"; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index c4c41bec..abca6a23 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -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", diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 80eaf496..dc6b6463 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -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"], diff --git a/packages/ai/src/providers/openai-responses-shared.ts b/packages/ai/src/providers/openai-responses-shared.ts index f25d6dcb..0d68cd7f 100644 --- a/packages/ai/src/providers/openai-responses-shared.ts +++ b/packages/ai/src/providers/openai-responses-shared.ts @@ -354,6 +354,16 @@ export async function processResponsesStream( }); } } + } 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( 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", diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index 0bc705a5..62c4c352 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -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 { 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 { }; 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 { 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 { 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 { 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); diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index e3520342..03cbf101 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -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; onProgress?: (message: string) => void; onManualCodeInput?: () => Promise; + /** Show an interactive selector and return the selected option id, or undefined on cancel. */ + onSelect?: (prompt: OAuthSelectPrompt) => Promise; signal?: AbortSignal; } diff --git a/packages/ai/test/openai-codex-oauth.test.ts b/packages/ai/test/openai-codex-oauth.test.ts new file mode 100644 index 00000000..276abe1b --- /dev/null +++ b/packages/ai/test/openai-codex-oauth.test.ts @@ -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 => { + 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(); + }); +}); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c9dbcd3d..2f8621aa 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 7cefabb7..ac299123 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -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" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 8db7e0bc..7f1f0088 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -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'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 3ff6671e..b74591d2 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -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'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 2c081e21..fbe7ec7c 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -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" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 7e3672fd..e9190db9 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -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'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index f8f490e2..98a99292 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -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" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 5607b6db..5825a89a 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -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'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 97fd1097..b4f3ba7f 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -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", diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 6c451ccb..ad66b9e2 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -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) { diff --git a/packages/coding-agent/src/core/export-html/template.css b/packages/coding-agent/src/core/export-html/template.css index ac292311..c807161e 100644 --- a/packages/coding-agent/src/core/export-html/template.css +++ b/packages/coding-agent/src/core/export-html/template.css @@ -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); diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index 844f1eb3..cf28b935 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -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: \n...\n\n\nuser message + */ + function parseSkillBlock(text) { + const match = text.match(/^\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 + `skill: ${escapeHtml(skillBlock.name)}`; + if (skillBlock.userMessage) { + treeHtml += ` · user: ${escapeHtml(truncate(normalize(skillBlock.userMessage)))}`; + } + return treeHtml; + } + const content = truncate(normalize(rawContent)); return labelHtml + `user: ${escapeHtml(content)}`; } if (msg.role === 'assistant') { @@ -1140,8 +1165,46 @@ const msg = entry.message; if (msg.role === 'user') { - let html = `
${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 = `
${copyBtnHtml}${tsHtml}`; + + // Skill invocation (collapsed by default, click to expand) + html += `
+
[skill] ${escapeHtml(skillBlock.name)}
+
${escapeHtml(skillBlock.name)} (click to expand)
+
${safeMarkedParse(skillBlock.content)}
+
`; + + // User message (separate block if present) + if (hasUserContent) { + html += '
'; + if (images.length > 0) { + html += '
'; + for (const img of images) { + html += ``; + } + html += '
'; + } + if (skillBlock.userMessage) { + html += `
${safeMarkedParse(skillBlock.userMessage)}
`; + } + html += '
'; + } + + html += '
'; + return html; + } + + // No skill block - normal user message + let html = `
${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 += `
${safeMarkedParse(text)}
`; } @@ -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 = () => { diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index b72e0655..c01252dc 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -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. diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 798b8f8c..f2584aa9 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -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 = diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 18762371..c49e8996 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -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 | 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>, 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; diff --git a/packages/coding-agent/src/core/tools/output-accumulator.ts b/packages/coding-agent/src/core/tools/output-accumulator.ts new file mode 100644 index 00000000..30f4d663 --- /dev/null +++ b/packages/coding-agent/src/core/tools/output-accumulator.ts @@ -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 { + if (!this.tempFileStream) { + return; + } + + const stream = this.tempFileStream; + this.tempFileStream = undefined; + + await new Promise((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 = []; + } +} diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index 98d2096b..f12bd417 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -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`; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 2fab6f56..4eecad9f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -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(); 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 { + 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 { 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, diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index 4393679f..84166e95 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -35,11 +35,20 @@ function emitOsc52(text: string): boolean { export async function copyToClipboard(text: string): Promise { 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 { return; } - const p = platform(); const options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] }; if (!copied) { diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts index 3e53d7f6..ab3ce2a5 100644 --- a/packages/coding-agent/test/args.test.ts +++ b/packages/coding-agent/test/args.test.ts @@ -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", () => { diff --git a/packages/coding-agent/test/export-html-skill-block.test.ts b/packages/coding-agent/test/export-html-skill-block.test.ts new file mode 100644 index 00000000..ae89decf --- /dev/null +++ b/packages/coding-agent/test/export-html-skill-block.test.ts @@ -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: + // \n...\n\n\nactual prompt + // The export renderer must detect that wrapper and render only the user-visible prompt, + // not the Pi-generated ... 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/); + }); +}); diff --git a/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts b/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts new file mode 100644 index 00000000..21a0114e --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/4167-thinking-toggle-pending-tool-render.test.ts @@ -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; + 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; + +function createFakeInteractiveModeThis(): RenderSessionContextThis { + const chatContainer = new Container(); + return { + pendingTools: new Map(), + 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"); + }); +}); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index df0c8f62..70e4e54f 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -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[] = []; diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 22c97e0a..1444bbff 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -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 diff --git a/packages/tui/package.json b/packages/tui/package.json index ae4617a6..bd7e17c8 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -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", diff --git a/packages/tui/src/fuzzy.ts b/packages/tui/src/fuzzy.ts index 09410238..35df468a 100644 --- a/packages/tui/src/fuzzy.ts +++ b/packages/tui/src/fuzzy.ts @@ -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 }; }; diff --git a/packages/tui/src/terminal-image.ts b/packages/tui/src/terminal-image.ts index b300d8cf..a396b497 100644 --- a/packages/tui/src/terminal-image.ts +++ b/packages/tui/src/terminal-image.ts @@ -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 }; diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index 59d21328..a6bcc8a5 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -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;;\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;;\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; } diff --git a/packages/tui/test/fuzzy.test.ts b/packages/tui/test/fuzzy.test.ts index e3e7d1e8..e784e49c 100644 --- a/packages/tui/test/fuzzy.test.ts +++ b/packages/tui/test/fuzzy.test.ts @@ -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 }, diff --git a/packages/tui/test/wrap-ansi.test.ts b/packages/tui/test/wrap-ansi.test.ts index e0944ded..bd24e815 100644 --- a/packages/tui/test/wrap-ansi.test.ts +++ b/packages/tui/test/wrap-ansi.test.ts @@ -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`; diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 44fdb19f..b03de5c9 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.73.0] - 2026-05-04 + ## [0.72.1] - 2026-05-02 ## [0.72.0] - 2026-05-01 diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index 13432d29..afe8c076 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "0.72.1", + "version": "0.73.0", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 488efc07..38e89871 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -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", diff --git a/scripts/tool-stats.ts b/scripts/tool-stats.ts new file mode 100755 index 00000000..8d39b868 --- /dev/null +++ b/scripts/tool-stats.ts @@ -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 } +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 ] [--output ]`); + 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(map: Map, 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 | 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(); +const bashCommands = new Map(); +const callsById = new Map(); +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 = ` + + + + Pi Tool Stats + + + + +
+

Pi Tool Stats

+

${data.files} session files from ${sessionsDir}. Generated ${data.generatedAt}.

+
+

Estimated result tokens by tool

+

Tool calls

+
+
+
+
+

Tool result token histogram

+ +
+

+ +
+
+
+

Bash result token histogram

+ +
+

+ +
+
+

Tools

+
+

Bash common commands (best effort)

+
+
+
+ + +`; + +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();