diff --git a/bun.lock b/bun.lock index 694abb5c68..4e9a7f1023 100644 --- a/bun.lock +++ b/bun.lock @@ -276,7 +276,7 @@ }, "packages/vscode": { "name": "pochi", - "version": "0.61.0-dev", + "version": "0.67.0-dev", "dependencies": { "@ai-sdk/google-vertex": "catalog:", "@getpochi/common": "workspace:*", @@ -375,6 +375,7 @@ "@radix-ui/react-checkbox": "^1.2.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-hover-card": "^1.1.11", "@radix-ui/react-label": "^2.1.6", @@ -1279,17 +1280,17 @@ "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w=="], "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.6", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ=="], - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.16", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ=="], "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], @@ -1305,7 +1306,7 @@ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.17", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], @@ -1321,7 +1322,7 @@ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="], "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], @@ -4741,22 +4742,106 @@ "@radix-ui/react-collapsible/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], + + "@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], + + "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="], + + "@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], + + "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="], + + "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], + + "@radix-ui/react-dialog/react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], + + "@radix-ui/react-hover-card/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-hover-card/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], "@radix-ui/react-hover-card/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-menu/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-navigation-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-popover/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-popover/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-popover/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], + + "@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-scroll-area/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-select/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-select/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-select/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-select/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], + "@radix-ui/react-tabs/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@1.29.2", "https://registry.npmmirror.com/@shikijs/types/-/types-1.29.2.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], @@ -5083,8 +5168,12 @@ "fumadocs-mdx/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "fumadocs-ui/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + "fumadocs-ui/@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw=="], + "fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "geckodriver/decamelize": ["decamelize@6.0.1", "https://registry.npmmirror.com/decamelize/-/decamelize-6.0.1.tgz", {}, "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ=="], "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], @@ -5593,6 +5682,10 @@ "@posthog/core/cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], + "@shikijs/rehype/shiki/@shikijs/core": ["@shikijs/core@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-oJwU+DxGqp6lUZpvtQgVOXNZcVsirN76tihOLBmwILkKuRuwHteApP8oTXmL4tF5vS5FbOY0+8seXmiCoslk4g=="], "@shikijs/rehype/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-6/ov6pxrSvew13k9ztIOnSBOytXeKs5kfIR7vbhdtVRg+KPzvp2HctYGeWkqv7V6YIoLicnig/QF3iajqyElZA=="], @@ -5893,14 +5986,28 @@ "fumadocs-mdx/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "fumadocs-ui/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "fumadocs-ui/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "fumadocs-ui/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "fumadocs-ui/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "fumadocs-ui/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="], "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="], + "fumadocs-ui/@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "fumadocs-ui/@radix-ui/react-popover/react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], diff --git a/packages/vscode-webui/package.json b/packages/vscode-webui/package.json index ded99fd93d..f586367512 100644 --- a/packages/vscode-webui/package.json +++ b/packages/vscode-webui/package.json @@ -41,6 +41,7 @@ "@radix-ui/react-checkbox": "^1.2.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-hover-card": "^1.1.11", "@radix-ui/react-label": "^2.1.6", diff --git a/packages/vscode-webui/src/components/ui/sheet.tsx b/packages/vscode-webui/src/components/ui/sheet.tsx new file mode 100644 index 0000000000..99df8c6fc5 --- /dev/null +++ b/packages/vscode-webui/src/components/ui/sheet.tsx @@ -0,0 +1,150 @@ +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { type VariantProps, cva } from "class-variance-authority"; +import { X } from "lucide-react"; +import type * as React from "react"; +import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:duration-300 data-[state=open]:duration-500", + { + variants: { + side: { + top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b", + bottom: + "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t", + left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", + right: + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + }, +); + +type SheetContentProps = React.ComponentProps & + VariantProps; + +function SheetContent({ + side = "right", + className, + children, + ...props +}: SheetContentProps) { + const { t } = useTranslation(); + + return ( + + + + {children} + + + {t("common.close")} + + + + ); +} + +function SheetHeader({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +function SheetFooter({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/packages/vscode-webui/src/features/chat/components/__stories__/background-job-manage-panel.stories.tsx b/packages/vscode-webui/src/features/chat/components/__stories__/background-job-manage-panel.stories.tsx new file mode 100644 index 0000000000..48b21a596d --- /dev/null +++ b/packages/vscode-webui/src/features/chat/components/__stories__/background-job-manage-panel.stories.tsx @@ -0,0 +1,109 @@ +import type { BackgroundJobNotification } from "@getpochi/common"; +import type { BackgroundCommands } from "@getpochi/common/vscode-webui-bridge"; +import type { Message } from "@getpochi/livekit"; +import { signal } from "@preact/signals-core"; +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, userEvent, within } from "@storybook/test"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { BackgroundJobManagePanel } from "../background-job-manage-panel"; + +const meta = { + title: "Features/Chat/BackgroundJobManagePanel", + component: BackgroundJobManagePanel, + args: { + taskId: "story-task", + messages: [], + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Empty: Story = { + play: openPanel, +}; + +export const WithJobs: Story = { + args: { + messages: [ + message([ + executeCommandPart("bgjob-cmd-1", "bun run dev"), + executeCommandPart("bgjob-cmd-2", "bun run build"), + notificationPart(notification("bgjob-cmd-2", "completed")), + executeCommandPart("bgjob-cmd-3", "bun run test"), + notificationPart(notification("bgjob-cmd-3", "failed")), + ]), + ], + }, + decorators: [withRunningCommands({ "bgjob-cmd-1": { isVisible: true } })], + play: openPanel, +}; + +async function openPanel({ + canvasElement, +}: { canvasElement: HTMLElement }): Promise { + const canvas = within(canvasElement); + const toggle = canvas.getByTestId("background-job-manage-panel-toggle"); + await userEvent.click(toggle); + await expect(toggle).toHaveAttribute("data-state", "open"); +} + +/** Seeds the host query so the panel sees running commands without a host. */ +function withRunningCommands(backgroundCommands: BackgroundCommands) { + const noop = async () => {}; + const data = { + backgroundCommands: signal(backgroundCommands), + show: noop, + hide: noop, + close: noop, + }; + + return (Story: React.ComponentType) => { + const queryClient = useQueryClient(); + useState(() => { + queryClient.setQueryData(["backgroundCommands"], data); + return null; + }); + return ; + }; +} + +function message(parts: unknown[]): Message { + return { id: "message-1", role: "assistant", parts } as unknown as Message; +} + +function executeCommandPart(backgroundJobId: string, command: string) { + return { + type: "tool-executeCommand", + state: "output-available", + input: { command, background: true }, + output: { _meta: { backgroundJobId } }, + }; +} + +function notificationPart(data: BackgroundJobNotification) { + return { type: "data-background-job-notification", data }; +} + +function notification( + backgroundJobId: string, + status: BackgroundJobNotification["status"], +): BackgroundJobNotification { + return { + notificationId: `${backgroundJobId}:terminal`, + backgroundJobId, + outputFile: `/tmp/${backgroundJobId}.log`, + command: `run ${backgroundJobId}`, + status, + summary: `Background command "${backgroundJobId}" ${status}`, + finishedAt: Date.now(), + }; +} diff --git a/packages/vscode-webui/src/features/chat/components/background-job-manage-panel.test.tsx b/packages/vscode-webui/src/features/chat/components/background-job-manage-panel.test.tsx new file mode 100644 index 0000000000..83ccf33943 --- /dev/null +++ b/packages/vscode-webui/src/features/chat/components/background-job-manage-panel.test.tsx @@ -0,0 +1,551 @@ +// @vitest-environment jsdom +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { BackgroundJobEntry } from "../lib/build-background-job-list"; +import { BackgroundJobManagePanel } from "./background-job-manage-panel"; + +const show = vi.fn(); +const hide = vi.fn(); +const close = vi.fn(); +const openFile = vi.fn(); +const copyToClipboard = vi.fn(); +let backgroundJobs: BackgroundJobEntry[] = []; +let backgroundCommands: Record | undefined = {}; +let isDevMode = false; +let backgroundTasks: Array<{ id: string; title: string }> = []; + +// Radix positions the tooltip with one, and jsdom has none. +vi.stubGlobal( + "ResizeObserver", + class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + }, +); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + // Keys stand in for their translations, with the exit code interpolated. + t: (key: string, options?: { exitCode?: number }) => + options?.exitCode === undefined ? key : `${key} ${options.exitCode}`, + }), +})); + +// The drawer is rendered inline so the content is always assertable. +vi.mock("@/components/ui/sheet", () => ({ + Sheet: ({ children }: { children: ReactNode }) => <>{children}, + SheetContent: ({ children }: { children: ReactNode }) => <>{children}, + SheetTitle: ({ children }: { children: ReactNode }) => <>{children}, + SheetTrigger: ({ children }: { children: ReactNode }) => <>{children}, +})); + +vi.mock("../hooks/use-background-job-list", () => ({ + useBackgroundJobList: () => backgroundJobs, +})); + +vi.mock("@/lib/hooks/use-background-commands", () => ({ + useBackgroundCommands: () => ({ backgroundCommands, show, hide, close }), +})); + +vi.mock("@/lib/vscode", () => ({ + vscodeHost: { + openFile: (path: string) => openFile(path), + }, +})); + +// jsdom has no working clipboard. +vi.mock("@/lib/hooks/use-copy-to-clipboard", () => ({ + useCopyToClipboard: () => ({ isCopied: false, copyToClipboard }), +})); + +vi.mock("@/features/settings", () => ({ + useIsDevMode: () => [isDevMode], +})); + +vi.mock("./background-task-debug-panel", () => ({ + BackgroundTasksLabel: "Background tasks", + useBackgroundTasks: () => backgroundTasks, + BackgroundTaskRow: ({ + task, + onSelect, + }: { + task: { title: string }; + onSelect: () => void; + }) => ( + + ), + BackgroundTaskDetail: ({ + taskId, + onBack, + }: { + taskId: string; + onBack: () => void; + }) => ( +
+ {taskId} + +
+ ), +})); + +const renderBackgroundJobManagePanel = () => + render(); + +const rowTitles = () => + screen.getAllByRole("listitem").map((row) => row.textContent); + +const runningJob = { + backgroundJobId: "bgjob-cmd-1", + displayId: "%1", + title: "bun run dev", + command: "bun run dev", + status: "running" as const, +}; + +const runningRow = { + ...runningJob, + displayId: undefined, + title: "running", +}; +const finishedRow = { + ...runningRow, + backgroundJobId: "bgjob-cmd-2", + title: "done", + status: "completed" as const, +}; + +describe("BackgroundJobManagePanel", () => { + beforeEach(() => { + show.mockClear(); + hide.mockClear(); + close.mockClear(); + openFile.mockClear(); + copyToClipboard.mockClear(); + backgroundJobs = []; + backgroundCommands = { "bgjob-cmd-1": { isVisible: true } }; + isDevMode = false; + backgroundTasks = []; + }); + + it("keeps the trigger bare when there is nothing running", () => { + const { container } = renderBackgroundJobManagePanel(); + + const trigger = screen.getByTestId("background-job-manage-panel-toggle"); + expect(trigger.textContent).toBe(""); + expect(trigger.getAttribute("aria-label")).toBe("managePanel.toggle"); + expect(trigger.querySelector("svg")?.classList.contains("size-4.5")).toBe( + true, + ); + expect(trigger.querySelector(".bg-blue-500")).toBeNull(); + expect(container.querySelector(".animate-spin")).toBeNull(); + expect(screen.getByText("managePanel.empty")).toBeDefined(); + expect(screen.queryByText("managePanel.pochiGroup")).toBeNull(); + }); + + it("badges the trigger while a command is running", () => { + backgroundJobs = [runningJob]; + + const { container } = renderBackgroundJobManagePanel(); + + const trigger = screen.getByTestId("background-job-manage-panel-toggle"); + expect(trigger.textContent).toBe("1"); + expect(trigger.querySelector(".bg-blue-500")?.textContent).toBe("1"); + expect(container.querySelector(".animate-spin")).not.toBeNull(); + expect(screen.getByText("bun run dev")).toBeDefined(); + expect(screen.queryByText("managePanel.empty")).toBeNull(); + }); + + it("drops the badge once every command has finished", () => { + backgroundJobs = [{ ...runningJob, status: "completed" }]; + + const { container } = renderBackgroundJobManagePanel(); + + expect( + screen + .getByTestId("background-job-manage-panel-toggle") + .querySelector(".bg-blue-500"), + ).toBeNull(); + expect(container.querySelector(".animate-spin")).toBeNull(); + }); + + it("collapses a section from its title", () => { + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + const title = screen.getByText("managePanel.pochiGroup"); + expect(title.classList.contains("text-sm")).toBe(true); + expect(title.classList.contains("font-medium")).toBe(true); + expect(title.classList.contains("text-muted-foreground")).toBe(true); + + const sectionHeader = title.closest("button"); + expect( + sectionHeader?.querySelector("svg")?.classList.contains("size-4"), + ).toBe(true); + expect(sectionHeader?.lastElementChild?.classList.contains("text-sm")).toBe( + true, + ); + expect( + sectionHeader?.lastElementChild?.classList.contains( + "text-muted-foreground/70", + ), + ).toBe(true); + + fireEvent.click(title); + expect(screen.queryByText("bun run dev")).toBeNull(); + + fireEvent.click(title); + expect(screen.getByText("bun run dev")).toBeDefined(); + }); + + it("holds a long category back behind a see-more toggle", () => { + backgroundJobs = Array.from({ length: 7 }, (_, index) => ({ + backgroundJobId: `bgjob-cmd-${index}`, + displayId: `%${index}`, + title: `bun run dev ${index}`, + status: "completed" as const, + })); + + renderBackgroundJobManagePanel(); + + expect(screen.getByText("bun run dev 4")).toBeDefined(); + expect(screen.queryByText("bun run dev 5")).toBeNull(); + + fireEvent.click(screen.getByText("managePanel.seeMore")); + expect(screen.getByText("bun run dev 6")).toBeDefined(); + + fireEvent.click(screen.getByText("managePanel.seeLess")); + expect(screen.queryByText("bun run dev 5")).toBeNull(); + }); + + it("explains a row by its command, and stays quiet without one", () => { + backgroundJobs = [ + runningJob, + { + ...runningJob, + backgroundJobId: "bgjob-cmd-2", + title: "bgjob-cmd-2", + command: undefined, + }, + ]; + + renderBackgroundJobManagePanel(); + + expect(screen.getByText("bun run dev").dataset.slot).toBe( + "tooltip-trigger", + ); + expect(screen.getByText("bgjob-cmd-2").dataset.slot).toBeUndefined(); + }); + + it("says how a command ended on hover, exit code included", async () => { + backgroundJobs = [ + { ...runningJob, status: "failed" as const, exitCode: 127 }, + ]; + + renderBackgroundJobManagePanel(); + + fireEvent.pointerMove(screen.getByText("bun run dev"), { + pointerType: "mouse", + }); + + await waitFor(() => { + const tooltip = screen.getByRole("tooltip"); + expect(tooltip.textContent).toContain("bun run dev"); + expect(tooltip.textContent).toContain( + "backgroundJobNotifications.failed 127", + ); + }); + }); + + it("numbers a row without asking to be clicked", () => { + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + const displayId = screen.getByText("%1"); + expect(displayId.tagName).toBe("SPAN"); + expect(displayId.closest("button")).toBeNull(); + expect(displayId.className).toContain("group-hover:opacity-0"); + }); + + it("keeps the number for a row that has nothing to press", () => { + backgroundJobs = [ + { ...runningJob, status: "stopped" as const, command: undefined }, + ]; + + renderBackgroundJobManagePanel(); + + expect(screen.getByText("%1").className).not.toContain( + "group-hover:opacity-0", + ); + }); + + it("lights the number only while the command terminal is visible", () => { + backgroundJobs = [runningJob]; + + const { rerender } = renderBackgroundJobManagePanel(); + expect(screen.getByText("%1").className).toContain("ring-1"); + + backgroundCommands = { "bgjob-cmd-1": { isVisible: false } }; + rerender(); + expect(screen.getByText("%1").className).not.toContain("ring-1"); + }); + + it("puts a running command's terminal on screen by clicking its row", () => { + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + const row = screen.getByText("bun run dev").closest('[role="button"]'); + expect(row).not.toBeNull(); + if (row) fireEvent.click(row); + expect(show).toHaveBeenCalledWith("bgjob-cmd-1"); + }); + + it("reads a finished command back by clicking its row", () => { + backgroundJobs = [ + { + ...runningJob, + status: "completed" as const, + outputFile: "/tmp/bgjob-cmd-1.log", + }, + ]; + + renderBackgroundJobManagePanel(); + + const row = screen.getByText("bun run dev").closest('[role="button"]'); + expect(row).not.toBeNull(); + if (row) fireEvent.click(row); + expect(openFile).toHaveBeenCalledWith("/tmp/bgjob-cmd-1.log"); + expect(show).not.toHaveBeenCalled(); + }); + + it("leaves a row with nothing to open unclickable", () => { + backgroundJobs = [{ ...runningJob, status: "stopped" as const }]; + + renderBackgroundJobManagePanel(); + + expect( + screen.getByText("bun run dev").closest('[role="button"]'), + ).toBeNull(); + }); + + it("keeps a row control from also firing the row", () => { + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + fireEvent.click(screen.getByLabelText("managePanel.kill")); + expect(close).toHaveBeenCalledWith("bgjob-cmd-1"); + expect(show).not.toHaveBeenCalled(); + }); + + it("keeps a row control's keyboard event from firing the row", () => { + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + fireEvent.keyDown(screen.getByLabelText("managePanel.kill"), { + key: "Enter", + }); + expect(show).not.toHaveBeenCalled(); + }); + + it("offers a running command a way to put its terminal away and to stop it", () => { + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + fireEvent.click(screen.getByLabelText("managePanel.hideTerminal")); + expect(hide).toHaveBeenCalledWith("bgjob-cmd-1"); + expect(screen.getByLabelText("managePanel.kill")).toBeDefined(); + expect( + screen.queryByLabelText("backgroundJobNotifications.openOutput"), + ).toBeNull(); + }); + + it("offers back the terminal of a running command whose tab was put away", () => { + backgroundCommands = { "bgjob-cmd-1": { isVisible: false } }; + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + fireEvent.click(screen.getByLabelText("managePanel.openTerminal")); + expect(show).toHaveBeenCalledWith("bgjob-cmd-1"); + // The process outlives its tab, so it can still be stopped. + expect(screen.getByLabelText("managePanel.kill")).toBeDefined(); + }); + + it("offers a finished command its output file", () => { + backgroundJobs = [ + { + ...runningJob, + status: "completed" as const, + outputFile: "/tmp/bgjob-cmd-1.log", + }, + ]; + + renderBackgroundJobManagePanel(); + + const openOutput = screen.getByLabelText( + "backgroundJobNotifications.openOutput", + ); + // Only the pointer brings the controls out, and they hold their place in + // the row while hidden. jsdom has no `:hover`, so the reveal itself cannot + // be observed here — that they start hidden, and are still clickable, can. + const controls = openOutput.parentElement; + expect(controls?.className).toContain("opacity-0"); + expect(controls?.className).toContain("group-hover:opacity-100"); + + fireEvent.click(openOutput); + expect(openFile).toHaveBeenCalledWith("/tmp/bgjob-cmd-1.log"); + expect(screen.queryByLabelText("managePanel.kill")).toBeNull(); + expect(screen.queryByLabelText("managePanel.openTerminal")).toBeNull(); + }); + + it("hands a finished command back to the clipboard", () => { + backgroundJobs = [ + { + ...runningJob, + status: "completed" as const, + outputFile: "/tmp/bgjob-cmd-1.log", + }, + ]; + + renderBackgroundJobManagePanel(); + + // Beside the transcript, not instead of it. + expect( + screen.getByLabelText("backgroundJobNotifications.openOutput"), + ).toBeDefined(); + fireEvent.click(screen.getByLabelText("managePanel.copyCommand")); + expect(copyToClipboard).toHaveBeenCalledWith("bun run dev"); + // The control speaks for itself; the row's own action must not fire too. + expect(openFile).not.toHaveBeenCalled(); + }); + + it("keeps the clipboard control away from a running command", () => { + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + expect(screen.queryByLabelText("managePanel.copyCommand")).toBeNull(); + }); + + it("leaves a finished command without a transcript with nothing to press", () => { + backgroundJobs = [ + { ...runningJob, status: "stopped" as const, command: undefined }, + ]; + + renderBackgroundJobManagePanel(); + + expect( + screen.queryByLabelText("backgroundJobNotifications.openOutput"), + ).toBeNull(); + expect(screen.queryByLabelText("managePanel.copyCommand")).toBeNull(); + }); + + it("lists running commands first", () => { + backgroundJobs = [finishedRow, runningRow]; + + renderBackgroundJobManagePanel(); + + expect(rowTitles()).toEqual(["running", "done"]); + }); + + it("keeps a command in place once it stops", () => { + backgroundJobs = [finishedRow, runningRow]; + + const { rerender } = renderBackgroundJobManagePanel(); + + // Killing the top row must not drop it to the bottom under the pointer. + backgroundJobs = [ + finishedRow, + { ...runningRow, status: "stopped" as const }, + ]; + rerender(); + + expect(rowTitles()).toEqual(["running", "done"]); + }); + + it("keeps background tasks out of the panel outside dev mode", () => { + backgroundTasks = [{ id: "task-1", title: "A background task" }]; + + renderBackgroundJobManagePanel(); + + expect(screen.queryByText("Background tasks")).toBeNull(); + expect(screen.getByText("managePanel.empty")).toBeDefined(); + }); + + it("hides the task section in dev mode while there is no task", () => { + isDevMode = true; + + renderBackgroundJobManagePanel(); + + expect(screen.queryByText("Background tasks")).toBeNull(); + expect(screen.getByText("managePanel.empty")).toBeDefined(); + }); + + it("lists background tasks in dev mode", () => { + isDevMode = true; + backgroundTasks = [{ id: "task-1", title: "A background task" }]; + + renderBackgroundJobManagePanel(); + + expect(screen.getByText("Background tasks")).toBeDefined(); + expect(screen.getByText("A background task")).toBeDefined(); + expect(screen.queryByText("managePanel.empty")).toBeNull(); + }); + + it("takes the drawer to a task and back again", () => { + isDevMode = true; + backgroundTasks = [{ id: "task-1", title: "A background task" }]; + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + fireEvent.click(screen.getByText("A background task")); + // The detail covers the list rather than replacing it. + expect(screen.getByTestId("background-task-detail").textContent).toContain( + "task-1", + ); + expect(screen.getByTestId("background-task-layer").dataset.state).toBe( + "open", + ); + expect( + screen.getByTestId("background-job-list-layer").hasAttribute("inert"), + ).toBe(true); + + fireEvent.click(screen.getByText("back")); + expect(screen.getByTestId("background-task-layer").dataset.state).toBe( + "closed", + ); + expect( + screen.getByTestId("background-job-list-layer").hasAttribute("inert"), + ).toBe(false); + }); + + it("keeps the list as it was left while a task is open", () => { + isDevMode = true; + backgroundTasks = [{ id: "task-1", title: "A background task" }]; + backgroundJobs = [runningJob]; + + renderBackgroundJobManagePanel(); + + // Fold the commands, then take a detour through a task detail. + fireEvent.click(screen.getByText("managePanel.pochiGroup")); + expect(screen.queryByText("bun run dev")).toBeNull(); + + fireEvent.click(screen.getByText("A background task")); + fireEvent.click(screen.getByText("back")); + + // The list was never unmounted, so it is still folded. + expect(screen.queryByText("bun run dev")).toBeNull(); + }); +}); diff --git a/packages/vscode-webui/src/features/chat/components/background-job-manage-panel.tsx b/packages/vscode-webui/src/features/chat/components/background-job-manage-panel.tsx new file mode 100644 index 0000000000..9defa6ec02 --- /dev/null +++ b/packages/vscode-webui/src/features/chat/components/background-job-manage-panel.tsx @@ -0,0 +1,497 @@ +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Sheet, + SheetContent, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useIsDevMode } from "@/features/settings"; +import { getBackgroundJobStatusLabel } from "@/lib/background-job-status-label"; +import { useBackgroundCommands } from "@/lib/hooks/use-background-commands"; +import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard"; +import { cn } from "@/lib/utils"; +import { vscodeHost } from "@/lib/vscode"; +import type { Message, Task } from "@getpochi/livekit"; +import { + CheckIcon, + ChevronRightIcon, + CopyIcon, + EyeIcon, + EyeOffIcon, + FileTextIcon, + ListIcon, + XIcon, +} from "lucide-react"; +import { Children, type ReactNode, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useBackgroundJobList } from "../hooks/use-background-job-list"; +import type { + BackgroundJobEntry, + JobStatus, +} from "../lib/build-background-job-list"; +import { + BackgroundTaskDetail, + BackgroundTaskRow, + BackgroundTasksLabel, + useBackgroundTasks, +} from "./background-task-debug-panel"; +import { RowStatusIndicator, type RowStatusTone } from "./row-status-indicator"; + +export function BackgroundJobManagePanel({ + taskId, + messages, +}: { + taskId: string; + messages: Message[]; +}) { + const { t } = useTranslation(); + const [isDevMode] = useIsDevMode(); + const [isOpen, setIsOpen] = useState(false); + const [detailTaskId, setDetailTaskId] = useState(null); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const backgroundJobs = useBackgroundJobList(taskId, messages); + + const runningCount = backgroundJobs.filter( + (job) => job.status === "running", + ).length; + + return ( + { + setIsOpen(open); + if (!open) { + setIsDetailOpen(false); + setDetailTaskId(null); + } + }} + > + + + + + + + {t("managePanel.title")} + + + {t("managePanel.title")} +
+
+ {isDevMode === true ? ( + { + setDetailTaskId(id); + setIsDetailOpen(true); + }} + /> + ) : ( + + )} +
+
+ {detailTaskId !== null && ( + setIsDetailOpen(false)} + /> + )} +
+
+
+
+ ); +} + +const NoTasks: readonly Task[] = []; + +/** + * Background tasks are only shown in dev mode, and hooks cannot be + * conditional, so their query lives in its own component. + */ +function DevPanelBody({ + backgroundJobs, + onSelectTask, +}: { + backgroundJobs: BackgroundJobEntry[]; + onSelectTask: (taskId: string) => void; +}) { + const tasks = useBackgroundTasks(); + + return ( + + ); +} + +function PanelBody({ + backgroundJobs, + tasks, + onSelectTask, +}: { + backgroundJobs: BackgroundJobEntry[]; + tasks: readonly Task[]; + onSelectTask?: (taskId: string) => void; +}) { + const { t } = useTranslation(); + const commands = useRunningFirst(backgroundJobs); + + if (backgroundJobs.length === 0 && tasks.length === 0) { + return ( +
+ {t("managePanel.empty")} +
+ ); + } + + return ( + +
+ {commands.length > 0 && ( + + {commands.map((job) => ( +
  • + +
  • + ))} +
    + )} + {tasks.length > 0 && ( + + {tasks.map((task) => ( +
  • + onSelectTask?.(task.id)} + /> +
  • + ))} +
    + )} +
    +
    + ); +} + +/** + * Running commands first, the incoming order kept within each half. A row is + * ranked by the status it had when it first appeared, so a command that stops + * keeps its place instead of dropping away under the pointer. + */ +function useRunningFirst(jobs: BackgroundJobEntry[]): BackgroundJobEntry[] { + const ranks = useRef(new Map()); + + const rankOf = (job: BackgroundJobEntry) => { + const known = ranks.current.get(job.backgroundJobId); + if (known !== undefined) return known; + const rank = job.status === "running" ? 0 : 1; + ranks.current.set(job.backgroundJobId, rank); + return rank; + }; + + return [...jobs].sort((a, b) => rankOf(a) - rankOf(b)); +} + +const CollapsedItemCount = 5; + +function PanelGroup({ + label, + children, +}: { + label: string; + children: ReactNode; +}) { + const { t } = useTranslation(); + const [isCollapsed, setIsCollapsed] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const items = Children.toArray(children); + const visibleItems = isExpanded ? items : items.slice(0, CollapsedItemCount); + + return ( +
    + + {!isCollapsed && ( + <> +
      {visibleItems}
    + {items.length > CollapsedItemCount && ( + + )} + + )} +
    + ); +} + +function JobRow({ job }: { job: BackgroundJobEntry }) { + const { t } = useTranslation(); + const { backgroundCommands, show, hide, close } = useBackgroundCommands(); + const isRunning = job.status === "running"; + const isVisible = backgroundCommands?.[job.backgroundJobId]?.isVisible; + const openOutputFile = () => { + if (job.outputFile) vscodeHost.openFile(job.outputFile); + }; + const open = isRunning + ? () => show?.(job.backgroundJobId) + : job.outputFile + ? openOutputFile + : undefined; + + const statusLabel = + job.status === "running" || job.status === "finished" + ? undefined + : getBackgroundJobStatusLabel(job.status, job.exitCode, t); + + const title = ( + {job.title} + ); + + const actions = isRunning ? ( + <> + + isVisible ? hide?.(job.backgroundJobId) : show?.(job.backgroundJobId) + } + > + {isVisible ? ( + + ) : ( + + )} + + close?.(job.backgroundJobId)} + > + + + + ) : ( + <> + {job.outputFile && ( + + + + )} + {job.command && } + + ); + const hasActions = + isRunning || job.outputFile !== undefined || job.command !== undefined; + + return ( +
    { + if (event.target !== event.currentTarget) return; + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + open(); + } + : undefined + } + className={cn( + "group flex w-full min-w-0 items-center gap-2 rounded-md px-2 py-1 transition-colors hover:bg-muted/60", + open && + "cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + )} + > + + {job.command || statusLabel ? ( + + {title} + + {job.command && ( + + {job.command} + + )} + {statusLabel && ( + + {statusLabel} + + )} + + + ) : ( + title + )} + + {job.displayId && ( + + {job.displayId} + + )} + {hasActions && ( + + {actions} + + )} + +
    + ); +} + +function JobAction({ + label, + destructive, + onClick, + children, +}: { + label: string; + destructive?: boolean; + onClick?: () => void; + children: ReactNode; +}) { + return ( + + + + + {label} + + ); +} + +function CopyCommandAction({ command }: { command: string }) { + const { t } = useTranslation(); + const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); + + return ( + { + if (!isCopied) copyToClipboard(command); + }} + > + {isCopied ? ( + + ) : ( + + )} + + ); +} + +function statusTone(status: JobStatus): RowStatusTone { + if (status === "completed") return "success"; + if (status === "failed") return "danger"; + return "muted"; +} diff --git a/packages/vscode-webui/src/features/chat/components/background-task-debug-panel.test.tsx b/packages/vscode-webui/src/features/chat/components/background-task-debug-panel.test.tsx index b78c6d8b84..96df2556dd 100644 --- a/packages/vscode-webui/src/features/chat/components/background-task-debug-panel.test.tsx +++ b/packages/vscode-webui/src/features/chat/components/background-task-debug-panel.test.tsx @@ -1,8 +1,10 @@ // @vitest-environment jsdom import { fireEvent, render, screen } from "@testing-library/react"; -import type { ReactNode } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { BackgroundTaskDebugPanel } from "./background-task-debug-panel"; +import { + BackgroundTaskDetail, + BackgroundTaskRow, +} from "./background-task-debug-panel"; const task = { id: "task-1", @@ -45,16 +47,6 @@ vi.mock("@/components/ui/button", () => ({ ), })); -vi.mock("@/components/ui/hover-card", () => ({ - HoverCard: ({ children }: { children: ReactNode }) => <>{children}, - HoverCardContent: ({ children }: { children: ReactNode }) => <>{children}, - HoverCardTrigger: ({ children }: { children: ReactNode }) => <>{children}, -})); - -vi.mock("@/features/settings", () => ({ - useIsDevMode: () => [true], -})); - vi.mock("@/lib/hooks/use-background-task-state", () => ({ useBackgroundTaskState: () => ({ backgroundTaskState: { @@ -87,19 +79,40 @@ vi.mock("@/lib/use-default-store", () => ({ })); function openTaskDetail() { - render(); - fireEvent.click(screen.getByText("Background task")); + render( {}} />); } function getDetailValue(label: string): string | null | undefined { return screen.getByText(label).parentElement?.lastElementChild?.textContent; } -describe("BackgroundTaskDebugPanel", () => { +describe("BackgroundTaskRow", () => { + it("names the task and hands its id back when picked", () => { + const onSelect = vi.fn(); + // biome-ignore lint/suspicious/noExplicitAny: the store rows are mocked. + render(); + + fireEvent.click(screen.getByText("Background task")); + expect(onSelect).toHaveBeenCalled(); + // A failed task rests on a dot, it does not spin. + expect(document.querySelector(".animate-spin")).toBeNull(); + expect(document.querySelector(".bg-destructive")).not.toBeNull(); + }); +}); + +describe("BackgroundTaskDetail", () => { beforeEach(() => { messageRows = []; }); + it("focuses the back button when the detail opens", () => { + openTaskDetail(); + + expect(document.activeElement).toBe( + screen.getByLabelText("Back to the background job list"), + ); + }); + it("uses a single borderless scroll area that fills the remaining height", () => { openTaskDetail(); diff --git a/packages/vscode-webui/src/features/chat/components/background-task-debug-panel.tsx b/packages/vscode-webui/src/features/chat/components/background-task-debug-panel.tsx index d7ee324f3b..525ef48a4e 100644 --- a/packages/vscode-webui/src/features/chat/components/background-task-debug-panel.tsx +++ b/packages/vscode-webui/src/features/chat/components/background-task-debug-panel.tsx @@ -1,220 +1,79 @@ /** - * BackgroundTaskDebugPanel — dev-mode-only floating debug UI for background tasks. - * - * Renders a thin vertical handle on the right edge of the chat page. Hovering - * the handle opens an overview list of all background tasks (any status). - * Clicking a task opens a slide-out panel that shows the task's messages and - * todos via the reusable component. - * - * Mounted from `features/chat/page.tsx` (only renders when `isDevMode` is true). - * - * This is a developer-only surface, so the user-facing strings here are not - * translated. + * Dev-mode background tasks, rendered as one group of the manage panel. Being + * developer-only, the strings here are not translated. */ -/* eslint-disable i18next/no-literal-string */ import { TaskThread, type TaskThreadSource } from "@/components/task-thread"; import { Button } from "@/components/ui/button"; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@/components/ui/hover-card"; -import { useIsDevMode } from "@/features/settings"; import { useBackgroundTaskState } from "@/lib/hooks/use-background-task-state"; import { useDefaultStore } from "@/lib/use-default-store"; import { cn } from "@/lib/utils"; import { type Message, type Task, catalog } from "@getpochi/livekit"; -import { - AlertCircle, - CheckCircle2, - Loader2, - PauseCircle, - X, -} from "lucide-react"; -import { useMemo, useState } from "react"; +import { ArrowLeftIcon } from "lucide-react"; +import { useEffect, useMemo, useRef } from "react"; import { formatTokens } from "../lib/format-tokens"; +import { RowStatusIndicator, type RowStatusTone } from "./row-status-indicator"; -export function BackgroundTaskDebugPanel() { - const [isDevMode] = useIsDevMode(); +export const BackgroundTasksLabel = "Background tasks"; - if (isDevMode !== true) return null; - - return ; -} - -function BackgroundTaskDebugPanelInner() { - const [selectedTaskId, setSelectedTaskId] = useState(null); - const [isListOpen, setIsListOpen] = useState(false); - - const handleSelectTask = (taskId: string) => { - setSelectedTaskId(taskId); - // Auto-hide the overview list as soon as a task is selected — the - // slide-out detail panel becomes the focus. - setIsListOpen(false); - }; - - return ( - <> - - - {/* - The visible gray bar stays small (`w-1.5 h-16`) so the UI is - unobtrusive, but the hit area is a much larger transparent - column (`w-5 h-40`) anchored to the right edge — hovering - anywhere within that column instantly opens the list. - */} - - - - - - - {selectedTaskId && ( - setSelectedTaskId(null)} - /> - )} - - ); +export function useBackgroundTasks(): readonly Task[] { + const store = useDefaultStore(); + return store.useQuery(catalog.queries.backgroundTasks$); } -function BackgroundTaskList({ - selectedTaskId, +export function BackgroundTaskRow({ + task, onSelect, }: { - selectedTaskId: string | null; - onSelect: (taskId: string) => void; + task: Task; + onSelect: () => void; }) { - const store = useDefaultStore(); - const backgroundTasks = store.useQuery(catalog.queries.backgroundTasks$); - return ( -
    -
    - - Background Tasks - - - {backgroundTasks.length} - -
    - {backgroundTasks.length === 0 ? ( -
    - No background tasks -
    - ) : ( -
      - {backgroundTasks.map((task) => ( - onSelect(task.id)} - /> - ))} -
    + ); } -function BackgroundTaskListItem({ - task, - isSelected, - onSelect, -}: { - task: Task; - isSelected: boolean; - onSelect: () => void; -}) { +function BackgroundTaskStatusIndicator({ status }: { status: Task["status"] }) { return ( -
  • - -
  • + ); } -function BackgroundTaskStatusIcon({ task }: { task: Task }) { - switch (task.status) { - case "pending-model": - case "pending-tool": - return ( - - ); - case "pending-input": - return ; - case "completed": - return ; - case "failed": - return ; - default: - return ; - } +function statusTone(status: Task["status"]): RowStatusTone { + if (status === "completed") return "success"; + if (status === "failed") return "danger"; + if (status === "pending-input") return "warning"; + return "muted"; } -function BackgroundTaskDetail({ +export function BackgroundTaskDetail({ taskId, - onClose, + isOpen = true, + onBack, }: { taskId: string; - onClose: () => void; + isOpen?: boolean; + onBack: () => void; }) { + const backButtonRef = useRef(null); const store = useDefaultStore(); const task = store.useQuery(catalog.queries.makeTaskQuery(taskId)); const messageRows = store.useQuery(catalog.queries.makeMessagesQuery(taskId)); @@ -238,18 +97,25 @@ function BackgroundTaskDetail({ ? latestAssistantMessage.metadata : undefined; + useEffect(() => { + if (isOpen) backButtonRef.current?.focus(); + }, [isOpen]); + return ( -
    -
    +
    +
    +
    - {task && } + {task && }
    {task?.title || "(Untitled)"} @@ -259,15 +125,6 @@ function BackgroundTaskDetail({
    -
    diff --git a/packages/vscode-webui/src/features/chat/components/chat-toolbar.test.tsx b/packages/vscode-webui/src/features/chat/components/chat-toolbar.test.tsx index 0618747aa8..0f8a22428b 100644 --- a/packages/vscode-webui/src/features/chat/components/chat-toolbar.test.tsx +++ b/packages/vscode-webui/src/features/chat/components/chat-toolbar.test.tsx @@ -175,6 +175,9 @@ vi.mock("./chat-input-form", () => ({ vi.mock("./error-message-view", () => ({ ErrorMessageView: () => null, })); +vi.mock("./background-job-manage-panel", () => ({ + BackgroundJobManagePanel: () => null, +})); vi.mock("./submit-review-button", () => ({ SubmitReviewsButton: () => null, })); diff --git a/packages/vscode-webui/src/features/chat/components/chat-toolbar.tsx b/packages/vscode-webui/src/features/chat/components/chat-toolbar.tsx index dcf167e740..abc6a2660c 100644 --- a/packages/vscode-webui/src/features/chat/components/chat-toolbar.tsx +++ b/packages/vscode-webui/src/features/chat/components/chat-toolbar.tsx @@ -62,6 +62,7 @@ import { enqueueBackgroundJobNotifications, getBackgroundJobNotificationIds, } from "../lib/background-job-notification-queue"; +import { BackgroundJobManagePanel } from "./background-job-manage-panel"; import { ChatInputForm, type ChatInputFormHandle } from "./chat-input-form"; import { ErrorMessageView } from "./error-message-view"; import { SubmitReviewsButton } from "./submit-review-button"; @@ -69,7 +70,9 @@ import { CompleteSubtaskButton } from "./subtask"; const PopupContainerClassName = tw`-translate-y-full -top-2 absolute left-0 w-full px-4 pt-1`; const PopupContentClassName = tw`flex w-full flex-col bg-background`; -const FooterContainerClassName = tw`my-2 flex shrink-0 justify-between gap-5 overflow-x-hidden`; +// `overflow-x-hidden` clips vertically too, so the row carries padding to keep +// room for anything hanging outside a control, such as the manage panel badge. +const FooterContainerClassName = tw`my-1 flex shrink-0 justify-between gap-5 overflow-x-hidden py-1`; const FooterLeftClassName = tw`flex items-center gap-2 overflow-x-hidden truncate`; const FooterRightClassName = tw`flex shrink-0 items-center gap-1`; @@ -580,6 +583,7 @@ export const ChatToolbar: React.FC = ({ todos={todos} getSystemPrompt={getSystemPrompt} /> + + {isRunning ? ( + + ) : ( + + )} + + ); +} diff --git a/packages/vscode-webui/src/features/chat/hooks/use-background-job-list.ts b/packages/vscode-webui/src/features/chat/hooks/use-background-job-list.ts new file mode 100644 index 0000000000..b347cca3cb --- /dev/null +++ b/packages/vscode-webui/src/features/chat/hooks/use-background-job-list.ts @@ -0,0 +1,23 @@ +import { useBackgroundCommands } from "@/lib/hooks/use-background-commands"; +import { useBackgroundJobNotifications } from "@/lib/hooks/use-background-job-notifications"; +import type { Message } from "@getpochi/livekit"; +import { useMemo } from "react"; +import { + type BackgroundJobEntry, + buildBackgroundJobList, +} from "../lib/build-background-job-list"; + +/** @useSignals */ +export function useBackgroundJobList( + taskId: string, + messages: Message[], +): BackgroundJobEntry[] { + const { backgroundCommands } = useBackgroundCommands(); + const { notifications } = useBackgroundJobNotifications(taskId); + + return useMemo( + () => + buildBackgroundJobList({ messages, notifications, backgroundCommands }), + [messages, notifications, backgroundCommands], + ); +} diff --git a/packages/vscode-webui/src/features/chat/lib/build-background-job-list.test.ts b/packages/vscode-webui/src/features/chat/lib/build-background-job-list.test.ts new file mode 100644 index 0000000000..924fb32064 --- /dev/null +++ b/packages/vscode-webui/src/features/chat/lib/build-background-job-list.test.ts @@ -0,0 +1,205 @@ +import type { BackgroundJobNotification } from "@getpochi/common"; +import { describe, expect, it } from "vitest"; +import type { Message } from "@getpochi/livekit"; +import { buildBackgroundJobList } from "./build-background-job-list"; + +describe("buildBackgroundJobList", () => { + it("lists a command the host still has a process for as running", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [message([executeCommandPart("bgjob-cmd-1", "bun run dev")])], + notifications: [], + backgroundCommands: { "bgjob-cmd-1": { isVisible: true } }, + }); + + expect(backgroundJobs).toEqual([ + { + backgroundJobId: "bgjob-cmd-1", + displayId: "%1", + title: "bun run dev", + command: "bun run dev", + status: "running", + outputFile: "/tmp/bgjob-cmd-1.log", + }, + ]); + }); + + it("keeps a finished job whose notification was already delivered", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [ + message([ + executeCommandPart("bgjob-cmd-1", "bun run dev"), + notificationPart(notification("bgjob-cmd-1", "failed")), + ]), + ], + notifications: [], + backgroundCommands: {}, + }); + + expect(backgroundJobs).toEqual([ + { + backgroundJobId: "bgjob-cmd-1", + displayId: "%1", + title: "bun run dev", + command: "bun run dev", + status: "failed", + outputFile: "/tmp/bgjob-cmd-1.log", + }, + ]); + }); + + it("keeps a finished job whose notification is still undelivered", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [message([executeCommandPart("bgjob-cmd-1", "bun run dev")])], + notifications: [notification("bgjob-cmd-1", "completed")], + backgroundCommands: {}, + }); + + expect(backgroundJobs).toMatchObject([ + { backgroundJobId: "bgjob-cmd-1", status: "completed" }, + ]); + }); + + it("carries the exit code a finished job reported", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [message([executeCommandPart("bgjob-cmd-1", "bun run dev")])], + notifications: [notification("bgjob-cmd-1", "failed", 127)], + backgroundCommands: {}, + }); + + expect(backgroundJobs).toMatchObject([ + { status: "failed", exitCode: 127 }, + ]); + }); + + it("still lists a gone command nothing reported an ending for", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [message([executeCommandPart("bgjob-cmd-1", "bun run dev")])], + notifications: [], + backgroundCommands: {}, + }); + + expect(backgroundJobs).toMatchObject([ + { + backgroundJobId: "bgjob-cmd-1", + status: "finished", + exitCode: undefined, + outputFile: "/tmp/bgjob-cmd-1.log", + }, + ]); + }); + + it("lists a command promoted from the foreground, background flag or not", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [ + message([ + { + ...executeCommandPart("bgjob-cmd-1", "bun run dev"), + input: { command: "bun run dev" }, + }, + ]), + ], + notifications: [], + backgroundCommands: { "bgjob-cmd-1": { isVisible: false } }, + }); + + expect(backgroundJobs).toMatchObject([ + { status: "running", title: "bun run dev" }, + ]); + }); + + it("surfaces a notification whose executeCommand part is gone", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [], + notifications: [notification("bgjob-cmd-9", "stopped")], + backgroundCommands: {}, + }); + + expect(backgroundJobs).toEqual([ + { + backgroundJobId: "bgjob-cmd-9", + title: "run bgjob-cmd-9", + command: "run bgjob-cmd-9", + status: "stopped", + outputFile: "/tmp/bgjob-cmd-9.log", + }, + ]); + }); + + it("lists the newest job first, numbered like the badges in the message list", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [ + message([ + executeCommandPart("bgjob-cmd-1", "first"), + executeCommandPart("bgjob-cmd-2", "second"), + ]), + ], + notifications: [ + notification("bgjob-cmd-1", "completed"), + notification("bgjob-cmd-2", "completed"), + notification("bgjob-cmd-9", "completed"), + ], + backgroundCommands: {}, + }); + + expect(backgroundJobs.map((job) => job.displayId)).toEqual([ + "%2", + "%1", + undefined, + ]); + }); + + it("falls back to what the notifications know while the host table loads", () => { + const backgroundJobs = buildBackgroundJobList({ + messages: [ + message([ + executeCommandPart("bgjob-cmd-1", "bun run dev"), + executeCommandPart("bgjob-cmd-2", "bun run build"), + notificationPart(notification("bgjob-cmd-2", "completed")), + ]), + ], + notifications: [], + backgroundCommands: undefined, + }); + + expect(backgroundJobs.map((job) => job.status)).toEqual([ + "completed", + "finished", + ]); + }); +}); + +function message(parts: unknown[]): Message { + return { id: "message-1", role: "assistant", parts } as unknown as Message; +} + +function executeCommandPart(backgroundJobId: string, command: string) { + return { + type: "tool-executeCommand", + state: "output-available", + input: { command, background: true }, + output: { + _meta: { backgroundJobId, outputFile: `/tmp/${backgroundJobId}.log` }, + }, + }; +} + +function notificationPart(data: BackgroundJobNotification) { + return { type: "data-background-job-notification", data }; +} + +function notification( + backgroundJobId: string, + status: BackgroundJobNotification["status"], + exitCode?: number, +): BackgroundJobNotification { + return { + notificationId: `${backgroundJobId}:terminal`, + backgroundJobId, + outputFile: `/tmp/${backgroundJobId}.log`, + command: `run ${backgroundJobId}`, + status, + exitCode, + summary: `Background command "${backgroundJobId}" ${status}`, + finishedAt: 1, + }; +} diff --git a/packages/vscode-webui/src/features/chat/lib/build-background-job-list.ts b/packages/vscode-webui/src/features/chat/lib/build-background-job-list.ts new file mode 100644 index 0000000000..a7b6f475fe --- /dev/null +++ b/packages/vscode-webui/src/features/chat/lib/build-background-job-list.ts @@ -0,0 +1,95 @@ +import type { BackgroundJobNotification } from "@getpochi/common"; +import type { BackgroundCommands } from "@getpochi/common/vscode-webui-bridge"; +import type { Message } from "@getpochi/livekit"; + +export type JobStatus = + | "running" + | "completed" + | "failed" + | "stopped" + | "finished"; + +export interface BackgroundJobEntry { + backgroundJobId: string; + displayId?: string; + title: string; + command?: string; + status: JobStatus; + exitCode?: number; + outputFile?: string; +} + +/** Collects the background commands Pochi started for this task. */ +export function buildBackgroundJobList({ + messages, + notifications, + backgroundCommands, +}: { + messages: readonly Message[]; + notifications: readonly BackgroundJobNotification[]; + backgroundCommands: BackgroundCommands | undefined; +}): BackgroundJobEntry[] { + const commands = new Map(); + const finished = new Map(); + + for (const message of messages) { + for (const part of message.parts) { + if ( + part.type === "tool-executeCommand" && + part.state !== "input-streaming" && + part.output?._meta?.backgroundJobId + ) { + const { backgroundJobId, outputFile } = part.output._meta; + // First occurrence wins so the `%N` numbering stays stable. + if (!commands.has(backgroundJobId)) { + commands.set(backgroundJobId, { + command: part.input?.command, + outputFile, + }); + } + } else if (part.type === "data-background-job-notification") { + finished.set(part.data.backgroundJobId, part.data); + } + } + } + for (const notification of notifications) { + finished.set(notification.backgroundJobId, notification); + } + + const backgroundJobs: BackgroundJobEntry[] = []; + let index = 0; + for (const [backgroundJobId, meta] of commands) { + index += 1; + const notification = finished.get(backgroundJobId); + const command = meta.command ?? notification?.command; + const isRunning = backgroundCommands?.[backgroundJobId] !== undefined; + backgroundJobs.push({ + backgroundJobId, + displayId: `%${index}`, + title: command ?? backgroundJobId, + command, + status: isRunning ? "running" : (notification?.status ?? "finished"), + exitCode: isRunning ? undefined : notification?.exitCode, + outputFile: meta.outputFile ?? notification?.outputFile, + }); + } + + // A notification can outlive the `executeCommand` part that started it, + // because compaction rewrites older messages. + const orphaned: BackgroundJobEntry[] = []; + for (const notification of finished.values()) { + if (commands.has(notification.backgroundJobId)) continue; + orphaned.push({ + backgroundJobId: notification.backgroundJobId, + title: notification.command ?? notification.backgroundJobId, + command: notification.command, + status: notification.status, + exitCode: notification.exitCode, + outputFile: notification.outputFile, + }); + } + + // Newest command first, with the `%N` labels still counting from the start + // of the task. + return [...backgroundJobs.reverse(), ...orphaned]; +} diff --git a/packages/vscode-webui/src/features/chat/page.tsx b/packages/vscode-webui/src/features/chat/page.tsx index b3694f9323..fc3b57a474 100644 --- a/packages/vscode-webui/src/features/chat/page.tsx +++ b/packages/vscode-webui/src/features/chat/page.tsx @@ -33,7 +33,6 @@ import { useSelectedModels, useSettingsStore, } from "../settings"; -import { BackgroundTaskDebugPanel } from "./components/background-task-debug-panel"; import { ChatArea } from "./components/chat-area"; import { ChatSkeleton } from "./components/chat-skeleton"; import { ChatToolbar } from "./components/chat-toolbar"; @@ -502,7 +501,6 @@ function Chat({ user, uid, info }: ChatProps) { onToolsExecutionEnded={chatKit.markEndToolsExecution} />
    -
    ); } diff --git a/packages/vscode-webui/src/features/tools/components/__tests__/command-execution-panel.test.tsx b/packages/vscode-webui/src/features/tools/components/__tests__/command-execution-panel.test.tsx index 707760910c..83a4e91e98 100644 --- a/packages/vscode-webui/src/features/tools/components/__tests__/command-execution-panel.test.tsx +++ b/packages/vscode-webui/src/features/tools/components/__tests__/command-execution-panel.test.tsx @@ -98,6 +98,18 @@ describe("BackgroundJobPanel job control", () => { expect(openFile).not.toHaveBeenCalled(); }); + it("opens a visible user terminal without waiting for background commands", () => { + terminals = [{ backgroundJobId: "term-1", name: "zsh", isActive: true }]; + backgroundCommands = undefined; + + renderTerminalPanel("/tmp/term-1.log"); + + fireEvent.click( + screen.getByLabelText("commandExecutionPanel.openTerminal"), + ); + expect(openBackgroundJobTerminal).toHaveBeenCalledWith("term-1"); + }); + it("opens a detachable background command through its dedicated control", () => { terminals = [ { backgroundJobId: "bgjob-cmd-1", name: "zsh", isActive: false }, @@ -173,6 +185,16 @@ describe("BackgroundJobPanel job control", () => { ).toBeNull(); }); + it("does not offer job output before background commands are loaded", () => { + backgroundCommands = undefined; + + renderBackgroundJobPanel("/tmp/bgjob-cmd-1.log"); + + expect( + screen.queryByLabelText("commandExecutionPanel.terminalClosedOpenOutput"), + ).toBeNull(); + }); + it("opens the output file from the notification row and shows its summary tooltip", async () => { jobInfo = undefined; const fullSummary = diff --git a/packages/vscode-webui/src/features/tools/components/command-execution-panel.tsx b/packages/vscode-webui/src/features/tools/components/command-execution-panel.tsx index 68cfd4bc88..93cf5e06ff 100644 --- a/packages/vscode-webui/src/features/tools/components/command-execution-panel.tsx +++ b/packages/vscode-webui/src/features/tools/components/command-execution-panel.tsx @@ -6,6 +6,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useBackgroundJobInfo } from "@/features/chat"; +import { getBackgroundJobStatusLabel } from "@/lib/background-job-status-label"; import { useBackgroundCommands } from "@/lib/hooks/use-background-commands"; import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard"; import { useDebounceState } from "@/lib/hooks/use-debounce-state"; @@ -13,7 +14,6 @@ import { useVisibleTerminals } from "@/lib/hooks/use-visible-terminals"; import { formatTerminalDisplayName } from "@/lib/terminal-display-name"; import { cn } from "@/lib/utils"; import { isVSCodeEnvironment, vscodeHost } from "@/lib/vscode"; -import type { TFunction } from "i18next"; import { CheckIcon, ChevronsDownUpIcon, @@ -25,14 +25,7 @@ import { TerminalIcon, XCircle, } from "lucide-react"; -import { - type FC, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { type FC, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { XTerm } from "./xterm"; @@ -246,19 +239,30 @@ export const BackgroundJobPanel: FC<{ const [expanded, setExpanded] = useState(false); const toggleExpanded = () => setExpanded((prev) => !prev); const info = useBackgroundJobInfo(backgroundJobId); - const { backgroundCommands, show: showBackgroundCommand } = - useBackgroundCommands(); const { terminals, openBackgroundJobTerminal } = useVisibleTerminals(); - const isUserTerminal = backgroundJobId.startsWith("term-"); - const isDetachableBackgroundCommand = - backgroundCommands?.[backgroundJobId] !== undefined; - // Live name wins over the snapshot: the terminal may have been renamed - // since the read. The snapshot keeps historical reads meaningful after the - // terminal is closed. - const liveTerminal = useMemo( - () => terminals?.find((tm) => tm.backgroundJobId === backgroundJobId), - [backgroundJobId, terminals], + const { backgroundCommands, show } = useBackgroundCommands(); + const liveTerminal = terminals?.find( + (tm) => tm.backgroundJobId === backgroundJobId, ); + const isUserTerminal = backgroundJobId.startsWith("term-"); + // A background command runs on a pty, so it outlives its terminal tab: the + // host lists it for exactly as long as the process lives. + const isRunning = backgroundCommands?.[backgroundJobId] !== undefined; + const canOpenTerminal = isRunning || liveTerminal !== undefined; + const hasResolvedControlState = + canOpenTerminal || + (terminals !== undefined && + (isUserTerminal || backgroundCommands !== undefined)); + const canOpenOutputFile = + hasResolvedControlState && !canOpenTerminal && outputFile !== undefined; + const openTerminalOrOutputFile = () => { + if (!canOpenTerminal) { + if (outputFile) vscodeHost.openFile(outputFile); + return; + } + if (isRunning) show?.(backgroundJobId); + else openBackgroundJobTerminal?.(backgroundJobId); + }; const isNotification = appearance === "notification"; const recoveredNotificationCommand = isNotification ? recoverNotificationCommand(summary, status) @@ -268,6 +272,9 @@ export const BackgroundJobPanel: FC<{ : (info?.command ?? command); const hasTrackedJob = Boolean(info?.command); const copyCommand = resolvedCommand ?? lastCommand; + // Live name wins over the snapshot: the terminal may have been renamed since + // the read. The snapshot keeps historical reads meaningful after the + // terminal is closed. const displayTerminalName = formatTerminalDisplayName( liveTerminal?.name ?? terminalName, lastCommand, @@ -277,65 +284,39 @@ export const BackgroundJobPanel: FC<{ : (resolvedCommand ?? backgroundJobId); const isActive = liveTerminal?.isActive ?? false; - // Terminals closed after the read keep their badge, so the panel still reads - // as a terminal/job panel; the badge then falls back to the output file. - const isTerminalClosed = terminals !== undefined && !liveTerminal; - const canOpenOutputFile = isTerminalClosed && outputFile !== undefined; - - const openTerminalOrOutputFile = useCallback(() => { - if (isTerminalClosed) { - if (outputFile) vscodeHost.openFile(outputFile); - return; - } - if (isDetachableBackgroundCommand) { - showBackgroundCommand?.(backgroundJobId); - } else if (isUserTerminal || backgroundCommands !== undefined) { - // Keep the legacy terminal path for user terminals and shell fallbacks. - openBackgroundJobTerminal?.(backgroundJobId); - } - }, [ - backgroundCommands, - backgroundJobId, - isDetachableBackgroundCommand, - isTerminalClosed, - isUserTerminal, - openBackgroundJobTerminal, - outputFile, - showBackgroundCommand, - ]); - const closedLabel = canOpenOutputFile ? t("commandExecutionPanel.terminalClosedOpenOutput") : t("commandExecutionPanel.terminalClosed"); const jobControl = isUserTerminal - ? (liveTerminal || isTerminalClosed) && ( + ? hasResolvedControlState && ( ) - : hasTrackedJob && + : hasResolvedControlState && + hasTrackedJob && info?.displayId && (
    @@ -470,20 +451,6 @@ const BackgroundJobStatus: FC<{ ); }; -function getBackgroundJobStatusLabel( - status: "completed" | "failed" | "stopped", - exitCode: number | undefined, - t: TFunction, -): string { - return status === "completed" - ? t("backgroundJobNotifications.completed", { exitCode: exitCode ?? 0 }) - : status === "failed" - ? exitCode === undefined - ? t("backgroundJobNotifications.failedNoExit") - : t("backgroundJobNotifications.failed", { exitCode }) - : t("backgroundJobNotifications.stopped"); -} - const OpenOutputFileButton: FC<{ outputFile: string }> = ({ outputFile }) => { const { t } = useTranslation(); const label = t("backgroundJobNotifications.openOutput"); diff --git a/packages/vscode-webui/src/i18n/locales/en.json b/packages/vscode-webui/src/i18n/locales/en.json index cb391a79d3..e53c8ec872 100644 --- a/packages/vscode-webui/src/i18n/locales/en.json +++ b/packages/vscode-webui/src/i18n/locales/en.json @@ -5,7 +5,8 @@ "account": "Account", "help": "Help", "reset": "Reset", - "clear": "Clear" + "clear": "Clear", + "close": "Close" }, "error": { "somethingWentWrong": "Something went wrong", @@ -593,6 +594,18 @@ "stopped": "Stopped", "openOutput": "Open output file" }, + "managePanel": { + "title": "Background Jobs", + "toggle": "Show background jobs", + "pochiGroup": "Background commands", + "openTerminal": "Open terminal", + "hideTerminal": "Hide terminal", + "copyCommand": "Copy command", + "kill": "Kill process", + "empty": "Nothing running in the background", + "seeMore": "See more", + "seeLess": "See less" + }, "forkTask": { "forkedTaskTitle": "Forked from {{taskTitle}}" }, diff --git a/packages/vscode-webui/src/i18n/locales/jp.json b/packages/vscode-webui/src/i18n/locales/jp.json index 626ce66bd2..fdd0e6f3ba 100644 --- a/packages/vscode-webui/src/i18n/locales/jp.json +++ b/packages/vscode-webui/src/i18n/locales/jp.json @@ -3,7 +3,8 @@ "disable": "無効化", "openFolder": "フォルダーを開く", "account": "アカウント", - "help": "ヘルプ" + "help": "ヘルプ", + "close": "閉じる" }, "error": { "somethingWentWrong": "問題が発生しました", @@ -592,6 +593,18 @@ "stopped": "停止", "openOutput": "出力ファイルを開く" }, + "managePanel": { + "title": "バックグラウンドジョブ", + "toggle": "バックグラウンドジョブを表示", + "pochiGroup": "バックグラウンドコマンド", + "openTerminal": "ターミナルを開く", + "hideTerminal": "ターミナルを非表示", + "copyCommand": "コマンドをコピー", + "kill": "プロセスを終了", + "empty": "バックグラウンドで実行中のものはありません", + "seeMore": "もっと見る", + "seeLess": "折りたたむ" + }, "forkTask": { "forkedTaskTitle": "フォーク 元: {{taskTitle}}" }, diff --git a/packages/vscode-webui/src/i18n/locales/ko.json b/packages/vscode-webui/src/i18n/locales/ko.json index 03c94b7a16..534bcda4d9 100644 --- a/packages/vscode-webui/src/i18n/locales/ko.json +++ b/packages/vscode-webui/src/i18n/locales/ko.json @@ -3,7 +3,8 @@ "disable": "비활성화", "openFolder": "폴더 열기", "account": "계정", - "help": "도움말" + "help": "도움말", + "close": "닫기" }, "error": { "somethingWentWrong": "문제가 발생했습니다", @@ -585,6 +586,18 @@ "stopped": "중지됨", "openOutput": "출력 파일 열기" }, + "managePanel": { + "title": "백그라운드 작업", + "toggle": "백그라운드 작업 보기", + "pochiGroup": "백그라운드 명령", + "openTerminal": "터미널 열기", + "hideTerminal": "터미널 숨기기", + "copyCommand": "명령 복사", + "kill": "프로세스 종료", + "empty": "백그라운드에서 실행 중인 항목이 없습니다", + "seeMore": "더 보기", + "seeLess": "간단히 보기" + }, "forkTask": { "forkedTaskTitle": "포크됨 원본: {{taskTitle}}" }, diff --git a/packages/vscode-webui/src/i18n/locales/zh.json b/packages/vscode-webui/src/i18n/locales/zh.json index b4edca98d5..2022015774 100644 --- a/packages/vscode-webui/src/i18n/locales/zh.json +++ b/packages/vscode-webui/src/i18n/locales/zh.json @@ -3,7 +3,8 @@ "disable": "禁用", "openFolder": "打开文件夹", "account": "账户", - "help": "帮助" + "help": "帮助", + "close": "关闭" }, "error": { "somethingWentWrong": "出错了", @@ -590,6 +591,18 @@ "stopped": "已停止", "openOutput": "打开输出文件" }, + "managePanel": { + "title": "后台任务", + "toggle": "查看后台任务", + "pochiGroup": "后台命令", + "openTerminal": "打开终端", + "hideTerminal": "隐藏终端", + "copyCommand": "复制命令", + "kill": "终止进程", + "empty": "当前没有后台任务", + "seeMore": "查看更多", + "seeLess": "收起" + }, "forkTask": { "forkedTaskTitle": "任务分支自:{{taskTitle}}" }, diff --git a/packages/vscode-webui/src/lib/background-job-status-label.ts b/packages/vscode-webui/src/lib/background-job-status-label.ts new file mode 100644 index 0000000000..8b6460a52e --- /dev/null +++ b/packages/vscode-webui/src/lib/background-job-status-label.ts @@ -0,0 +1,20 @@ +import type { TFunction } from "i18next"; + +/** + * How a finished background job is worded, wherever it is shown: next to a + * notification's title, and on the hover of a row in the manage panel. The + * exit code is the part a status colour cannot carry. + */ +export function getBackgroundJobStatusLabel( + status: "completed" | "failed" | "stopped", + exitCode: number | undefined, + t: TFunction, +): string { + return status === "completed" + ? t("backgroundJobNotifications.completed", { exitCode: exitCode ?? 0 }) + : status === "failed" + ? exitCode === undefined + ? t("backgroundJobNotifications.failedNoExit") + : t("backgroundJobNotifications.failed", { exitCode }) + : t("backgroundJobNotifications.stopped"); +}