From c3c80c69d0c4abab292e5718f2de25f1b490e597 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 12 Jun 2025 18:30:52 +0200 Subject: [PATCH 01/75] remove: envd gen + chore: fix test ts issues --- bun.lock | 119 ++-- package.json | 4 +- spec/envd/buf.gen.yaml | 21 - spec/envd/filesystem/filesystem.proto | 124 ---- .../envd/filesystem/filesystem_connect.ts | 118 ---- .../clients/envd/filesystem/filesystem_pb.ts | 616 ------------------ 6 files changed, 45 insertions(+), 957 deletions(-) delete mode 100644 spec/envd/buf.gen.yaml delete mode 100644 spec/envd/filesystem/filesystem.proto delete mode 100644 src/lib/clients/envd/filesystem/filesystem_connect.ts delete mode 100644 src/lib/clients/envd/filesystem/filesystem_pb.ts diff --git a/bun.lock b/bun.lock index 59b196972..e43d1c5d9 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,6 @@ "": { "name": "@e2b/dashboard", "dependencies": { - "@connectrpc/connect": "^2.0.2", "@fumadocs/mdx-remote": "^1.2.0", "@google-cloud/storage": "^7.15.2", "@hookform/resolvers": "^3.10.0", @@ -47,6 +46,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", + "e2b": "^1.7.1", "fast-xml-parser": "^4.5.1", "fumadocs-core": "^15.0.6", "fumadocs-mdx": "^11.5.3", @@ -106,7 +106,6 @@ "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "^19.1.0-rc.2", "drizzle-kit": "^0.30.3", - "e2b": "^1.7.1", "eslint": "^9.19.0", "eslint-config-next": "^15.1.6", "eslint-config-prettier": "^10.0.1", @@ -169,11 +168,11 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="], + "@babel/helpers": ["@babel/helpers@7.28.2", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.2" } }, "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw=="], "@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="], - "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], + "@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], @@ -223,7 +222,7 @@ "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], - "@connectrpc/connect": ["@connectrpc/connect@2.0.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-jAbVMHVtDCydGt2P20VpmLjbLtERqSV0RMSyQF3k2zhK8pzQ2QaCAcyVhufClqrOAFZUKL5BqVYtttaxvhmRgg=="], + "@connectrpc/connect": ["@connectrpc/connect@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ=="], "@connectrpc/connect-web": ["@connectrpc/connect-web@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0", "@connectrpc/connect": "2.0.0-rc.3" } }, "sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw=="], @@ -309,7 +308,7 @@ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="], + "@eslint/js": ["@eslint/js@9.32.0", "", {}, "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg=="], "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], @@ -667,45 +666,45 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.1", "", { "os": "android", "cpu": "arm" }, "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.1", "", { "os": "android", "cpu": "arm" }, "sha512-oENme6QxtLCqjChRUUo3S6X8hjCXnWmJWnedD7VbGML5GUtaOtAyx+fEEXnBXVf0CBZApMQU0Idwi0FmyxzQhw=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.1", "", { "os": "android", "cpu": "arm64" }, "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.46.1", "", { "os": "android", "cpu": "arm64" }, "sha512-OikvNT3qYTl9+4qQ9Bpn6+XHM+ogtFadRLuT2EXiFQMiNkXFLQfNVppi5o28wvYdHL2s3fM0D/MZJ8UkNFZWsw=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.46.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EFYNNGij2WllnzljQDQnlFTXzSJw87cpAs4TVBAWLdkvic5Uh5tISrIL6NRcxoh/b2EFBG/TK8hgRrGx94zD4A=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.46.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZaNH06O1KeTug9WI2+GRBE5Ujt9kZw4a1+OIwnBHal92I8PxSsl5KpsrPvthRynkhMck4XPdvY0z26Cym/b7oA=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.46.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n4SLVebZP8uUlJ2r04+g2U/xFeiQlw09Me5UFqny8HGbARl503LNH5CqFTb5U5jNxTouhRjai6qPT0CR5c/Iig=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.46.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8vu9c02F16heTqpvo3yeiu7Vi1REDEC/yES/dIfq3tSXe6mLndiwvYr3AAvd1tMNUqE9yeGYa5w7PRbI5QUV+w=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.46.1", "", { "os": "linux", "cpu": "arm" }, "sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.46.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.46.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.46.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw=="], - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.46.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.46.1", "", { "os": "linux", "cpu": "none" }, "sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.46.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.46.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.46.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.46.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.46.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-7Thzy9TMXDw9AU4f4vsLNBxh7/VOKuXi73VH3d/kHGr0tZ3x/ewgL9uC7ojUKmH1/zvmZe2tLapYcZllk3SO8Q=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.1", "", { "os": "win32", "cpu": "x64" }, "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.46.1", "", { "os": "win32", "cpu": "x64" }, "sha512-7GVB4luhFmGUNXXJhH2jJwZCFB3pIOixv2E3s17GQHBFUOQaISlt7aGcQgqvCaDSxTZJUzlK/QJ1FN8S94MrzQ=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -847,9 +846,9 @@ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], - "@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], - "@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.3", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "redent": "^3.0.0" } }, "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA=="], + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.4", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ=="], "@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="], @@ -1167,7 +1166,7 @@ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], @@ -1263,7 +1262,7 @@ "chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="], - "chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="], @@ -1557,7 +1556,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ=="], + "eslint": ["eslint@9.32.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.32.0", "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg=="], "eslint-config-next": ["eslint-config-next@15.4.4", "", { "dependencies": { "@next/eslint-plugin-next": "15.4.4", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-sK/lWLUVF5om18O5w76Jt3F8uzu/LP5mVa6TprCMWkjWHUmByq80iHGHcdH7k1dLiJlj+DRIWf98d5piwRsSuA=="], @@ -1679,7 +1678,7 @@ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], - "framer-motion": ["framer-motion@12.23.9", "", { "dependencies": { "motion-dom": "^12.23.9", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-TqEHXj8LWfQSKqfdr5Y4mYltYLw96deu6/K9kGDd+ysqRJPNwF9nb5mZcrLmybHbU7gcJ+HQar41U3UTGanbbQ=="], + "framer-motion": ["framer-motion@12.23.10", "", { "dependencies": { "motion-dom": "^12.23.9", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ziXHr+C91FhgdSV65YA9SNbLy7uIDQ0pq7pEWlMP6Bh9UJAHFUNUvKWzE41g1B7YuvgJtUUNLgNmZyHd/YQ2gA=="], "fs-extra": ["fs-extra@4.0.3", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg=="], @@ -1687,11 +1686,11 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "fumadocs-core": ["fumadocs-core@15.6.5", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.11", "@shikijs/rehype": "^3.8.1", "@shikijs/transformers": "^3.8.1", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "react-remove-scroll": "^2.7.1", "remark": "^15.0.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.8.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@oramacloud/client": "1.x.x || 2.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x" }, "optionalPeers": ["@oramacloud/client", "@types/react", "algoliasearch", "next", "react", "react-dom"] }, "sha512-n+IXfJs+nQMpH2vC4g5ipfUhfZD+ML8tVUUW+Nsc5SddpVbxlYytP9PSJw3kdyfookiyZDhcpH5Jz8/G6pqXcg=="], + "fumadocs-core": ["fumadocs-core@15.6.6", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.11", "@shikijs/rehype": "^3.8.1", "@shikijs/transformers": "^3.8.1", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "react-remove-scroll": "^2.7.1", "remark": "^15.0.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.8.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@mixedbread/sdk": "^0.19.0", "@oramacloud/client": "1.x.x || 2.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x" }, "optionalPeers": ["@mixedbread/sdk", "@oramacloud/client", "@types/react", "algoliasearch", "next", "react", "react-dom"] }, "sha512-90sUbejUDevfDHykXXudw+3xTqYjuSZU1evhJiRBiZ0Oy0xQX4p5zPO48b5dhuVp44osvOH0ZKfHsVdkor6kZQ=="], - "fumadocs-mdx": ["fumadocs-mdx@11.7.0", "", { "dependencies": { "@mdx-js/mdx": "^3.1.0", "@standard-schema/spec": "^1.0.0", "chokidar": "^4.0.3", "esbuild": "^0.25.8", "estree-util-value-to-estree": "^3.4.0", "js-yaml": "^4.1.0", "lru-cache": "^11.1.0", "picocolors": "^1.1.1", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unist-util-visit": "^5.0.0", "zod": "^4.0.5" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "fumadocs-core": "^14.0.0 || ^15.0.0", "next": "^15.3.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "next", "react", "vite"], "bin": { "fumadocs-mdx": "bin.js" } }, "sha512-Cjel0WZHqKaRDxRK6yQW/bUnMMq3Sy+TL4U3S6A4Htwbc22qoPi/ZRz7kP2i43TEml/AVVpostu4XdjDRcWgbg=="], + "fumadocs-mdx": ["fumadocs-mdx@11.7.1", "", { "dependencies": { "@mdx-js/mdx": "^3.1.0", "@standard-schema/spec": "^1.0.0", "chokidar": "^4.0.3", "esbuild": "^0.25.8", "estree-util-value-to-estree": "^3.4.0", "js-yaml": "^4.1.0", "lru-cache": "^11.1.0", "picocolors": "^1.1.1", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unist-util-visit": "^5.0.0", "zod": "^4.0.10" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "fumadocs-core": "^14.0.0 || ^15.0.0", "next": "^15.3.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "next", "react", "vite"], "bin": { "fumadocs-mdx": "bin.js" } }, "sha512-zY2s3OP0XsNhayp1ac3Qz/xSZLdfjFE3zCCt+LDlwAfRwlpP8WdwfUNsPzZSnnXYigLl0oEQNuL+SF4ZgXctfQ=="], - "fumadocs-ui": ["fumadocs-ui@15.6.5", "", { "dependencies": { "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-presence": "^1.1.4", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.6.5", "lodash.merge": "^4.6.2", "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.0", "react-medium-image-zoom": "^5.3.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "tailwindcss": "^3.4.14 || ^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-YrVlHtXXW9Y+bo2lAoCj2ifpLmzRCWnMTY2QYpeHbnpIte3oJyAea3nhF+rpw3/XxE66Wjluzw/6GXOlpzcpvw=="], + "fumadocs-ui": ["fumadocs-ui@15.6.6", "", { "dependencies": { "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.13", "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-presence": "^1.1.4", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.6.6", "lodash.merge": "^4.6.2", "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.0", "react-medium-image-zoom": "^5.3.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "tailwindcss": "^3.4.14 || ^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-Ft/F8yrea7Z1kcI6NDFxKUwLiE4b0elvMDGfmJ/EZJHTuHfl5niXUUCCfvgkTDcZRYjPJktnMxC2msr78wWrzA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1937,7 +1936,7 @@ "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], "js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="], @@ -2195,7 +2194,7 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "motion": ["motion@12.23.9", "", { "dependencies": { "framer-motion": "^12.23.9", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5PDgsbNtZ4cpfew3STYL0p06rIiy8vOveQuQBXUAa2+m1WMzjf65DXYn6eo88dM2s+XLxAQq3ZiOjcnKMACEtQ=="], + "motion": ["motion@12.23.10", "", { "dependencies": { "framer-motion": "^12.23.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-pE3WsRXbRjVQaB2vCmVLt8gYiZdX11KYIoWblc49JnG2dTn6oHobIw+bORL31ZfiYBZD8BYmqkvEZp+HLPBBVw=="], "motion-dom": ["motion-dom@12.23.9", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-6Sv++iWS8XMFCgU1qwKj9l4xuC47Hp4+2jvPfyTXkqDg2tTzSgX6nWKD4kNFXk0k7llO59LZTPuJigza4A2K1A=="], @@ -2375,7 +2374,7 @@ "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], - "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.1.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0" }, "optionalPeers": ["vue-tsc"] }, "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A=="], + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.2.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg=="], "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="], @@ -2517,7 +2516,7 @@ "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], - "rollup": ["rollup@4.45.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.1", "@rollup/rollup-android-arm64": "4.45.1", "@rollup/rollup-darwin-arm64": "4.45.1", "@rollup/rollup-darwin-x64": "4.45.1", "@rollup/rollup-freebsd-arm64": "4.45.1", "@rollup/rollup-freebsd-x64": "4.45.1", "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", "@rollup/rollup-linux-arm-musleabihf": "4.45.1", "@rollup/rollup-linux-arm64-gnu": "4.45.1", "@rollup/rollup-linux-arm64-musl": "4.45.1", "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-musl": "4.45.1", "@rollup/rollup-linux-s390x-gnu": "4.45.1", "@rollup/rollup-linux-x64-gnu": "4.45.1", "@rollup/rollup-linux-x64-musl": "4.45.1", "@rollup/rollup-win32-arm64-msvc": "4.45.1", "@rollup/rollup-win32-ia32-msvc": "4.45.1", "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw=="], + "rollup": ["rollup@4.46.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.1", "@rollup/rollup-android-arm64": "4.46.1", "@rollup/rollup-darwin-arm64": "4.46.1", "@rollup/rollup-darwin-x64": "4.46.1", "@rollup/rollup-freebsd-arm64": "4.46.1", "@rollup/rollup-freebsd-x64": "4.46.1", "@rollup/rollup-linux-arm-gnueabihf": "4.46.1", "@rollup/rollup-linux-arm-musleabihf": "4.46.1", "@rollup/rollup-linux-arm64-gnu": "4.46.1", "@rollup/rollup-linux-arm64-musl": "4.46.1", "@rollup/rollup-linux-loongarch64-gnu": "4.46.1", "@rollup/rollup-linux-ppc64-gnu": "4.46.1", "@rollup/rollup-linux-riscv64-gnu": "4.46.1", "@rollup/rollup-linux-riscv64-musl": "4.46.1", "@rollup/rollup-linux-s390x-gnu": "4.46.1", "@rollup/rollup-linux-x64-gnu": "4.46.1", "@rollup/rollup-linux-x64-musl": "4.46.1", "@rollup/rollup-win32-arm64-msvc": "4.46.1", "@rollup/rollup-win32-ia32-msvc": "4.46.1", "@rollup/rollup-win32-x64-msvc": "4.46.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ=="], "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], @@ -2813,7 +2812,7 @@ "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], @@ -2887,8 +2886,6 @@ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], - "yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], @@ -2933,8 +2930,6 @@ "@iconify/utils/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], - "@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], "@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], @@ -2963,6 +2958,8 @@ "@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "@sentry/nextjs/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], + "@sentry/nextjs/resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="], "@sentry/node/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -2977,8 +2974,6 @@ "@shikijs/twoslash/@shikijs/types": ["@shikijs/types@3.2.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-/NTWAk4KE2M8uac0RhOsIhYQf4pdU0OywQuYDGIGAJ6Mjunxl2cGiuLkvu4HLCMn+OTTLRWkjZITp+aYJv60yA=="], - "@tailwindcss/node/jiti": ["jiti@2.5.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-NWDAhdnATItTnRhip9VTd8oXDjVcbhetRN6YzckApnXGxpGUooKMAaf0KVvlZG0+KlJMGkeLElVn4M1ReuxKUQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], @@ -2991,7 +2986,7 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@testing-library/dom/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -3031,12 +3026,8 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "e2b/@connectrpc/connect": ["@connectrpc/connect@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ=="], - "e2b/openapi-fetch": ["openapi-fetch@0.9.8", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.8" } }, "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg=="], - "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -3045,8 +3036,6 @@ "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "eslint-plugin-jsx-a11y/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -3081,20 +3070,12 @@ "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-diff/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-matcher-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-matcher-utils/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -3243,8 +3224,6 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@jest/types/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@redocly/openapi-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], @@ -3259,6 +3238,8 @@ "@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "@sentry/nextjs/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "@sentry/node/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@sentry/webpack-plugin/unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -3267,8 +3248,6 @@ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], - "@testing-library/dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "@types/jest/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3297,8 +3276,6 @@ "e2b/openapi-fetch/openapi-typescript-helpers": ["openapi-typescript-helpers@0.0.8", "", {}, "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g=="], - "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "extract-zip/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "fumadocs-core/@shikijs/rehype/@shikijs/types": ["@shikijs/types@3.8.1", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg=="], @@ -3353,20 +3330,12 @@ "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "jest-diff/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-diff/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "jest-matcher-utils/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-matcher-utils/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "jest-message-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "mermaid.cli/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "mermaid.cli/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], diff --git a/package.json b/package.json index 93b15a6a3..432282016 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "<<<<<< Gen": "", "generate:infra": "bunx openapi-typescript ./spec/openapi.yaml -o ./src/types/infra-api.d.ts", "generate:supabase": "bunx supabase@latest gen types typescript --schema public > src/types/database.types.ts --project-id $SUPABASE_PROJECT_ID", - "generate:envd": "buf generate --template ./spec/envd/buf.gen.yaml", "<<<<<< Scripts": "", "scripts:check-app-env": "bun scripts/check-app-env.ts", "scripts:check-e2e-env": "bun scripts/check-e2e-env.ts", @@ -38,7 +37,6 @@ "test:development:metrics": "vitest run src/__test__/development/metrics.test.ts" }, "dependencies": { - "@connectrpc/connect": "^2.0.2", "@fumadocs/mdx-remote": "^1.2.0", "@google-cloud/storage": "^7.15.2", "@hookform/resolvers": "^3.10.0", @@ -81,6 +79,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", + "e2b": "^1.7.1", "fast-xml-parser": "^4.5.1", "fumadocs-core": "^15.0.6", "fumadocs-mdx": "^11.5.3", @@ -141,7 +140,6 @@ "autoprefixer": "^10.4.20", "babel-plugin-react-compiler": "^19.1.0-rc.2", "drizzle-kit": "^0.30.3", - "e2b": "^1.7.1", "eslint": "^9.19.0", "eslint-config-next": "^15.1.6", "eslint-config-prettier": "^10.0.1", diff --git a/spec/envd/buf.gen.yaml b/spec/envd/buf.gen.yaml deleted file mode 100644 index a3317f9b4..000000000 --- a/spec/envd/buf.gen.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# buf.gen.yaml defines a local generation template. -# For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml -version: v2 -plugins: - - local: protoc-gen-es - out: ./src/lib/clients/envd - opt: - - target=ts - - local: protoc-gen-connect-es - out: ./src/lib/clients/envd - opt: - - target=ts - -managed: - enabled: true - override: - - file_option: optimize_for - value: SPEED - -inputs: - - directory: spec/envd diff --git a/spec/envd/filesystem/filesystem.proto b/spec/envd/filesystem/filesystem.proto deleted file mode 100644 index f80c37c17..000000000 --- a/spec/envd/filesystem/filesystem.proto +++ /dev/null @@ -1,124 +0,0 @@ -syntax = "proto3"; - -package filesystem; - -service Filesystem { - rpc Stat(StatRequest) returns (StatResponse); - rpc MakeDir(MakeDirRequest) returns (MakeDirResponse); - rpc Move(MoveRequest) returns (MoveResponse); - rpc ListDir(ListDirRequest) returns (ListDirResponse); - rpc Remove(RemoveRequest) returns (RemoveResponse); - - rpc WatchDir(WatchDirRequest) returns (stream WatchDirResponse); - - // Non-streaming versions of WatchDir - rpc CreateWatcher(CreateWatcherRequest) returns (CreateWatcherResponse); - rpc GetWatcherEvents(GetWatcherEventsRequest) returns (GetWatcherEventsResponse); - rpc RemoveWatcher(RemoveWatcherRequest) returns (RemoveWatcherResponse); -} - -message MoveRequest { - string source = 1; - string destination = 2; -} - -message MoveResponse { - EntryInfo entry = 1; -} - -message MakeDirRequest { - string path = 1; -} - -message MakeDirResponse { - EntryInfo entry = 1; -} - -message RemoveRequest { - string path = 1; -} - -message RemoveResponse {} - -message StatRequest { - string path = 1; -} - -message StatResponse { - EntryInfo entry = 1; -} - -message EntryInfo { - string name = 1; - FileType type = 2; - string path = 3; -} - -enum FileType { - FILE_TYPE_UNSPECIFIED = 0; - FILE_TYPE_FILE = 1; - FILE_TYPE_DIRECTORY = 2; -} - -message ListDirRequest { - string path = 1; - uint32 depth = 2; -} - -message ListDirResponse { - repeated EntryInfo entries = 1; -} - -message WatchDirRequest { - string path = 1; - bool recursive = 2; -} - -message FilesystemEvent { - string name = 1; - EventType type = 2; -} - -message WatchDirResponse { - oneof event { - StartEvent start = 1; - FilesystemEvent filesystem = 2; - KeepAlive keepalive = 3; - } - - message StartEvent {} - - message KeepAlive {} -} - -message CreateWatcherRequest { - string path = 1; - bool recursive = 2; -} - -message CreateWatcherResponse { - string watcher_id = 1; -} - -message GetWatcherEventsRequest { - string watcher_id = 1; -} - -message GetWatcherEventsResponse { - repeated FilesystemEvent events = 1; -} - -message RemoveWatcherRequest { - string watcher_id = 1; -} - -message RemoveWatcherResponse {} - -enum EventType { - EVENT_TYPE_UNSPECIFIED = 0; - EVENT_TYPE_CREATE = 1; - EVENT_TYPE_WRITE = 2; - EVENT_TYPE_REMOVE = 3; - EVENT_TYPE_RENAME = 4; - EVENT_TYPE_CHMOD = 5; -} diff --git a/src/lib/clients/envd/filesystem/filesystem_connect.ts b/src/lib/clients/envd/filesystem/filesystem_connect.ts deleted file mode 100644 index ba5a7636c..000000000 --- a/src/lib/clients/envd/filesystem/filesystem_connect.ts +++ /dev/null @@ -1,118 +0,0 @@ -// @generated by protoc-gen-connect-es v1.6.1 with parameter "target=ts" -// @generated from file filesystem/filesystem.proto (package filesystem, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import { MethodKind } from '@bufbuild/protobuf' -import { - CreateWatcherRequest, - CreateWatcherResponse, - GetWatcherEventsRequest, - GetWatcherEventsResponse, - ListDirRequest, - ListDirResponse, - MakeDirRequest, - MakeDirResponse, - MoveRequest, - MoveResponse, - RemoveRequest, - RemoveResponse, - RemoveWatcherRequest, - RemoveWatcherResponse, - StatRequest, - StatResponse, - WatchDirRequest, - WatchDirResponse, -} from './filesystem_pb.js' - -/** - * @generated from service filesystem.Filesystem - */ -export const Filesystem = { - typeName: 'filesystem.Filesystem', - methods: { - /** - * @generated from rpc filesystem.Filesystem.Stat - */ - stat: { - name: 'Stat', - I: StatRequest, - O: StatResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.MakeDir - */ - makeDir: { - name: 'MakeDir', - I: MakeDirRequest, - O: MakeDirResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.Move - */ - move: { - name: 'Move', - I: MoveRequest, - O: MoveResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.ListDir - */ - listDir: { - name: 'ListDir', - I: ListDirRequest, - O: ListDirResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.Remove - */ - remove: { - name: 'Remove', - I: RemoveRequest, - O: RemoveResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.WatchDir - */ - watchDir: { - name: 'WatchDir', - I: WatchDirRequest, - O: WatchDirResponse, - kind: MethodKind.ServerStreaming, - }, - /** - * Non-streaming versions of WatchDir - * - * @generated from rpc filesystem.Filesystem.CreateWatcher - */ - createWatcher: { - name: 'CreateWatcher', - I: CreateWatcherRequest, - O: CreateWatcherResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.GetWatcherEvents - */ - getWatcherEvents: { - name: 'GetWatcherEvents', - I: GetWatcherEventsRequest, - O: GetWatcherEventsResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc filesystem.Filesystem.RemoveWatcher - */ - removeWatcher: { - name: 'RemoveWatcher', - I: RemoveWatcherRequest, - O: RemoveWatcherResponse, - kind: MethodKind.Unary, - }, - }, -} as const diff --git a/src/lib/clients/envd/filesystem/filesystem_pb.ts b/src/lib/clients/envd/filesystem/filesystem_pb.ts deleted file mode 100644 index 618d11098..000000000 --- a/src/lib/clients/envd/filesystem/filesystem_pb.ts +++ /dev/null @@ -1,616 +0,0 @@ -// @generated by protoc-gen-es v2.5.2 with parameter "target=ts" -// @generated from file filesystem/filesystem.proto (package filesystem, syntax proto3) -/* eslint-disable */ - -import type { Message } from '@bufbuild/protobuf' -import type { - GenEnum, - GenFile, - GenMessage, - GenService, -} from '@bufbuild/protobuf/codegenv2' -import { - enumDesc, - fileDesc, - messageDesc, - serviceDesc, -} from '@bufbuild/protobuf/codegenv2' - -/** - * Describes the file filesystem/filesystem.proto. - */ -export const file_filesystem_filesystem: GenFile = - /*@__PURE__*/ - fileDesc( - 'ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iSwoJRW50cnlJbmZvEgwKBG5hbWUYASABKAkSIgoEdHlwZRgCIAEoDjIULmZpbGVzeXN0ZW0uRmlsZVR5cGUSDAoEcGF0aBgDIAEoCSItCg5MaXN0RGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJEg0KBWRlcHRoGAIgASgNIjkKD0xpc3REaXJSZXNwb25zZRImCgdlbnRyaWVzGAEgAygLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iMgoPV2F0Y2hEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIkQKD0ZpbGVzeXN0ZW1FdmVudBIMCgRuYW1lGAEgASgJEiMKBHR5cGUYAiABKA4yFS5maWxlc3lzdGVtLkV2ZW50VHlwZSLgAQoQV2F0Y2hEaXJSZXNwb25zZRI4CgVzdGFydBgBIAEoCzInLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZS5TdGFydEV2ZW50SAASMQoKZmlsZXN5c3RlbRgCIAEoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50SAASOwoJa2VlcGFsaXZlGAMgASgLMiYuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLktlZXBBbGl2ZUgAGgwKClN0YXJ0RXZlbnQaCwoJS2VlcEFsaXZlQgcKBWV2ZW50IjcKFENyZWF0ZVdhdGNoZXJSZXF1ZXN0EgwKBHBhdGgYASABKAkSEQoJcmVjdXJzaXZlGAIgASgIIisKFUNyZWF0ZVdhdGNoZXJSZXNwb25zZRISCgp3YXRjaGVyX2lkGAEgASgJIi0KF0dldFdhdGNoZXJFdmVudHNSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiRwoYR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlEisKBmV2ZW50cxgBIAMoCzIbLmZpbGVzeXN0ZW0uRmlsZXN5c3RlbUV2ZW50IioKFFJlbW92ZVdhdGNoZXJSZXF1ZXN0EhIKCndhdGNoZXJfaWQYASABKAkiFwoVUmVtb3ZlV2F0Y2hlclJlc3BvbnNlKlIKCEZpbGVUeXBlEhkKFUZJTEVfVFlQRV9VTlNQRUNJRklFRBAAEhIKDkZJTEVfVFlQRV9GSUxFEAESFwoTRklMRV9UWVBFX0RJUkVDVE9SWRACKpgBCglFdmVudFR5cGUSGgoWRVZFTlRfVFlQRV9VTlNQRUNJRklFRBAAEhUKEUVWRU5UX1RZUEVfQ1JFQVRFEAESFAoQRVZFTlRfVFlQRV9XUklURRACEhUKEUVWRU5UX1RZUEVfUkVNT1ZFEAMSFQoRRVZFTlRfVFlQRV9SRU5BTUUQBBIUChBFVkVOVF9UWVBFX0NITU9EEAUynwUKCkZpbGVzeXN0ZW0SOQoEU3RhdBIXLmZpbGVzeXN0ZW0uU3RhdFJlcXVlc3QaGC5maWxlc3lzdGVtLlN0YXRSZXNwb25zZRJCCgdNYWtlRGlyEhouZmlsZXN5c3RlbS5NYWtlRGlyUmVxdWVzdBobLmZpbGVzeXN0ZW0uTWFrZURpclJlc3BvbnNlEjkKBE1vdmUSFy5maWxlc3lzdGVtLk1vdmVSZXF1ZXN0GhguZmlsZXN5c3RlbS5Nb3ZlUmVzcG9uc2USQgoHTGlzdERpchIaLmZpbGVzeXN0ZW0uTGlzdERpclJlcXVlc3QaGy5maWxlc3lzdGVtLkxpc3REaXJSZXNwb25zZRI/CgZSZW1vdmUSGS5maWxlc3lzdGVtLlJlbW92ZVJlcXVlc3QaGi5maWxlc3lzdGVtLlJlbW92ZVJlc3BvbnNlEkcKCFdhdGNoRGlyEhsuZmlsZXN5c3RlbS5XYXRjaERpclJlcXVlc3QaHC5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UwARJUCg1DcmVhdGVXYXRjaGVyEiAuZmlsZXN5c3RlbS5DcmVhdGVXYXRjaGVyUmVxdWVzdBohLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlc3BvbnNlEl0KEEdldFdhdGNoZXJFdmVudHMSIy5maWxlc3lzdGVtLkdldFdhdGNoZXJFdmVudHNSZXF1ZXN0GiQuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVzcG9uc2USVAoNUmVtb3ZlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uUmVtb3ZlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXNwb25zZUJpCg5jb20uZmlsZXN5c3RlbUIPRmlsZXN5c3RlbVByb3RvUAGiAgNGWFiqAgpGaWxlc3lzdGVtygIKRmlsZXN5c3RlbeICFkZpbGVzeXN0ZW1cR1BCTWV0YWRhdGHqAgpGaWxlc3lzdGVtYgZwcm90bzM' - ) - -/** - * @generated from message filesystem.MoveRequest - */ -export type MoveRequest = Message<'filesystem.MoveRequest'> & { - /** - * @generated from field: string source = 1; - */ - source: string - - /** - * @generated from field: string destination = 2; - */ - destination: string -} - -/** - * Describes the message filesystem.MoveRequest. - * Use `create(MoveRequestSchema)` to create a new message. - */ -export const MoveRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 0) - -/** - * @generated from message filesystem.MoveResponse - */ -export type MoveResponse = Message<'filesystem.MoveResponse'> & { - /** - * @generated from field: filesystem.EntryInfo entry = 1; - */ - entry?: EntryInfo -} - -/** - * Describes the message filesystem.MoveResponse. - * Use `create(MoveResponseSchema)` to create a new message. - */ -export const MoveResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 1) - -/** - * @generated from message filesystem.MakeDirRequest - */ -export type MakeDirRequest = Message<'filesystem.MakeDirRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string -} - -/** - * Describes the message filesystem.MakeDirRequest. - * Use `create(MakeDirRequestSchema)` to create a new message. - */ -export const MakeDirRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 2) - -/** - * @generated from message filesystem.MakeDirResponse - */ -export type MakeDirResponse = Message<'filesystem.MakeDirResponse'> & { - /** - * @generated from field: filesystem.EntryInfo entry = 1; - */ - entry?: EntryInfo -} - -/** - * Describes the message filesystem.MakeDirResponse. - * Use `create(MakeDirResponseSchema)` to create a new message. - */ -export const MakeDirResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 3) - -/** - * @generated from message filesystem.RemoveRequest - */ -export type RemoveRequest = Message<'filesystem.RemoveRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string -} - -/** - * Describes the message filesystem.RemoveRequest. - * Use `create(RemoveRequestSchema)` to create a new message. - */ -export const RemoveRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 4) - -/** - * @generated from message filesystem.RemoveResponse - */ -export type RemoveResponse = Message<'filesystem.RemoveResponse'> & {} - -/** - * Describes the message filesystem.RemoveResponse. - * Use `create(RemoveResponseSchema)` to create a new message. - */ -export const RemoveResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 5) - -/** - * @generated from message filesystem.StatRequest - */ -export type StatRequest = Message<'filesystem.StatRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string -} - -/** - * Describes the message filesystem.StatRequest. - * Use `create(StatRequestSchema)` to create a new message. - */ -export const StatRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 6) - -/** - * @generated from message filesystem.StatResponse - */ -export type StatResponse = Message<'filesystem.StatResponse'> & { - /** - * @generated from field: filesystem.EntryInfo entry = 1; - */ - entry?: EntryInfo -} - -/** - * Describes the message filesystem.StatResponse. - * Use `create(StatResponseSchema)` to create a new message. - */ -export const StatResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 7) - -/** - * @generated from message filesystem.EntryInfo - */ -export type EntryInfo = Message<'filesystem.EntryInfo'> & { - /** - * @generated from field: string name = 1; - */ - name: string - - /** - * @generated from field: filesystem.FileType type = 2; - */ - type: FileType - - /** - * @generated from field: string path = 3; - */ - path: string -} - -/** - * Describes the message filesystem.EntryInfo. - * Use `create(EntryInfoSchema)` to create a new message. - */ -export const EntryInfoSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 8) - -/** - * @generated from message filesystem.ListDirRequest - */ -export type ListDirRequest = Message<'filesystem.ListDirRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string - - /** - * @generated from field: uint32 depth = 2; - */ - depth: number -} - -/** - * Describes the message filesystem.ListDirRequest. - * Use `create(ListDirRequestSchema)` to create a new message. - */ -export const ListDirRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 9) - -/** - * @generated from message filesystem.ListDirResponse - */ -export type ListDirResponse = Message<'filesystem.ListDirResponse'> & { - /** - * @generated from field: repeated filesystem.EntryInfo entries = 1; - */ - entries: EntryInfo[] -} - -/** - * Describes the message filesystem.ListDirResponse. - * Use `create(ListDirResponseSchema)` to create a new message. - */ -export const ListDirResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 10) - -/** - * @generated from message filesystem.WatchDirRequest - */ -export type WatchDirRequest = Message<'filesystem.WatchDirRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string - - /** - * @generated from field: bool recursive = 2; - */ - recursive: boolean -} - -/** - * Describes the message filesystem.WatchDirRequest. - * Use `create(WatchDirRequestSchema)` to create a new message. - */ -export const WatchDirRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 11) - -/** - * @generated from message filesystem.FilesystemEvent - */ -export type FilesystemEvent = Message<'filesystem.FilesystemEvent'> & { - /** - * @generated from field: string name = 1; - */ - name: string - - /** - * @generated from field: filesystem.EventType type = 2; - */ - type: EventType -} - -/** - * Describes the message filesystem.FilesystemEvent. - * Use `create(FilesystemEventSchema)` to create a new message. - */ -export const FilesystemEventSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 12) - -/** - * @generated from message filesystem.WatchDirResponse - */ -export type WatchDirResponse = Message<'filesystem.WatchDirResponse'> & { - /** - * @generated from oneof filesystem.WatchDirResponse.event - */ - event: - | { - /** - * @generated from field: filesystem.WatchDirResponse.StartEvent start = 1; - */ - value: WatchDirResponse_StartEvent - case: 'start' - } - | { - /** - * @generated from field: filesystem.FilesystemEvent filesystem = 2; - */ - value: FilesystemEvent - case: 'filesystem' - } - | { - /** - * @generated from field: filesystem.WatchDirResponse.KeepAlive keepalive = 3; - */ - value: WatchDirResponse_KeepAlive - case: 'keepalive' - } - | { case: undefined; value?: undefined } -} - -/** - * Describes the message filesystem.WatchDirResponse. - * Use `create(WatchDirResponseSchema)` to create a new message. - */ -export const WatchDirResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 13) - -/** - * @generated from message filesystem.WatchDirResponse.StartEvent - */ -export type WatchDirResponse_StartEvent = - Message<'filesystem.WatchDirResponse.StartEvent'> & {} - -/** - * Describes the message filesystem.WatchDirResponse.StartEvent. - * Use `create(WatchDirResponse_StartEventSchema)` to create a new message. - */ -export const WatchDirResponse_StartEventSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 13, 0) - -/** - * @generated from message filesystem.WatchDirResponse.KeepAlive - */ -export type WatchDirResponse_KeepAlive = - Message<'filesystem.WatchDirResponse.KeepAlive'> & {} - -/** - * Describes the message filesystem.WatchDirResponse.KeepAlive. - * Use `create(WatchDirResponse_KeepAliveSchema)` to create a new message. - */ -export const WatchDirResponse_KeepAliveSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 13, 1) - -/** - * @generated from message filesystem.CreateWatcherRequest - */ -export type CreateWatcherRequest = - Message<'filesystem.CreateWatcherRequest'> & { - /** - * @generated from field: string path = 1; - */ - path: string - - /** - * @generated from field: bool recursive = 2; - */ - recursive: boolean - } - -/** - * Describes the message filesystem.CreateWatcherRequest. - * Use `create(CreateWatcherRequestSchema)` to create a new message. - */ -export const CreateWatcherRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 14) - -/** - * @generated from message filesystem.CreateWatcherResponse - */ -export type CreateWatcherResponse = - Message<'filesystem.CreateWatcherResponse'> & { - /** - * @generated from field: string watcher_id = 1; - */ - watcherId: string - } - -/** - * Describes the message filesystem.CreateWatcherResponse. - * Use `create(CreateWatcherResponseSchema)` to create a new message. - */ -export const CreateWatcherResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 15) - -/** - * @generated from message filesystem.GetWatcherEventsRequest - */ -export type GetWatcherEventsRequest = - Message<'filesystem.GetWatcherEventsRequest'> & { - /** - * @generated from field: string watcher_id = 1; - */ - watcherId: string - } - -/** - * Describes the message filesystem.GetWatcherEventsRequest. - * Use `create(GetWatcherEventsRequestSchema)` to create a new message. - */ -export const GetWatcherEventsRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 16) - -/** - * @generated from message filesystem.GetWatcherEventsResponse - */ -export type GetWatcherEventsResponse = - Message<'filesystem.GetWatcherEventsResponse'> & { - /** - * @generated from field: repeated filesystem.FilesystemEvent events = 1; - */ - events: FilesystemEvent[] - } - -/** - * Describes the message filesystem.GetWatcherEventsResponse. - * Use `create(GetWatcherEventsResponseSchema)` to create a new message. - */ -export const GetWatcherEventsResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 17) - -/** - * @generated from message filesystem.RemoveWatcherRequest - */ -export type RemoveWatcherRequest = - Message<'filesystem.RemoveWatcherRequest'> & { - /** - * @generated from field: string watcher_id = 1; - */ - watcherId: string - } - -/** - * Describes the message filesystem.RemoveWatcherRequest. - * Use `create(RemoveWatcherRequestSchema)` to create a new message. - */ -export const RemoveWatcherRequestSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 18) - -/** - * @generated from message filesystem.RemoveWatcherResponse - */ -export type RemoveWatcherResponse = - Message<'filesystem.RemoveWatcherResponse'> & {} - -/** - * Describes the message filesystem.RemoveWatcherResponse. - * Use `create(RemoveWatcherResponseSchema)` to create a new message. - */ -export const RemoveWatcherResponseSchema: GenMessage = - /*@__PURE__*/ - messageDesc(file_filesystem_filesystem, 19) - -/** - * @generated from enum filesystem.FileType - */ -export enum FileType { - /** - * @generated from enum value: FILE_TYPE_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: FILE_TYPE_FILE = 1; - */ - FILE = 1, - - /** - * @generated from enum value: FILE_TYPE_DIRECTORY = 2; - */ - DIRECTORY = 2, -} - -/** - * Describes the enum filesystem.FileType. - */ -export const FileTypeSchema: GenEnum = - /*@__PURE__*/ - enumDesc(file_filesystem_filesystem, 0) - -/** - * @generated from enum filesystem.EventType - */ -export enum EventType { - /** - * @generated from enum value: EVENT_TYPE_UNSPECIFIED = 0; - */ - UNSPECIFIED = 0, - - /** - * @generated from enum value: EVENT_TYPE_CREATE = 1; - */ - CREATE = 1, - - /** - * @generated from enum value: EVENT_TYPE_WRITE = 2; - */ - WRITE = 2, - - /** - * @generated from enum value: EVENT_TYPE_REMOVE = 3; - */ - REMOVE = 3, - - /** - * @generated from enum value: EVENT_TYPE_RENAME = 4; - */ - RENAME = 4, - - /** - * @generated from enum value: EVENT_TYPE_CHMOD = 5; - */ - CHMOD = 5, -} - -/** - * Describes the enum filesystem.EventType. - */ -export const EventTypeSchema: GenEnum = - /*@__PURE__*/ - enumDesc(file_filesystem_filesystem, 1) - -/** - * @generated from service filesystem.Filesystem - */ -export const Filesystem: GenService<{ - /** - * @generated from rpc filesystem.Filesystem.Stat - */ - stat: { - methodKind: 'unary' - input: typeof StatRequestSchema - output: typeof StatResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.MakeDir - */ - makeDir: { - methodKind: 'unary' - input: typeof MakeDirRequestSchema - output: typeof MakeDirResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.Move - */ - move: { - methodKind: 'unary' - input: typeof MoveRequestSchema - output: typeof MoveResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.ListDir - */ - listDir: { - methodKind: 'unary' - input: typeof ListDirRequestSchema - output: typeof ListDirResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.Remove - */ - remove: { - methodKind: 'unary' - input: typeof RemoveRequestSchema - output: typeof RemoveResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.WatchDir - */ - watchDir: { - methodKind: 'server_streaming' - input: typeof WatchDirRequestSchema - output: typeof WatchDirResponseSchema - } - /** - * Non-streaming versions of WatchDir - * - * @generated from rpc filesystem.Filesystem.CreateWatcher - */ - createWatcher: { - methodKind: 'unary' - input: typeof CreateWatcherRequestSchema - output: typeof CreateWatcherResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.GetWatcherEvents - */ - getWatcherEvents: { - methodKind: 'unary' - input: typeof GetWatcherEventsRequestSchema - output: typeof GetWatcherEventsResponseSchema - } - /** - * @generated from rpc filesystem.Filesystem.RemoveWatcher - */ - removeWatcher: { - methodKind: 'unary' - input: typeof RemoveWatcherRequestSchema - output: typeof RemoveWatcherResponseSchema - } -}> = /*@__PURE__*/ serviceDesc(file_filesystem_filesystem, 0) From 8454a218ffe9858e2d7e9e59436d6b2fb84b871a Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 12:12:24 +0200 Subject: [PATCH 02/75] chore: move/rename server context --- src/app/dashboard/layout.tsx | 2 +- .../dashboard/server-context.tsx} | 0 src/lib/hooks/use-teams.ts | 2 +- src/lib/hooks/use-user.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/{lib/hooks/use-server-context.tsx => features/dashboard/server-context.tsx} (100%) diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 6f1cd3b34..a5ed3566e 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,7 +1,7 @@ import { COOKIE_KEYS } from '@/configs/keys' import { DashboardTitleProvider } from '@/features/dashboard/dashboard-title-provider' +import { ServerContextProvider } from '@/features/dashboard/server-context' import Sidebar from '@/features/dashboard/sidebar/sidebar' -import { ServerContextProvider } from '@/lib/hooks/use-server-context' import { resolveTeamIdInServerComponent, resolveTeamSlugInServerComponent, diff --git a/src/lib/hooks/use-server-context.tsx b/src/features/dashboard/server-context.tsx similarity index 100% rename from src/lib/hooks/use-server-context.tsx rename to src/features/dashboard/server-context.tsx diff --git a/src/lib/hooks/use-teams.ts b/src/lib/hooks/use-teams.ts index bc6faf6e1..67e0eb46e 100644 --- a/src/lib/hooks/use-teams.ts +++ b/src/lib/hooks/use-teams.ts @@ -1,4 +1,4 @@ -import { useServerContext } from './use-server-context' +import { useServerContext } from '../../features/dashboard/server-context' export const useTeams = () => { const { teams } = useServerContext() diff --git a/src/lib/hooks/use-user.ts b/src/lib/hooks/use-user.ts index e470b3ffa..6ee8baa27 100644 --- a/src/lib/hooks/use-user.ts +++ b/src/lib/hooks/use-user.ts @@ -1,6 +1,6 @@ 'use client' -import { useServerContext } from './use-server-context' +import { useServerContext } from '../../features/dashboard/server-context' export const useUser = () => { const { user } = useServerContext() From 4ca841c59b3a596dc237b6051fee662bbf07b34f Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 12:19:14 +0200 Subject: [PATCH 03/75] add: fs context,store & events-manager --- .../sandbox/filesystem/hooks/use-directory.ts | 65 ++++ .../filesystem/hooks/use-filesystem.ts | 36 ++ .../sandbox/filesystem/hooks/use-node.ts | 80 +++++ .../sandbox/filesystem/state/context.tsx | 182 ++++++++++ .../filesystem/state/events-manager.ts | 208 ++++++++++++ .../sandbox/filesystem/state/store.ts | 319 ++++++++++++++++++ .../sandbox/filesystem/state/types.ts | 31 ++ src/lib/utils/filesystem.ts | 105 ++++++ tsconfig.json | 1 - 9 files changed, 1026 insertions(+), 1 deletion(-) create mode 100644 src/features/dashboard/sandbox/filesystem/hooks/use-directory.ts create mode 100644 src/features/dashboard/sandbox/filesystem/hooks/use-filesystem.ts create mode 100644 src/features/dashboard/sandbox/filesystem/hooks/use-node.ts create mode 100644 src/features/dashboard/sandbox/filesystem/state/context.tsx create mode 100644 src/features/dashboard/sandbox/filesystem/state/events-manager.ts create mode 100644 src/features/dashboard/sandbox/filesystem/state/store.ts create mode 100644 src/features/dashboard/sandbox/filesystem/state/types.ts create mode 100644 src/lib/utils/filesystem.ts diff --git a/src/features/dashboard/sandbox/filesystem/hooks/use-directory.ts b/src/features/dashboard/sandbox/filesystem/hooks/use-directory.ts new file mode 100644 index 000000000..16f9ba931 --- /dev/null +++ b/src/features/dashboard/sandbox/filesystem/hooks/use-directory.ts @@ -0,0 +1,65 @@ +import { useMemo } from 'react' +import { useFilesystemContext } from '../state/context' +import { FileType } from 'e2b' +import { FilesystemNode } from '../state/types' + +/** + * Hook for accessing directory children with automatic updates + */ +export function useDirectoryChildren(path: string): FilesystemNode[] { + const { store } = useFilesystemContext() + + return store((state) => state.getChildren(path)) +} + +/** + * Hook for accessing directory state (expanded, loading, error) + */ +export function useDirectoryState(path: string) { + const { store } = useFilesystemContext() + + return store((state) => { + const node = state.getNode(path) + return { + isExpanded: state.isExpanded(path), + isLoading: state.loadingPaths.has(path), + hasError: state.errorPaths.has(path), + error: state.errorPaths.get(path), + isLoaded: node?.type === FileType.DIR ? !!node?.isLoaded : undefined, + hasChildren: state.hasChildren(path), + } + }) +} + +/** + * Hook for directory operations + */ +export function useDirectoryOperations(path: string) { + const { operations } = useFilesystemContext() + + return useMemo( + () => ({ + toggle: () => operations.toggleDirectory(path), + load: () => operations.loadDirectory(path), + refresh: () => operations.refreshDirectory(path), + watch: () => operations.watchDirectory(path), + unwatch: () => operations.unwatchDirectory(path), + }), + [operations, path] + ) +} + +/** + * Combined hook for directory data and operations + */ +export function useDirectory(path: string) { + const children = useDirectoryChildren(path) + const state = useDirectoryState(path) + const ops = useDirectoryOperations(path) + + return { + children, + ...state, + ...ops, + } +} diff --git a/src/features/dashboard/sandbox/filesystem/hooks/use-filesystem.ts b/src/features/dashboard/sandbox/filesystem/hooks/use-filesystem.ts new file mode 100644 index 000000000..b18ccacd0 --- /dev/null +++ b/src/features/dashboard/sandbox/filesystem/hooks/use-filesystem.ts @@ -0,0 +1,36 @@ +import { useFilesystemContext } from '../state/context' +import type { FilesystemOperations } from '../state/types' + +/** + * Main hook for accessing filesystem operations + */ +export function useFilesystem(): FilesystemOperations { + const { operations } = useFilesystemContext() + return operations +} + +/** + * Hook for accessing the raw Zustand store + * Use this when you need access to the full store API + */ +export function useFilesystemStore() { + const { store } = useFilesystemContext() + return store +} + +/** + * Hook for accessing the event manager + * Use this for advanced operations like custom watch handling + */ +export function useFilesystemEventManager() { + const { eventManager } = useFilesystemContext() + return eventManager +} + +/** + * Hook for accessing the sandbox connection + */ +export function useFilesystemSandbox() { + const { sandbox } = useFilesystemContext() + return sandbox +} diff --git a/src/features/dashboard/sandbox/filesystem/hooks/use-node.ts b/src/features/dashboard/sandbox/filesystem/hooks/use-node.ts new file mode 100644 index 000000000..9390dd68b --- /dev/null +++ b/src/features/dashboard/sandbox/filesystem/hooks/use-node.ts @@ -0,0 +1,80 @@ +import { useMemo } from 'react' +import { useFilesystemContext } from '../state/context' +import type { FilesystemNode } from '../state/types' + +/** + * Hook for accessing a specific filesystem node + */ +export function useFilesystemNode(path: string): FilesystemNode | undefined { + const { store } = useFilesystemContext() + + return store((state) => state.getNode(path)) +} + +/** + * Hook for accessing node selection state + */ +export function useNodeSelection(path: string) { + const { store, operations } = useFilesystemContext() + + const isSelected = store((state) => state.isSelected(path)) + + const select = useMemo( + () => () => operations.selectNode(path), + [operations, path] + ) + + return { + isSelected, + select, + } +} + +/** + * Combined hook for node data and operations + */ +export function useNode(path: string) { + const node = useFilesystemNode(path) + const selection = useNodeSelection(path) + + return { + node, + ...selection, + } +} + +/** + * Hook for getting root directory children (commonly used) + */ +export function useRootChildren() { + const { store } = useFilesystemContext() + + return store((state) => state.getChildren(state.rootPath)) +} + +/** + * Hook for getting selected node path + */ +export function useSelectedPath() { + const { store } = useFilesystemContext() + + return store((state) => state.selectedPath) +} + +/** + * Hook for getting all loading paths + */ +export function useLoadingPaths() { + const { store } = useFilesystemContext() + + return store((state) => Array.from(state.loadingPaths)) +} + +/** + * Hook for getting all error paths and their messages + */ +export function useErrorPaths() { + const { store } = useFilesystemContext() + + return store((state) => Object.fromEntries(state.errorPaths)) +} diff --git a/src/features/dashboard/sandbox/filesystem/state/context.tsx b/src/features/dashboard/sandbox/filesystem/state/context.tsx new file mode 100644 index 000000000..5d1decd2b --- /dev/null +++ b/src/features/dashboard/sandbox/filesystem/state/context.tsx @@ -0,0 +1,182 @@ +'use client' + +import React, { + createContext, + useContext, + useEffect, + useRef, + ReactNode, + useMemo, + useLayoutEffect, +} from 'react' +import { FileType, Sandbox, SandboxInfo } from 'e2b' +import { createFilesystemStore, type FilesystemStore } from './store' +import { FilesystemNode, FilesystemOperations } from './types' +import { FilesystemEventManager } from './events-manager' +import { getParentPath, normalizePath } from '@/lib/utils/filesystem' + +interface FilesystemContextValue { + store: FilesystemStore + operations: FilesystemOperations + sandbox: Sandbox + eventManager: FilesystemEventManager +} + +const FilesystemContext = createContext(null) + +interface FilesystemProviderProps { + children: ReactNode + sandboxInfo: SandboxInfo + rootPath: string +} + +export function FilesystemProvider({ + children, + sandboxInfo, + rootPath, +}: FilesystemProviderProps) { + const sandboxRef = useRef(null) + const storeRef = useRef(null) + const eventManagerRef = useRef(null) + + useLayoutEffect(() => { + if (sandboxRef.current) return + + Sandbox.connect(sandboxInfo.sandboxId).then((sandbox) => { + sandboxRef.current = sandbox + }) + }, [sandboxInfo.sandboxId]) + + useLayoutEffect(() => { + if (!sandboxRef.current || storeRef.current) return + + storeRef.current = createFilesystemStore(rootPath) + eventManagerRef.current = new FilesystemEventManager( + storeRef.current, + sandboxRef.current + ) + }, [rootPath, sandboxRef, storeRef]) + + useLayoutEffect(() => { + const initializeRoot = async () => { + if (!storeRef.current || !eventManagerRef.current) return + + const state = storeRef.current.getState() + const normalizedRootPath = normalizePath(rootPath) + + if (!state.getNode(normalizedRootPath)) { + const rootName = + normalizedRootPath === '/' + ? '/' + : normalizedRootPath.split('/').pop() || '' + + const rootNode: FilesystemNode = { + name: rootName, + path: normalizedRootPath, + type: FileType.DIR, + isExpanded: true, + isLoaded: false, + children: [], + } + + const parentPath = getParentPath(normalizedRootPath) + state.addNodes(parentPath, [rootNode]) + } + + try { + await eventManagerRef.current.loadDirectory(normalizedRootPath) + await eventManagerRef.current.startWatching(normalizedRootPath) + } catch (error) { + console.error('Failed to initialize root directory:', error) + state.setError(normalizedRootPath, 'Failed to load root directory') + } + } + + initializeRoot() + + return () => { + if (eventManagerRef.current) { + eventManagerRef.current.stopAllWatching() + } + } + }, [rootPath, sandboxRef]) + + const operations = useMemo(() => { + if (!storeRef.current || !eventManagerRef.current) { + throw new Error('Filesystem store or event manager not initialized') + } + const eventManager = eventManagerRef.current + const store = storeRef.current + + return { + loadDirectory: async (path: string) => { + await eventManager.loadDirectory(path) + }, + watchDirectory: async (path: string) => { + await eventManager.startWatching(path) + }, + unwatchDirectory: (path: string) => { + eventManager.stopWatching(path) + }, + selectNode: (path: string) => { + store.getState().setSelected(path) + }, + toggleDirectory: async (path: string) => { + const normalizedPath = normalizePath(path) + const state = store.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== FileType.DIR) return + + const newExpandedState = !node.isExpanded + state.setExpanded(normalizedPath, newExpandedState) + + if (newExpandedState) { + if (!node.isLoaded) { + await eventManager.loadDirectory(normalizedPath) + } + if (!eventManager.isWatching(normalizedPath)) { + try { + await eventManager.startWatching(normalizedPath) + } catch (error) { + console.error( + `Failed to start watching ${normalizedPath}:`, + error + ) + } + } + } + }, + refreshDirectory: async (path: string) => { + await eventManager.refreshDirectory(path) + }, + } + }, []) + + if (!storeRef.current || !eventManagerRef.current || !sandboxRef.current) { + return null + } + + const contextValue: FilesystemContextValue = { + store: storeRef.current, + operations: operations, + sandbox: sandboxRef.current, + eventManager: eventManagerRef.current, + } + + return ( + + {children} + + ) +} + +export function useFilesystemContext(): FilesystemContextValue { + const context = useContext(FilesystemContext) + if (!context) { + throw new Error( + 'useFilesystemContext must be used within a FilesystemProvider' + ) + } + return context +} diff --git a/src/features/dashboard/sandbox/filesystem/state/events-manager.ts b/src/features/dashboard/sandbox/filesystem/state/events-manager.ts new file mode 100644 index 000000000..1df3fa77e --- /dev/null +++ b/src/features/dashboard/sandbox/filesystem/state/events-manager.ts @@ -0,0 +1,208 @@ +import { + FileType, + type Sandbox, + type FilesystemEvent, + type WatchHandle, + type EntryInfo, + FilesystemEventType, +} from 'e2b' +import type { FilesystemStore } from './store' +import { FilesystemNode } from './types' +import { normalizePath, joinPath } from '@/lib/utils/filesystem' + +export class FilesystemEventManager { + private watchHandles = new Map() + private store: FilesystemStore + private sandbox: Sandbox + + constructor(store: FilesystemStore, sandbox: Sandbox) { + this.store = store + this.sandbox = sandbox + } + + /** + * Start watching a directory for changes + */ + async startWatching(path: string): Promise { + const normalizedPath = normalizePath(path) + + // Don't start watching if already watching + if (this.watchHandles.has(normalizedPath)) { + return + } + + try { + const handle = await this.sandbox.files.watchDir( + normalizedPath, + (event) => this.handleFilesystemEvent(event, normalizedPath), + { recursive: false } + ) + + this.watchHandles.set(normalizedPath, handle) + + // Mark as watched in store + this.store.getState().watchedPaths.add(normalizedPath) + } catch (error) { + console.error(`Failed to start watching ${normalizedPath}:`, error) + throw error + } + } + + /** + * Stop watching a directory + */ + stopWatching(path: string): void { + const normalizedPath = normalizePath(path) + const handle = this.watchHandles.get(normalizedPath) + + if (handle) { + handle.stop() + this.watchHandles.delete(normalizedPath) + + // Remove from watched paths in store + this.store.getState().watchedPaths.delete(normalizedPath) + } + } + + /** + * Stop watching all directories + */ + stopAllWatching(): void { + for (const [path] of this.watchHandles) { + this.stopWatching(path) + } + } + + /** + * Handle incoming filesystem events + */ + private handleFilesystemEvent( + event: FilesystemEvent, + parentPath: string + ): void { + const { type, name } = event + const normalizedPath = normalizePath(joinPath(parentPath, name)) + + switch (type) { + case FilesystemEventType.CREATE: + case FilesystemEventType.REMOVE: + case FilesystemEventType.RENAME: + // A filesystem event occurred that changed the directory structure. + // We don't have enough information to granularly update the store (e.g. on CREATE, we don't know if it's a file or dir). + // The most robust approach is to refresh the parent directory's contents from the sandbox. + console.log( + `Filesystem event '${type}' for '${normalizedPath}', refreshing parent '${parentPath}'` + ) + this.refreshDirectory(parentPath) + break + + case FilesystemEventType.WRITE: + case FilesystemEventType.CHMOD: + // For now, we don't handle these events as they don't change the tree structure. + // We could potentially use them to update file-specific state in the future (e.g., last modified time). + break + } + } + + /** + * Load directory contents from the sandbox + */ + async loadDirectory(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + + // Check if already loaded or loading + const node = state.getNode(normalizedPath) + + if ( + !node || + node.type !== FileType.DIR || + node.isLoaded || + state.loadingPaths.has(normalizedPath) + ) + return + + // Set loading state + state.setLoading(normalizedPath, true) + state.setError(normalizedPath) // Clear any previous errors + + try { + const entries = await this.sandbox.files.list(normalizedPath) + + // Convert entries to filesystem nodes + const nodes: FilesystemNode[] = entries.map((entry: EntryInfo) => { + if (entry.type === FileType.DIR) { + return { + name: entry.name, + path: entry.path, + type: FileType.DIR, + isExpanded: false, + isSelected: false, + isLoaded: false, + children: [], + } + } else { + return { + name: entry.name, + path: entry.path, + type: FileType.FILE, + isSelected: false, + } + } + }) + + // Add nodes to store + state.addNodes(normalizedPath, nodes) + + // Mark directory as loaded + state.updateNode(normalizedPath, { isLoaded: true }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to load directory' + state.setError(normalizedPath, errorMessage) + console.error(`Failed to load directory ${normalizedPath}:`, error) + } finally { + state.setLoading(normalizedPath, false) + } + } + + /** + * Refresh directory contents (force reload) + */ + async refreshDirectory(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + + // Mark as not loaded to force refresh + state.updateNode(normalizedPath, { isLoaded: false }) + + // Clear existing children + const node = state.getNode(normalizedPath) + if (node && node.type === FileType.DIR) { + // Create a copy of children paths, as the store mutation will modify the original array + const childrenPaths = [...node.children] + // Remove all children from store, which will also recursively remove their descendants + for (const childPath of childrenPaths) { + state.removeNode(childPath) + } + } + + // Reload directory + await this.loadDirectory(normalizedPath) + } + + /** + * Check if a directory is being watched + */ + isWatching(path: string): boolean { + const normalizedPath = normalizePath(path) + return this.watchHandles.has(normalizedPath) + } + + /** + * Get all watched paths + */ + getWatchedPaths(): string[] { + return Array.from(this.watchHandles.keys()) + } +} diff --git a/src/features/dashboard/sandbox/filesystem/state/store.ts b/src/features/dashboard/sandbox/filesystem/state/store.ts new file mode 100644 index 000000000..b598ef6d2 --- /dev/null +++ b/src/features/dashboard/sandbox/filesystem/state/store.ts @@ -0,0 +1,319 @@ +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' +import { + normalizePath, + getParentPath, + isChildPath, +} from '@/lib/utils/filesystem' +import { FileType } from 'e2b' +import { FilesystemNode } from './types' + +interface FilesystemStatics { + rootPath: string +} + +// Mutable state +export interface FilesystemState { + nodes: Map + selectedPath?: string + watchedPaths: Set + loadingPaths: Set + errorPaths: Map +} + +// Mutations/actions that modify state +export interface FilesystemMutations { + addNodes: (parentPath: string, nodes: FilesystemNode[]) => void + removeNode: (path: string) => void + updateNode: (path: string, updates: Partial) => void + setExpanded: (path: string, expanded: boolean) => void + setSelected: (path: string) => void + setLoading: (path: string, loading: boolean) => void + setError: (path: string, error?: string) => void + reset: () => void +} + +// Computed/derived values +export interface FilesystemComputed { + getChildren: (path: string) => FilesystemNode[] + getNode: (path: string) => FilesystemNode | undefined + isExpanded: (path: string) => boolean + isSelected: (path: string) => boolean + hasChildren: (path: string) => boolean +} + +// Combined store type +export type FilesystemStoreData = FilesystemStatics & + FilesystemState & + FilesystemMutations & + FilesystemComputed + +export const createFilesystemStore = (rootPath: string) => + create()( + immer((set, get) => ({ + // statics + rootPath: normalizePath(rootPath), + + // core + nodes: new Map(), + watchedPaths: new Set(), + + // loading states + loadingPaths: new Set(), + errorPaths: new Map(), + + // actions + addNodes: (parentPath: string, nodes: FilesystemNode[]) => { + const normalizedParentPath = normalizePath(parentPath) + + set((state: FilesystemState) => { + // get or create parent node + let parentNode = state.nodes.get(normalizedParentPath) + + if (!parentNode) { + // create parent node if it doesn't exist + const parentName = + normalizedParentPath === '/' + ? '/' + : normalizedParentPath.split('/').pop() || '' + parentNode = { + name: parentName, + path: normalizedParentPath, + type: FileType.DIR, + isExpanded: false, + children: [], + } + state.nodes.set(normalizedParentPath, parentNode) + } + + if (parentNode.type === FileType.FILE) { + console.error('Parent node is a file', parentNode) + return + } + + // Ensure parent has children array + if (!parentNode.children) { + parentNode.children = [] + } + + // Add new nodes + for (const node of nodes) { + const normalizedPath = normalizePath(node.path) + + // Add to nodes map + state.nodes.set(normalizedPath, { + ...node, + path: normalizedPath, + }) + + // Add to parent's children if not already there and if it's not the parent itself + if ( + normalizedPath !== normalizedParentPath && + !parentNode.children.includes(normalizedPath) + ) { + parentNode.children.push(normalizedPath) + } + } + + // Sort children by type (directories first) then by name + parentNode.children.sort((a: string, b: string) => { + const nodeA = state.nodes.get(a) + const nodeB = state.nodes.get(b) + + if (!nodeA || !nodeB) return 0 + + // Directories first + if (nodeA.type === 'dir' && nodeB.type === 'file') return -1 + if (nodeA.type === 'file' && nodeB.type === 'dir') return 1 + + // Then alphabetically + return nodeA.name.localeCompare(nodeB.name) + }) + }) + }, + + removeNode: (path: string) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + const node = state.nodes.get(normalizedPath) + if (!node) return + + // Remove from parent's children + const parentPath = getParentPath(normalizedPath) + const parentNode = state.nodes.get(parentPath) + if (parentNode && parentNode.type === FileType.DIR) { + parentNode.children = parentNode.children.filter( + (childPath: string) => childPath !== normalizedPath + ) + } + + // Remove node and all its descendants + const toRemove = [normalizedPath] + for (const [nodePath] of state.nodes) { + if (isChildPath(normalizedPath, nodePath)) { + toRemove.push(nodePath) + } + } + + for (const pathToRemove of toRemove) { + state.nodes.delete(pathToRemove) + state.loadingPaths.delete(pathToRemove) + state.errorPaths.delete(pathToRemove) + state.watchedPaths.delete(pathToRemove) + + // Clear selection if removing selected node + if (state.selectedPath === pathToRemove) { + state.selectedPath = undefined + } + } + }) + }, + + updateNode: (path: string, updates: Partial) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + const node = state.nodes.get(normalizedPath) + if (node) { + Object.assign(node, updates) + } + }) + }, + + setExpanded: (path: string, expanded: boolean) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + const node = state.nodes.get(normalizedPath) + + if (!node) return + + if (node?.type === FileType.FILE) { + console.error('Cannot expand file', node) + return + } + + node.isExpanded = expanded + }) + }, + + setSelected: (path: string) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + // Clear previous selection + if (state.selectedPath) { + const prevNode = state.nodes.get(state.selectedPath) + + if (!prevNode) return + + prevNode.isSelected = false + } + + // Set new selection + const node = state.nodes.get(normalizedPath) + + if (!node) return + + node.isSelected = true + state.selectedPath = normalizedPath + }) + }, + + setLoading: (path: string, loading: boolean) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + if (loading) { + state.loadingPaths.add(normalizedPath) + } else { + state.loadingPaths.delete(normalizedPath) + } + + // Update node loading state + const node = state.nodes.get(normalizedPath) + + if (!node || node.type === FileType.FILE) return + + node.isLoading = loading + }) + }, + + setError: (path: string, error?: string) => { + const normalizedPath = normalizePath(path) + + set((state: FilesystemState) => { + if (error) { + state.errorPaths.set(normalizedPath, error) + } else { + state.errorPaths.delete(normalizedPath) + } + + // Update node error state + const node = state.nodes.get(normalizedPath) + + if (!node || node.type === FileType.FILE) return + + node.error = error + }) + }, + + reset: () => { + set((state: FilesystemState) => { + state.nodes.clear() + state.selectedPath = undefined + state.watchedPaths.clear() + state.loadingPaths.clear() + state.errorPaths.clear() + }) + }, + + // computed + getChildren: (path: string) => { + const normalizedPath = normalizePath(path) + const state = get() + const node = state.nodes.get(normalizedPath) + + if (!node || node.type === FileType.FILE) return [] + + return node.children + .map((childPath) => state.nodes.get(childPath)) + .filter((child): child is FilesystemNode => child !== undefined) + }, + + getNode: (path: string) => { + const normalizedPath = normalizePath(path) + return get().nodes.get(normalizedPath) + }, + + isExpanded: (path: string) => { + const normalizedPath = normalizePath(path) + const node = get().nodes.get(normalizedPath) + + if (!node || node.type === FileType.FILE) return false + + return !!node.isExpanded + }, + + isSelected: (path: string) => { + const normalizedPath = normalizePath(path) + const node = get().nodes.get(normalizedPath) + + if (!node) return false + + return !!node.isSelected + }, + + hasChildren: (path: string) => { + const normalizedPath = normalizePath(path) + const node = get().nodes.get(normalizedPath) + + if (!node || node.type === FileType.FILE) return false + + return node.children.length > 0 + }, + })) + ) + +export type FilesystemStore = ReturnType diff --git a/src/features/dashboard/sandbox/filesystem/state/types.ts b/src/features/dashboard/sandbox/filesystem/state/types.ts new file mode 100644 index 000000000..b8846450d --- /dev/null +++ b/src/features/dashboard/sandbox/filesystem/state/types.ts @@ -0,0 +1,31 @@ +import { FileType } from 'e2b' + +interface FilesystemDir { + type: FileType.DIR + name: string + path: string + children: string[] // paths of children + isExpanded?: boolean + isLoaded?: boolean + isSelected?: boolean + isLoading?: boolean + error?: string +} + +interface FilesystemFile { + type: FileType.FILE + name: string + path: string + isSelected?: boolean +} + +export type FilesystemNode = FilesystemDir | FilesystemFile + +export interface FilesystemOperations { + loadDirectory: (path: string) => Promise + watchDirectory: (path: string) => Promise + unwatchDirectory: (path: string) => void + selectNode: (path: string) => void + toggleDirectory: (path: string) => Promise + refreshDirectory: (path: string) => Promise +} diff --git a/src/lib/utils/filesystem.ts b/src/lib/utils/filesystem.ts new file mode 100644 index 000000000..7936ef962 --- /dev/null +++ b/src/lib/utils/filesystem.ts @@ -0,0 +1,105 @@ +/** + * Normalize a path by removing duplicate slashes and resolving . and .. segments + */ +export function normalizePath(path: string): string { + // Handle empty path + if (!path || path === '') return '/' + + // Ensure path starts with / + if (!path.startsWith('/')) { + path = '/' + path + } + + // Split path into segments + const segments = path + .split('/') + .filter((segment) => segment !== '' && segment !== '.') + const normalized: string[] = [] + + for (const segment of segments) { + if (segment === '..') { + // Pop the last segment if we have one (don't go above root) + if (normalized.length > 0) { + normalized.pop() + } + } else { + normalized.push(segment) + } + } + + // Join segments back together + const result = '/' + normalized.join('/') + + // Ensure we don't return empty string, always at least '/' + return result === '' ? '/' : result +} + +/** + * Get the parent directory of a path + */ +export function getParentPath(path: string): string { + const normalized = normalizePath(path) + if (normalized === '/') return '/' + + const lastSlashIndex = normalized.lastIndexOf('/') + if (lastSlashIndex === 0) return '/' + + return normalized.substring(0, lastSlashIndex) +} + +/** + * Get the basename (filename) of a path + */ +export function getBasename(path: string): string { + const normalized = normalizePath(path) + if (normalized === '/') return '/' + + const lastSlashIndex = normalized.lastIndexOf('/') + return normalized.substring(lastSlashIndex + 1) +} + +/** + * Join path segments together + */ +export function joinPath(...segments: string[]): string { + if (segments.length === 0) return '/' + + const joined = segments + .filter((segment) => segment !== '' && segment != null) + .join('/') + + return normalizePath(joined) +} + +/** + * Check if a path is a child of another path + */ +export function isChildPath(parentPath: string, childPath: string): boolean { + const normalizedParent = normalizePath(parentPath) + const normalizedChild = normalizePath(childPath) + + if (normalizedParent === normalizedChild) return false + + // Ensure parent ends with / for proper comparison + const parentWithSlash = + normalizedParent === '/' ? '/' : normalizedParent + '/' + + return normalizedChild.startsWith(parentWithSlash) +} + +/** + * Get the depth of a path (number of directory levels) + */ +export function getPathDepth(path: string): number { + const normalized = normalizePath(path) + if (normalized === '/') return 0 + + return normalized.split('/').length - 1 +} + +/** + * Check if a path is the root path + */ +export function isRootPath(path: string): boolean { + return normalizePath(path) === '/' +} diff --git a/tsconfig.json b/tsconfig.json index 211eb95eb..486e7fe99 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,6 @@ "moduleResolution": "bundler", "noUncheckedIndexedAccess": true, "resolveJsonModule": true, - "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ From 4c3ff78da1c87ba46c75da912df93784edd2f10b Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 13:09:48 +0200 Subject: [PATCH 04/75] update: e2b sdk to fix isolatedModules errors --- tsconfig.json | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 486e7fe99..1f5c78aa7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2020", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -20,9 +24,18 @@ } ], "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "isolatedModules": true }, - "include": ["next-env.d.ts", "src", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "src", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } From 1abe664f615acfc44e58e60b898f16f54847a8f0 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 13:14:06 +0200 Subject: [PATCH 05/75] chore: rename feature folder --- .../sandbox/{filesystem => inspect}/hooks/use-directory.ts | 0 .../sandbox/{filesystem => inspect}/hooks/use-filesystem.ts | 0 .../dashboard/sandbox/{filesystem => inspect}/hooks/use-node.ts | 0 .../dashboard/sandbox/{filesystem => inspect}/state/context.tsx | 0 .../sandbox/{filesystem => inspect}/state/events-manager.ts | 0 .../dashboard/sandbox/{filesystem => inspect}/state/store.ts | 0 .../dashboard/sandbox/{filesystem => inspect}/state/types.ts | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename src/features/dashboard/sandbox/{filesystem => inspect}/hooks/use-directory.ts (100%) rename src/features/dashboard/sandbox/{filesystem => inspect}/hooks/use-filesystem.ts (100%) rename src/features/dashboard/sandbox/{filesystem => inspect}/hooks/use-node.ts (100%) rename src/features/dashboard/sandbox/{filesystem => inspect}/state/context.tsx (100%) rename src/features/dashboard/sandbox/{filesystem => inspect}/state/events-manager.ts (100%) rename src/features/dashboard/sandbox/{filesystem => inspect}/state/store.ts (100%) rename src/features/dashboard/sandbox/{filesystem => inspect}/state/types.ts (100%) diff --git a/src/features/dashboard/sandbox/filesystem/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts similarity index 100% rename from src/features/dashboard/sandbox/filesystem/hooks/use-directory.ts rename to src/features/dashboard/sandbox/inspect/hooks/use-directory.ts diff --git a/src/features/dashboard/sandbox/filesystem/hooks/use-filesystem.ts b/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts similarity index 100% rename from src/features/dashboard/sandbox/filesystem/hooks/use-filesystem.ts rename to src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts diff --git a/src/features/dashboard/sandbox/filesystem/hooks/use-node.ts b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts similarity index 100% rename from src/features/dashboard/sandbox/filesystem/hooks/use-node.ts rename to src/features/dashboard/sandbox/inspect/hooks/use-node.ts diff --git a/src/features/dashboard/sandbox/filesystem/state/context.tsx b/src/features/dashboard/sandbox/inspect/state/context.tsx similarity index 100% rename from src/features/dashboard/sandbox/filesystem/state/context.tsx rename to src/features/dashboard/sandbox/inspect/state/context.tsx diff --git a/src/features/dashboard/sandbox/filesystem/state/events-manager.ts b/src/features/dashboard/sandbox/inspect/state/events-manager.ts similarity index 100% rename from src/features/dashboard/sandbox/filesystem/state/events-manager.ts rename to src/features/dashboard/sandbox/inspect/state/events-manager.ts diff --git a/src/features/dashboard/sandbox/filesystem/state/store.ts b/src/features/dashboard/sandbox/inspect/state/store.ts similarity index 100% rename from src/features/dashboard/sandbox/filesystem/state/store.ts rename to src/features/dashboard/sandbox/inspect/state/store.ts diff --git a/src/features/dashboard/sandbox/filesystem/state/types.ts b/src/features/dashboard/sandbox/inspect/state/types.ts similarity index 100% rename from src/features/dashboard/sandbox/filesystem/state/types.ts rename to src/features/dashboard/sandbox/inspect/state/types.ts From 6250725b689e773728d7c31e81fc917d886fd28d Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 13:45:01 +0200 Subject: [PATCH 06/75] chore: rename context --- .../sandbox/inspect/hooks/use-context.ts | 18 ++++++ .../sandbox/inspect/hooks/use-directory.ts | 8 +-- .../sandbox/inspect/hooks/use-filesystem.ts | 36 ------------ .../sandbox/inspect/hooks/use-node.ts | 14 ++--- .../inspect/hooks/use-sandbox-state.tsx | 39 +++++++++++++ .../sandbox/inspect/state/context.tsx | 58 +++++++++++++------ 6 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 src/features/dashboard/sandbox/inspect/hooks/use-context.ts delete mode 100644 src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts create mode 100644 src/features/dashboard/sandbox/inspect/hooks/use-sandbox-state.tsx diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-context.ts b/src/features/dashboard/sandbox/inspect/hooks/use-context.ts new file mode 100644 index 000000000..dc727c685 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-context.ts @@ -0,0 +1,18 @@ +import { useSandboxInspectContext } from '../state/context' +import type { FilesystemOperations } from '../state/types' + +/** + * Main hook for accessing filesystem operations + */ +export function useFilesystem(): FilesystemOperations { + const { operations } = useSandboxInspectContext() + return operations +} + +/** + * Hook for accessing the sandbox connection + */ +export function useSandboxInfo() { + const { sandboxInfo } = useSandboxInspectContext() + return sandboxInfo +} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts index 16f9ba931..533f70363 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { useFilesystemContext } from '../state/context' +import { useSandboxInspectContext } from '../state/context' import { FileType } from 'e2b' import { FilesystemNode } from '../state/types' @@ -7,7 +7,7 @@ import { FilesystemNode } from '../state/types' * Hook for accessing directory children with automatic updates */ export function useDirectoryChildren(path: string): FilesystemNode[] { - const { store } = useFilesystemContext() + const { store } = useSandboxInspectContext() return store((state) => state.getChildren(path)) } @@ -16,7 +16,7 @@ export function useDirectoryChildren(path: string): FilesystemNode[] { * Hook for accessing directory state (expanded, loading, error) */ export function useDirectoryState(path: string) { - const { store } = useFilesystemContext() + const { store } = useSandboxInspectContext() return store((state) => { const node = state.getNode(path) @@ -35,7 +35,7 @@ export function useDirectoryState(path: string) { * Hook for directory operations */ export function useDirectoryOperations(path: string) { - const { operations } = useFilesystemContext() + const { operations } = useSandboxInspectContext() return useMemo( () => ({ diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts b/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts deleted file mode 100644 index b18ccacd0..000000000 --- a/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useFilesystemContext } from '../state/context' -import type { FilesystemOperations } from '../state/types' - -/** - * Main hook for accessing filesystem operations - */ -export function useFilesystem(): FilesystemOperations { - const { operations } = useFilesystemContext() - return operations -} - -/** - * Hook for accessing the raw Zustand store - * Use this when you need access to the full store API - */ -export function useFilesystemStore() { - const { store } = useFilesystemContext() - return store -} - -/** - * Hook for accessing the event manager - * Use this for advanced operations like custom watch handling - */ -export function useFilesystemEventManager() { - const { eventManager } = useFilesystemContext() - return eventManager -} - -/** - * Hook for accessing the sandbox connection - */ -export function useFilesystemSandbox() { - const { sandbox } = useFilesystemContext() - return sandbox -} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts index 9390dd68b..001b67caf 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react' -import { useFilesystemContext } from '../state/context' +import { useSandboxInspectContext } from '../state/context' import type { FilesystemNode } from '../state/types' /** * Hook for accessing a specific filesystem node */ export function useFilesystemNode(path: string): FilesystemNode | undefined { - const { store } = useFilesystemContext() + const { store } = useSandboxInspectContext() return store((state) => state.getNode(path)) } @@ -15,7 +15,7 @@ export function useFilesystemNode(path: string): FilesystemNode | undefined { * Hook for accessing node selection state */ export function useNodeSelection(path: string) { - const { store, operations } = useFilesystemContext() + const { store, operations } = useSandboxInspectContext() const isSelected = store((state) => state.isSelected(path)) @@ -47,7 +47,7 @@ export function useNode(path: string) { * Hook for getting root directory children (commonly used) */ export function useRootChildren() { - const { store } = useFilesystemContext() + const { store } = useSandboxInspectContext() return store((state) => state.getChildren(state.rootPath)) } @@ -56,7 +56,7 @@ export function useRootChildren() { * Hook for getting selected node path */ export function useSelectedPath() { - const { store } = useFilesystemContext() + const { store } = useSandboxInspectContext() return store((state) => state.selectedPath) } @@ -65,7 +65,7 @@ export function useSelectedPath() { * Hook for getting all loading paths */ export function useLoadingPaths() { - const { store } = useFilesystemContext() + const { store } = useSandboxInspectContext() return store((state) => Array.from(state.loadingPaths)) } @@ -74,7 +74,7 @@ export function useLoadingPaths() { * Hook for getting all error paths and their messages */ export function useErrorPaths() { - const { store } = useFilesystemContext() + const { store } = useSandboxInspectContext() return store((state) => Object.fromEntries(state.errorPaths)) } diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-sandbox-state.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-sandbox-state.tsx new file mode 100644 index 000000000..4a9e2dfe5 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-sandbox-state.tsx @@ -0,0 +1,39 @@ +import { SandboxInfo } from 'e2b' +import { useLayoutEffect, useState } from 'react' + +export interface SandboxState { + secondsLeft: number + isRunning: boolean +} + +export function useSandboxState(sandboxInfo: SandboxInfo): SandboxState { + const [secondsLeft, setSecondsLeft] = useState(0) + const [isRunning, setIsRunning] = useState(false) + + useLayoutEffect(() => { + const interval = setInterval(() => { + const now = new Date() + + if (sandboxInfo.endAt <= now) { + setIsRunning(false) + setSecondsLeft(0) + clearInterval(interval) + } else { + setIsRunning(true) + } + + const diff = sandboxInfo.endAt.getTime() - now.getTime() + setSecondsLeft(Math.max(0, Math.floor(diff / 1000))) + }, 1000) + + return () => { + if (!interval) return + clearInterval(interval) + } + }, [sandboxInfo.sandboxId, sandboxInfo.endAt]) + + return { + secondsLeft, + isRunning, + } +} diff --git a/src/features/dashboard/sandbox/inspect/state/context.tsx b/src/features/dashboard/sandbox/inspect/state/context.tsx index 5d1decd2b..d4221c462 100644 --- a/src/features/dashboard/sandbox/inspect/state/context.tsx +++ b/src/features/dashboard/sandbox/inspect/state/context.tsx @@ -3,7 +3,6 @@ import React, { createContext, useContext, - useEffect, useRef, ReactNode, useMemo, @@ -14,38 +13,59 @@ import { createFilesystemStore, type FilesystemStore } from './store' import { FilesystemNode, FilesystemOperations } from './types' import { FilesystemEventManager } from './events-manager' import { getParentPath, normalizePath } from '@/lib/utils/filesystem' +import { supabase } from '@/lib/clients/supabase/client' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { SandboxState, useSandboxState } from '../hooks/use-sandbox-state' -interface FilesystemContextValue { +interface SandboxInspectContextValue { store: FilesystemStore operations: FilesystemOperations - sandbox: Sandbox + sandboxInfo: SandboxInfo eventManager: FilesystemEventManager } -const FilesystemContext = createContext(null) +const SandboxInspectContext = createContext( + null +) -interface FilesystemProviderProps { +interface SandboxInspectProviderProps { children: ReactNode + teamId: string sandboxInfo: SandboxInfo rootPath: string } -export function FilesystemProvider({ +export function SandboxInspectProvider({ children, + teamId, sandboxInfo, rootPath, -}: FilesystemProviderProps) { +}: SandboxInspectProviderProps) { const sandboxRef = useRef(null) const storeRef = useRef(null) const eventManagerRef = useRef(null) useLayoutEffect(() => { - if (sandboxRef.current) return + if (sandboxRef.current || !teamId || !sandboxInfo.sandboxId) return + + const connectSandbox = async () => { + const accessToken = await supabase.auth.getSession().then(({ data }) => { + return data.session?.access_token + }) + + if (!accessToken) { + throw new Error('No access token found') + } + + sandboxRef.current = await Sandbox.connect(sandboxInfo.sandboxId, { + headers: { + ...SUPABASE_AUTH_HEADERS(accessToken, teamId), + }, + }) + } - Sandbox.connect(sandboxInfo.sandboxId).then((sandbox) => { - sandboxRef.current = sandbox - }) - }, [sandboxInfo.sandboxId]) + connectSandbox() + }, [sandboxInfo.sandboxId, teamId]) useLayoutEffect(() => { if (!sandboxRef.current || storeRef.current) return @@ -157,25 +177,25 @@ export function FilesystemProvider({ return null } - const contextValue: FilesystemContextValue = { + const contextValue: SandboxInspectContextValue = { store: storeRef.current, operations: operations, - sandbox: sandboxRef.current, + sandboxInfo: sandboxInfo, eventManager: eventManagerRef.current, } return ( - + {children} - + ) } -export function useFilesystemContext(): FilesystemContextValue { - const context = useContext(FilesystemContext) +export function useSandboxInspectContext(): SandboxInspectContextValue { + const context = useContext(SandboxInspectContext) if (!context) { throw new Error( - 'useFilesystemContext must be used within a FilesystemProvider' + 'useSandboxInspectContext must be used within a SandboxInspectProvider' ) } return context From 3243011a2b2d260892ca3ba5bed54758bd6ccf4e Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 14:27:31 +0200 Subject: [PATCH 07/75] add: sandbox context --- src/features/dashboard/sandbox/context.tsx | 81 +++++++++++++++++++ .../sandbox/inspect/hooks/use-directory.ts | 2 + .../{use-context.ts => use-filesystem.ts} | 10 +-- .../sandbox/inspect/hooks/use-node.ts | 2 + .../inspect/hooks/use-sandbox-state.tsx | 39 --------- .../sandbox/inspect/state/context.tsx | 15 ++-- .../dashboard/sandbox/inspect/state/store.ts | 2 + 7 files changed, 95 insertions(+), 56 deletions(-) create mode 100644 src/features/dashboard/sandbox/context.tsx rename src/features/dashboard/sandbox/inspect/hooks/{use-context.ts => use-filesystem.ts} (65%) delete mode 100644 src/features/dashboard/sandbox/inspect/hooks/use-sandbox-state.tsx diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx new file mode 100644 index 000000000..744a9e519 --- /dev/null +++ b/src/features/dashboard/sandbox/context.tsx @@ -0,0 +1,81 @@ +'use client' + +import React, { + createContext, + useContext, + ReactNode, + useLayoutEffect, + useState, +} from 'react' +import { SandboxInfo } from 'e2b' + +interface SandboxState { + secondsLeft: number + isRunning: boolean +} + +interface SandboxContextValue { + sandboxInfo: SandboxInfo + state: SandboxState +} + +const SandboxContext = createContext(null) + +export function useSandboxContext() { + const context = useContext(SandboxContext) + if (!context) { + throw new Error('useSandboxContext must be used within a SandboxProvider') + } + return context +} + +interface SandboxProviderProps { + children: ReactNode + sandboxInfo: SandboxInfo +} + +export function SandboxProvider({ + children, + sandboxInfo, +}: SandboxProviderProps) { + const [secondsLeft, setSecondsLeft] = useState(0) + const [isRunning, setIsRunning] = useState(false) + + useLayoutEffect(() => { + const interval = setInterval(() => { + const now = new Date() + + if (sandboxInfo.endAt <= now) { + setIsRunning(false) + setSecondsLeft(0) + clearInterval(interval) + } else { + setIsRunning(true) + } + + const diff = sandboxInfo.endAt.getTime() - now.getTime() + setSecondsLeft(Math.max(0, Math.floor(diff / 1000))) + }, 1000) + + return () => { + if (!interval) return + clearInterval(interval) + } + }, [sandboxInfo.sandboxId, sandboxInfo.endAt]) + + const state = { + secondsLeft, + isRunning, + } + + return ( + + {children} + + ) +} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts index 533f70363..2cd4906d7 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts @@ -1,3 +1,5 @@ +'use client' + import { useMemo } from 'react' import { useSandboxInspectContext } from '../state/context' import { FileType } from 'e2b' diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-context.ts b/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts similarity index 65% rename from src/features/dashboard/sandbox/inspect/hooks/use-context.ts rename to src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts index dc727c685..aa2476645 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-context.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts @@ -1,3 +1,5 @@ +'use client' + import { useSandboxInspectContext } from '../state/context' import type { FilesystemOperations } from '../state/types' @@ -8,11 +10,3 @@ export function useFilesystem(): FilesystemOperations { const { operations } = useSandboxInspectContext() return operations } - -/** - * Hook for accessing the sandbox connection - */ -export function useSandboxInfo() { - const { sandboxInfo } = useSandboxInspectContext() - return sandboxInfo -} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts index 001b67caf..503841e05 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts @@ -1,3 +1,5 @@ +'use client' + import { useMemo } from 'react' import { useSandboxInspectContext } from '../state/context' import type { FilesystemNode } from '../state/types' diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-sandbox-state.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-sandbox-state.tsx deleted file mode 100644 index 4a9e2dfe5..000000000 --- a/src/features/dashboard/sandbox/inspect/hooks/use-sandbox-state.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { SandboxInfo } from 'e2b' -import { useLayoutEffect, useState } from 'react' - -export interface SandboxState { - secondsLeft: number - isRunning: boolean -} - -export function useSandboxState(sandboxInfo: SandboxInfo): SandboxState { - const [secondsLeft, setSecondsLeft] = useState(0) - const [isRunning, setIsRunning] = useState(false) - - useLayoutEffect(() => { - const interval = setInterval(() => { - const now = new Date() - - if (sandboxInfo.endAt <= now) { - setIsRunning(false) - setSecondsLeft(0) - clearInterval(interval) - } else { - setIsRunning(true) - } - - const diff = sandboxInfo.endAt.getTime() - now.getTime() - setSecondsLeft(Math.max(0, Math.floor(diff / 1000))) - }, 1000) - - return () => { - if (!interval) return - clearInterval(interval) - } - }, [sandboxInfo.sandboxId, sandboxInfo.endAt]) - - return { - secondsLeft, - isRunning, - } -} diff --git a/src/features/dashboard/sandbox/inspect/state/context.tsx b/src/features/dashboard/sandbox/inspect/state/context.tsx index d4221c462..cd06f38e8 100644 --- a/src/features/dashboard/sandbox/inspect/state/context.tsx +++ b/src/features/dashboard/sandbox/inspect/state/context.tsx @@ -8,19 +8,17 @@ import React, { useMemo, useLayoutEffect, } from 'react' -import { FileType, Sandbox, SandboxInfo } from 'e2b' +import { FileType, Sandbox } from 'e2b' import { createFilesystemStore, type FilesystemStore } from './store' import { FilesystemNode, FilesystemOperations } from './types' import { FilesystemEventManager } from './events-manager' import { getParentPath, normalizePath } from '@/lib/utils/filesystem' import { supabase } from '@/lib/clients/supabase/client' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { SandboxState, useSandboxState } from '../hooks/use-sandbox-state' interface SandboxInspectContextValue { store: FilesystemStore operations: FilesystemOperations - sandboxInfo: SandboxInfo eventManager: FilesystemEventManager } @@ -30,15 +28,15 @@ const SandboxInspectContext = createContext( interface SandboxInspectProviderProps { children: ReactNode + sandboxId: string teamId: string - sandboxInfo: SandboxInfo rootPath: string } export function SandboxInspectProvider({ children, teamId, - sandboxInfo, + sandboxId, rootPath, }: SandboxInspectProviderProps) { const sandboxRef = useRef(null) @@ -46,7 +44,7 @@ export function SandboxInspectProvider({ const eventManagerRef = useRef(null) useLayoutEffect(() => { - if (sandboxRef.current || !teamId || !sandboxInfo.sandboxId) return + if (sandboxRef.current || !teamId) return const connectSandbox = async () => { const accessToken = await supabase.auth.getSession().then(({ data }) => { @@ -57,7 +55,7 @@ export function SandboxInspectProvider({ throw new Error('No access token found') } - sandboxRef.current = await Sandbox.connect(sandboxInfo.sandboxId, { + sandboxRef.current = await Sandbox.connect(sandboxId, { headers: { ...SUPABASE_AUTH_HEADERS(accessToken, teamId), }, @@ -65,7 +63,7 @@ export function SandboxInspectProvider({ } connectSandbox() - }, [sandboxInfo.sandboxId, teamId]) + }, [sandboxId, teamId]) useLayoutEffect(() => { if (!sandboxRef.current || storeRef.current) return @@ -180,7 +178,6 @@ export function SandboxInspectProvider({ const contextValue: SandboxInspectContextValue = { store: storeRef.current, operations: operations, - sandboxInfo: sandboxInfo, eventManager: eventManagerRef.current, } diff --git a/src/features/dashboard/sandbox/inspect/state/store.ts b/src/features/dashboard/sandbox/inspect/state/store.ts index b598ef6d2..d3945013f 100644 --- a/src/features/dashboard/sandbox/inspect/state/store.ts +++ b/src/features/dashboard/sandbox/inspect/state/store.ts @@ -1,3 +1,5 @@ +'use client' + import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' import { From 97fd5596618cf0086bafe289f0605930f7b0928a Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 14:29:34 +0200 Subject: [PATCH 08/75] chore: re-organize files --- .../dashboard/sandbox/inspect/{state => }/context.tsx | 6 +++--- .../sandbox/inspect/{state => filesystem}/events-manager.ts | 0 .../sandbox/inspect/{state => filesystem}/store.ts | 0 .../sandbox/inspect/{state => filesystem}/types.ts | 0 .../dashboard/sandbox/inspect/hooks/use-directory.ts | 4 ++-- .../dashboard/sandbox/inspect/hooks/use-filesystem.ts | 4 ++-- src/features/dashboard/sandbox/inspect/hooks/use-node.ts | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) rename src/features/dashboard/sandbox/inspect/{state => }/context.tsx (96%) rename src/features/dashboard/sandbox/inspect/{state => filesystem}/events-manager.ts (100%) rename src/features/dashboard/sandbox/inspect/{state => filesystem}/store.ts (100%) rename src/features/dashboard/sandbox/inspect/{state => filesystem}/types.ts (100%) diff --git a/src/features/dashboard/sandbox/inspect/state/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx similarity index 96% rename from src/features/dashboard/sandbox/inspect/state/context.tsx rename to src/features/dashboard/sandbox/inspect/context.tsx index cd06f38e8..e08f2d9f4 100644 --- a/src/features/dashboard/sandbox/inspect/state/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -9,9 +9,9 @@ import React, { useLayoutEffect, } from 'react' import { FileType, Sandbox } from 'e2b' -import { createFilesystemStore, type FilesystemStore } from './store' -import { FilesystemNode, FilesystemOperations } from './types' -import { FilesystemEventManager } from './events-manager' +import { createFilesystemStore, type FilesystemStore } from './filesystem/store' +import { FilesystemNode, FilesystemOperations } from './filesystem/types' +import { FilesystemEventManager } from './filesystem/events-manager' import { getParentPath, normalizePath } from '@/lib/utils/filesystem' import { supabase } from '@/lib/clients/supabase/client' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' diff --git a/src/features/dashboard/sandbox/inspect/state/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts similarity index 100% rename from src/features/dashboard/sandbox/inspect/state/events-manager.ts rename to src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts diff --git a/src/features/dashboard/sandbox/inspect/state/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts similarity index 100% rename from src/features/dashboard/sandbox/inspect/state/store.ts rename to src/features/dashboard/sandbox/inspect/filesystem/store.ts diff --git a/src/features/dashboard/sandbox/inspect/state/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts similarity index 100% rename from src/features/dashboard/sandbox/inspect/state/types.ts rename to src/features/dashboard/sandbox/inspect/filesystem/types.ts diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts index 2cd4906d7..5808c6cc0 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts @@ -1,9 +1,9 @@ 'use client' import { useMemo } from 'react' -import { useSandboxInspectContext } from '../state/context' +import { useSandboxInspectContext } from '../context' import { FileType } from 'e2b' -import { FilesystemNode } from '../state/types' +import { FilesystemNode } from '../filesystem/types' /** * Hook for accessing directory children with automatic updates diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts b/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts index aa2476645..3718a80c0 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts @@ -1,7 +1,7 @@ 'use client' -import { useSandboxInspectContext } from '../state/context' -import type { FilesystemOperations } from '../state/types' +import { useSandboxInspectContext } from '../context' +import type { FilesystemOperations } from '../filesystem/types' /** * Main hook for accessing filesystem operations diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts index 503841e05..aac7bd30d 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts @@ -1,8 +1,8 @@ 'use client' import { useMemo } from 'react' -import { useSandboxInspectContext } from '../state/context' -import type { FilesystemNode } from '../state/types' +import { useSandboxInspectContext } from '../context' +import type { FilesystemNode } from '../filesystem/types' /** * Hook for accessing a specific filesystem node From 9f7ccb02ff4cdbafb2d19e5f4cfcac5642694371 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 14:39:26 +0200 Subject: [PATCH 09/75] refactor: move sandbox connection to sandbox context --- src/features/dashboard/sandbox/context.tsx | 32 ++++++++++++- .../dashboard/sandbox/inspect/context.tsx | 45 ++++--------------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index 744a9e519..93b900e63 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -7,7 +7,9 @@ import React, { useLayoutEffect, useState, } from 'react' -import { SandboxInfo } from 'e2b' +import { Sandbox, SandboxInfo } from 'e2b' +import { supabase } from '@/lib/clients/supabase/client' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' interface SandboxState { secondsLeft: number @@ -17,6 +19,7 @@ interface SandboxState { interface SandboxContextValue { sandboxInfo: SandboxInfo state: SandboxState + sandbox: Sandbox | null } const SandboxContext = createContext(null) @@ -32,14 +35,40 @@ export function useSandboxContext() { interface SandboxProviderProps { children: ReactNode sandboxInfo: SandboxInfo + teamId: string } export function SandboxProvider({ children, sandboxInfo, + teamId, }: SandboxProviderProps) { const [secondsLeft, setSecondsLeft] = useState(0) const [isRunning, setIsRunning] = useState(false) + const [sandbox, setSandbox] = useState(null) + + useLayoutEffect(() => { + if (sandbox || !teamId) return + + const connectSandbox = async () => { + const accessToken = await supabase.auth.getSession().then(({ data }) => { + return data.session?.access_token + }) + + if (!accessToken) { + throw new Error('No access token found') + } + + const sbx = await Sandbox.connect(sandboxInfo.sandboxId, { + headers: { + ...SUPABASE_AUTH_HEADERS(accessToken, teamId), + }, + }) + setSandbox(sbx) + } + + connectSandbox() + }, [sandboxInfo.sandboxId, teamId, sandbox]) useLayoutEffect(() => { const interval = setInterval(() => { @@ -73,6 +102,7 @@ export function SandboxProvider({ value={{ sandboxInfo, state, + sandbox, }} > {children} diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index e08f2d9f4..0d017fb47 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -8,13 +8,12 @@ import React, { useMemo, useLayoutEffect, } from 'react' -import { FileType, Sandbox } from 'e2b' +import { FileType } from 'e2b' import { createFilesystemStore, type FilesystemStore } from './filesystem/store' import { FilesystemNode, FilesystemOperations } from './filesystem/types' import { FilesystemEventManager } from './filesystem/events-manager' import { getParentPath, normalizePath } from '@/lib/utils/filesystem' -import { supabase } from '@/lib/clients/supabase/client' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { useSandboxContext } from '../context' interface SandboxInspectContextValue { store: FilesystemStore @@ -28,52 +27,24 @@ const SandboxInspectContext = createContext( interface SandboxInspectProviderProps { children: ReactNode - sandboxId: string - teamId: string rootPath: string } export function SandboxInspectProvider({ children, - teamId, - sandboxId, rootPath, }: SandboxInspectProviderProps) { - const sandboxRef = useRef(null) + const { sandbox } = useSandboxContext() const storeRef = useRef(null) const eventManagerRef = useRef(null) - useLayoutEffect(() => { - if (sandboxRef.current || !teamId) return - - const connectSandbox = async () => { - const accessToken = await supabase.auth.getSession().then(({ data }) => { - return data.session?.access_token - }) - - if (!accessToken) { - throw new Error('No access token found') - } - - sandboxRef.current = await Sandbox.connect(sandboxId, { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - } - - connectSandbox() - }, [sandboxId, teamId]) - - useLayoutEffect(() => { - if (!sandboxRef.current || storeRef.current) return - + if (!storeRef.current && sandbox) { storeRef.current = createFilesystemStore(rootPath) eventManagerRef.current = new FilesystemEventManager( storeRef.current, - sandboxRef.current + sandbox ) - }, [rootPath, sandboxRef, storeRef]) + } useLayoutEffect(() => { const initializeRoot = async () => { @@ -117,7 +88,7 @@ export function SandboxInspectProvider({ eventManagerRef.current.stopAllWatching() } } - }, [rootPath, sandboxRef]) + }, [rootPath, sandbox]) const operations = useMemo(() => { if (!storeRef.current || !eventManagerRef.current) { @@ -171,7 +142,7 @@ export function SandboxInspectProvider({ } }, []) - if (!storeRef.current || !eventManagerRef.current || !sandboxRef.current) { + if (!storeRef.current || !eventManagerRef.current || !sandbox) { return null } From 57d53c0ea7abdefbac1e65c96792410c534aad4e Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 14:42:30 +0200 Subject: [PATCH 10/75] chore: syntax --- src/features/dashboard/sandbox/context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index 93b900e63..71d758632 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -18,8 +18,8 @@ interface SandboxState { interface SandboxContextValue { sandboxInfo: SandboxInfo - state: SandboxState sandbox: Sandbox | null + state: SandboxState } const SandboxContext = createContext(null) From 7ce857e6d820a57b8e5b6dce69568d4156a942a5 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 15:34:47 +0200 Subject: [PATCH 11/75] improve: watchHandle event handling --- .../inspect/filesystem/events-manager.ts | 89 +++++++++---------- .../sandbox/inspect/filesystem/store.ts | 32 ++----- 2 files changed, 46 insertions(+), 75 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index 1df3fa77e..ca9980b42 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -20,13 +20,9 @@ export class FilesystemEventManager { this.sandbox = sandbox } - /** - * Start watching a directory for changes - */ async startWatching(path: string): Promise { const normalizedPath = normalizePath(path) - // Don't start watching if already watching if (this.watchHandles.has(normalizedPath)) { return } @@ -39,8 +35,6 @@ export class FilesystemEventManager { ) this.watchHandles.set(normalizedPath, handle) - - // Mark as watched in store this.store.getState().watchedPaths.add(normalizedPath) } catch (error) { console.error(`Failed to start watching ${normalizedPath}:`, error) @@ -48,9 +42,6 @@ export class FilesystemEventManager { } } - /** - * Stop watching a directory - */ stopWatching(path: string): void { const normalizedPath = normalizePath(path) const handle = this.watchHandles.get(normalizedPath) @@ -58,24 +49,16 @@ export class FilesystemEventManager { if (handle) { handle.stop() this.watchHandles.delete(normalizedPath) - - // Remove from watched paths in store this.store.getState().watchedPaths.delete(normalizedPath) } } - /** - * Stop watching all directories - */ stopAllWatching(): void { for (const [path] of this.watchHandles) { this.stopWatching(path) } } - /** - * Handle incoming filesystem events - */ private handleFilesystemEvent( event: FilesystemEvent, parentPath: string @@ -85,33 +68,60 @@ export class FilesystemEventManager { switch (type) { case FilesystemEventType.CREATE: + console.log( + `Filesystem CREATE event for '${normalizedPath}', refreshing parent '${parentPath}'` + ) + void this.refreshDirectory(parentPath) + break + case FilesystemEventType.REMOVE: + console.log( + `Filesystem REMOVE event for '${normalizedPath}', removing node from store` + ) + this.handleRemoveEvent(normalizedPath, parentPath) + break + case FilesystemEventType.RENAME: - // A filesystem event occurred that changed the directory structure. - // We don't have enough information to granularly update the store (e.g. on CREATE, we don't know if it's a file or dir). - // The most robust approach is to refresh the parent directory's contents from the sandbox. console.log( - `Filesystem event '${type}' for '${normalizedPath}', refreshing parent '${parentPath}'` + `Filesystem RENAME event for '${normalizedPath}', refreshing parent '${parentPath}'` ) - this.refreshDirectory(parentPath) + void this.refreshDirectory(parentPath) break case FilesystemEventType.WRITE: case FilesystemEventType.CHMOD: - // For now, we don't handle these events as they don't change the tree structure. - // We could potentially use them to update file-specific state in the future (e.g., last modified time). + console.debug(`Ignoring ${type} event for '${normalizedPath}'`) + break + + default: + console.warn(`Unknown filesystem event type: ${type}`) break } } - /** - * Load directory contents from the sandbox - */ + private handleRemoveEvent(removedPath: string, parentPath: string): void { + const state = this.store.getState() + const node = state.getNode(removedPath) + + if (!node) { + console.debug( + `Node '${removedPath}' not found in store, skipping removal` + ) + return + } + + state.removeNode(removedPath) + console.log(`Successfully removed node '${removedPath}' from store`) + + if (node.type === FileType.DIR && this.isWatching(removedPath)) { + this.stopWatching(removedPath) + console.log(`Stopped watching removed directory '${removedPath}'`) + } + } + async loadDirectory(path: string): Promise { const normalizedPath = normalizePath(path) const state = this.store.getState() - - // Check if already loaded or loading const node = state.getNode(normalizedPath) if ( @@ -122,14 +132,12 @@ export class FilesystemEventManager { ) return - // Set loading state state.setLoading(normalizedPath, true) - state.setError(normalizedPath) // Clear any previous errors + state.setError(normalizedPath) // clear any previous errors try { const entries = await this.sandbox.files.list(normalizedPath) - // Convert entries to filesystem nodes const nodes: FilesystemNode[] = entries.map((entry: EntryInfo) => { if (entry.type === FileType.DIR) { return { @@ -151,10 +159,7 @@ export class FilesystemEventManager { } }) - // Add nodes to store state.addNodes(normalizedPath, nodes) - - // Mark directory as loaded state.updateNode(normalizedPath, { isLoaded: true }) } catch (error) { const errorMessage = @@ -166,42 +171,28 @@ export class FilesystemEventManager { } } - /** - * Refresh directory contents (force reload) - */ async refreshDirectory(path: string): Promise { const normalizedPath = normalizePath(path) const state = this.store.getState() - // Mark as not loaded to force refresh state.updateNode(normalizedPath, { isLoaded: false }) - // Clear existing children const node = state.getNode(normalizedPath) if (node && node.type === FileType.DIR) { - // Create a copy of children paths, as the store mutation will modify the original array const childrenPaths = [...node.children] - // Remove all children from store, which will also recursively remove their descendants for (const childPath of childrenPaths) { state.removeNode(childPath) } } - // Reload directory await this.loadDirectory(normalizedPath) } - /** - * Check if a directory is being watched - */ isWatching(path: string): boolean { const normalizedPath = normalizePath(path) return this.watchHandles.has(normalizedPath) } - /** - * Get all watched paths - */ getWatchedPaths(): string[] { return Array.from(this.watchHandles.keys()) } diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index d3945013f..22082ef08 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -14,7 +14,7 @@ interface FilesystemStatics { rootPath: string } -// Mutable state +// mutable state export interface FilesystemState { nodes: Map selectedPath?: string @@ -23,7 +23,7 @@ export interface FilesystemState { errorPaths: Map } -// Mutations/actions that modify state +// mutations/actions that modify state export interface FilesystemMutations { addNodes: (parentPath: string, nodes: FilesystemNode[]) => void removeNode: (path: string) => void @@ -35,7 +35,7 @@ export interface FilesystemMutations { reset: () => void } -// Computed/derived values +// computed/derived values export interface FilesystemComputed { getChildren: (path: string) => FilesystemNode[] getNode: (path: string) => FilesystemNode | undefined @@ -44,7 +44,7 @@ export interface FilesystemComputed { hasChildren: (path: string) => boolean } -// Combined store type +// combined store type export type FilesystemStoreData = FilesystemStatics & FilesystemState & FilesystemMutations & @@ -53,27 +53,20 @@ export type FilesystemStoreData = FilesystemStatics & export const createFilesystemStore = (rootPath: string) => create()( immer((set, get) => ({ - // statics rootPath: normalizePath(rootPath), - // core nodes: new Map(), watchedPaths: new Set(), - - // loading states loadingPaths: new Set(), errorPaths: new Map(), - // actions addNodes: (parentPath: string, nodes: FilesystemNode[]) => { const normalizedParentPath = normalizePath(parentPath) set((state: FilesystemState) => { - // get or create parent node let parentNode = state.nodes.get(normalizedParentPath) if (!parentNode) { - // create parent node if it doesn't exist const parentName = normalizedParentPath === '/' ? '/' @@ -93,22 +86,18 @@ export const createFilesystemStore = (rootPath: string) => return } - // Ensure parent has children array if (!parentNode.children) { parentNode.children = [] } - // Add new nodes for (const node of nodes) { const normalizedPath = normalizePath(node.path) - // Add to nodes map state.nodes.set(normalizedPath, { ...node, path: normalizedPath, }) - // Add to parent's children if not already there and if it's not the parent itself if ( normalizedPath !== normalizedParentPath && !parentNode.children.includes(normalizedPath) @@ -117,18 +106,17 @@ export const createFilesystemStore = (rootPath: string) => } } - // Sort children by type (directories first) then by name parentNode.children.sort((a: string, b: string) => { const nodeA = state.nodes.get(a) const nodeB = state.nodes.get(b) if (!nodeA || !nodeB) return 0 - // Directories first + // directories first if (nodeA.type === 'dir' && nodeB.type === 'file') return -1 if (nodeA.type === 'file' && nodeB.type === 'dir') return 1 - // Then alphabetically + // then alphabetically return nodeA.name.localeCompare(nodeB.name) }) }) @@ -141,7 +129,6 @@ export const createFilesystemStore = (rootPath: string) => const node = state.nodes.get(normalizedPath) if (!node) return - // Remove from parent's children const parentPath = getParentPath(normalizedPath) const parentNode = state.nodes.get(parentPath) if (parentNode && parentNode.type === FileType.DIR) { @@ -150,7 +137,6 @@ export const createFilesystemStore = (rootPath: string) => ) } - // Remove node and all its descendants const toRemove = [normalizedPath] for (const [nodePath] of state.nodes) { if (isChildPath(normalizedPath, nodePath)) { @@ -164,7 +150,6 @@ export const createFilesystemStore = (rootPath: string) => state.errorPaths.delete(pathToRemove) state.watchedPaths.delete(pathToRemove) - // Clear selection if removing selected node if (state.selectedPath === pathToRemove) { state.selectedPath = undefined } @@ -204,7 +189,6 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) set((state: FilesystemState) => { - // Clear previous selection if (state.selectedPath) { const prevNode = state.nodes.get(state.selectedPath) @@ -213,7 +197,6 @@ export const createFilesystemStore = (rootPath: string) => prevNode.isSelected = false } - // Set new selection const node = state.nodes.get(normalizedPath) if (!node) return @@ -233,7 +216,6 @@ export const createFilesystemStore = (rootPath: string) => state.loadingPaths.delete(normalizedPath) } - // Update node loading state const node = state.nodes.get(normalizedPath) if (!node || node.type === FileType.FILE) return @@ -252,7 +234,6 @@ export const createFilesystemStore = (rootPath: string) => state.errorPaths.delete(normalizedPath) } - // Update node error state const node = state.nodes.get(normalizedPath) if (!node || node.type === FileType.FILE) return @@ -271,7 +252,6 @@ export const createFilesystemStore = (rootPath: string) => }) }, - // computed getChildren: (path: string) => { const normalizedPath = normalizePath(path) const state = get() From f37c626476f55e0d0eea319474263be9adfeeea7 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 15:49:47 +0200 Subject: [PATCH 12/75] add: mermaid overview chart --- .../dashboard/sandbox/overview.mermaid | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 src/features/dashboard/sandbox/overview.mermaid diff --git a/src/features/dashboard/sandbox/overview.mermaid b/src/features/dashboard/sandbox/overview.mermaid new file mode 100644 index 000000000..aaa749137 --- /dev/null +++ b/src/features/dashboard/sandbox/overview.mermaid @@ -0,0 +1,167 @@ +flowchart TD +subgraph SANDBOX_LAYER["@sandbox — runtime lifecycle"] + direction TB + SP[/"SandboxProvider (React component)"/] + SC[(SandboxContext)] + SP -- "provides context value" --> SC + SP -- "establishes connection" --> SANDBOX_OBJ[("Sandbox class instance (e2b)")] + SP -- "manages timer" --> TIMER["Timer (useLayoutEffect)"] + noteSP["State: sandboxInfo (immutable), sandbox instance (useState), isRunning, secondsLeft"] +end + +subgraph INSPECT_LAYER["@inspect — filesystem inspection"] + direction TB + IP[/"SandboxInspectProvider"/] + IC[(InspectContext)] + IP -- "provides context value" --> IC + IP -- "initializes root directory" --> INIT["Root Initialization (useLayoutEffect)"] + noteIP["Creates: FilesystemStore (Zustand), FilesystemEventManager, operations object"] +end +SC -->|"consumes sandbox instance"| IP + +subgraph FILESYSTEM_LAYER["@filesystem — persistent tree state"] + direction TB + FS["FilesystemStore (Zustand)"] + FEM[["FilesystemEventManager (class)"]] + WATCHERS["Watch Handles Map"] + + FEM -- "calls mutations" --> FS + FEM -- "manages" --> WATCHERS + FS -- "provides state to" --> FEM + + subgraph FS_STATE["Store State Structure"] + NODES["nodes: Map"] + WATCHED["watchedPaths: Set"] + LOADING["loadingPaths: Set"] + ERRORS["errorPaths: Map"] + SELECTED["selectedPath: string"] + end + FS --> FS_STATE + + noteFS["Dual storage pattern: node properties + collections for performance"] +end + +IP -->|"creates & configures"| FS +IP -->|"creates with store + sandbox"| FEM + +subgraph HOOKS_LAYER["@hooks — typed selectors & helpers"] + direction TB + + subgraph CORE_HOOKS["Core Hooks"] + H1[[useFilesystem]] + H6[[useFilesystemNode]] + end + + subgraph DIRECTORY_HOOKS["Directory Hooks"] + H2[[useDirectoryChildren]] + H3[[useDirectoryState]] + H4[[useDirectoryOperations]] + H5[[useDirectory]] + end + + subgraph NODE_HOOKS["Node Hooks"] + H7[[useNodeSelection]] + H8[[useNode]] + end + + subgraph UTILITY_HOOKS["Utility Hooks"] + H9[[useRootChildren]] + H10[[useSelectedPath]] + H11[[useLoadingPaths]] + H12[[useErrorPaths]] + end +end + +IC -->|"exposes store & operations"| HOOKS_LAYER + +subgraph UI["UI components"] + direction LR + FT["FileTree Component"] + EDITOR["Code Editor Component"] + OTHER["Other UI Components"] +end + +subgraph OPERATIONS["Operations Object"] + direction TB + OP1["loadDirectory()"] + OP2["watchDirectory()"] + OP3["unwatchDirectory()"] + OP4["selectNode()"] + OP5["toggleDirectory()"] + OP6["refreshDirectory()"] +end + +HOOKS_LAYER --> UI +UI -- "subscribes to store state" --> FS +UI -- "calls operations" --> OPERATIONS +OPERATIONS -- "calls EventManager methods" --> FEM +OPERATIONS -- "calls store mutations" --> FS + +subgraph REMOTE["E2B cloud infrastructure"] + direction TB + SBOX[["Remote Sandbox Instance"]] + FS_API["Filesystem API"] + WATCH_API["Watch API"] + + SBOX --> FS_API + SBOX --> WATCH_API +end + +subgraph EVENT_FLOW["Event Processing"] + direction LR + CREATE["CREATE events"] + REMOVE["REMOVE events"] + RENAME["RENAME events"] + WRITE["WRITE/CHMOD events"] + + CREATE --> REFRESH["refreshDirectory()"] + REMOVE --> DIRECT["direct removeNode()"] + RENAME --> REFRESH + WRITE --> IGNORE["ignored"] +end + +FEM -- "sandbox.files.list()" --> FS_API +FEM -- "sandbox.files.watchDir()" --> WATCH_API +WATCH_API -- "filesystem events" --> EVENT_FLOW +EVENT_FLOW -- "processes events" --> FEM + +subgraph ERROR_HANDLING["Error Management"] + direction TB + STORE_ERRORS["Store Error State"] + UI_ERRORS["UI Error Display"] + FALLBACK["Fallback Mechanisms"] + + STORE_ERRORS --> UI_ERRORS + FEM --> STORE_ERRORS + OPERATIONS --> STORE_ERRORS +end + +subgraph PERFORMANCE["Performance Optimizations"] + direction TB + DUAL_STORAGE["Dual Storage Pattern"] + GRANULAR_SUBS["Granular Subscriptions"] + IMMER["Immer Middleware"] + VOID_ASYNC["Void Async in Events"] + + FS --> DUAL_STORAGE + HOOKS_LAYER --> GRANULAR_SUBS + FS --> IMMER + EVENT_FLOW --> VOID_ASYNC +end + +%% Styling +classDef providerClass fill:#E3F2FD,stroke:#1976D2,stroke-width:2px +classDef contextClass fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px +classDef storeClass fill:#E8F5E8,stroke:#388E3C,stroke-width:2px +classDef managerClass fill:#FFF3E0,stroke:#F57C00,stroke-width:2px +classDef hooksClass fill:#FCE4EC,stroke:#C2185B,stroke-width:2px +classDef uiClass fill:#F1F8E9,stroke:#689F38,stroke-width:2px +classDef remoteClass fill:#FFEBEE,stroke:#D32F2F,stroke-width:2px + +class SP,IP providerClass +class SC,IC contextClass +class FS storeClass +class FEM managerClass +class H1,H2,H3,H4,H5,H6,H7,H8,H9,H10,H11,H12 hooksClass +class FT,EDITOR,OTHER uiClass +class SBOX,FS_API,WATCH_API remoteClass \ No newline at end of file From 99bd9f304a0daf25dedd425a4e41ed93f48ee5cf Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 19:19:46 +0200 Subject: [PATCH 13/75] refactor: mermaid chart --- .../dashboard/sandbox/overview.mermaid | 208 ++++++------------ 1 file changed, 71 insertions(+), 137 deletions(-) diff --git a/src/features/dashboard/sandbox/overview.mermaid b/src/features/dashboard/sandbox/overview.mermaid index aaa749137..4698492f7 100644 --- a/src/features/dashboard/sandbox/overview.mermaid +++ b/src/features/dashboard/sandbox/overview.mermaid @@ -1,167 +1,101 @@ +--- +config: + theme: base + look: classic +--- flowchart TD -subgraph SANDBOX_LAYER["@sandbox — runtime lifecycle"] +subgraph SANDBOX_CONTEXT["Sandbox Context"] direction TB - SP[/"SandboxProvider (React component)"/] - SC[(SandboxContext)] - SP -- "provides context value" --> SC - SP -- "establishes connection" --> SANDBOX_OBJ[("Sandbox class instance (e2b)")] - SP -- "manages timer" --> TIMER["Timer (useLayoutEffect)"] - noteSP["State: sandboxInfo (immutable), sandbox instance (useState), isRunning, secondsLeft"] + SANDBOX_PROVIDER["SandboxProvider"] + SANDBOX_INSTANCE["Sandbox Instance"] + SANDBOX_STATE["Runtime State"] + + SANDBOX_PROVIDER -- "creates & manages" --> SANDBOX_INSTANCE + SANDBOX_PROVIDER -- "tracks lifecycle" --> SANDBOX_STATE end -subgraph INSPECT_LAYER["@inspect — filesystem inspection"] +subgraph INSPECT_CONTEXT["Inspect Context"] direction TB - IP[/"SandboxInspectProvider"/] - IC[(InspectContext)] - IP -- "provides context value" --> IC - IP -- "initializes root directory" --> INIT["Root Initialization (useLayoutEffect)"] - noteIP["Creates: FilesystemStore (Zustand), FilesystemEventManager, operations object"] + INSPECT_PROVIDER["SandboxInspectProvider"] + FILESYSTEM_STORE["FilesystemStore"] + EVENT_MANAGER["FilesystemEventManager"] + OPERATIONS["Operations Object"] + + INSPECT_PROVIDER -- "creates singleton" --> FILESYSTEM_STORE + INSPECT_PROVIDER -- "creates with store + sandbox" --> EVENT_MANAGER + INSPECT_PROVIDER -- "exposes interface" --> OPERATIONS + EVENT_MANAGER -- "mutates state via" --> FILESYSTEM_STORE + OPERATIONS -- "delegates to" --> EVENT_MANAGER + OPERATIONS -- "calls mutations on" --> FILESYSTEM_STORE end -SC -->|"consumes sandbox instance"| IP -subgraph FILESYSTEM_LAYER["@filesystem — persistent tree state"] +subgraph HOOKS["Hook Layer"] direction TB - FS["FilesystemStore (Zustand)"] - FEM[["FilesystemEventManager (class)"]] - WATCHERS["Watch Handles Map"] - - FEM -- "calls mutations" --> FS - FEM -- "manages" --> WATCHERS - FS -- "provides state to" --> FEM - - subgraph FS_STATE["Store State Structure"] - NODES["nodes: Map"] - WATCHED["watchedPaths: Set"] - LOADING["loadingPaths: Set"] - ERRORS["errorPaths: Map"] - SELECTED["selectedPath: string"] - end - FS --> FS_STATE - - noteFS["Dual storage pattern: node properties + collections for performance"] -end + FILESYSTEM_HOOKS["Filesystem Hooks"] + DIRECTORY_HOOKS["Directory Hooks"] + NODE_HOOKS["Node Hooks"] -IP -->|"creates & configures"| FS -IP -->|"creates with store + sandbox"| FEM + FILESYSTEM_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE + DIRECTORY_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE + NODE_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE -subgraph HOOKS_LAYER["@hooks — typed selectors & helpers"] - direction TB - - subgraph CORE_HOOKS["Core Hooks"] - H1[[useFilesystem]] - H6[[useFilesystemNode]] - end - - subgraph DIRECTORY_HOOKS["Directory Hooks"] - H2[[useDirectoryChildren]] - H3[[useDirectoryState]] - H4[[useDirectoryOperations]] - H5[[useDirectory]] - end - - subgraph NODE_HOOKS["Node Hooks"] - H7[[useNodeSelection]] - H8[[useNode]] - end - - subgraph UTILITY_HOOKS["Utility Hooks"] - H9[[useRootChildren]] - H10[[useSelectedPath]] - H11[[useLoadingPaths]] - H12[[useErrorPaths]] - end + FILESYSTEM_HOOKS -- "return operations" --> OPERATIONS + DIRECTORY_HOOKS -- "return operations" --> OPERATIONS + NODE_HOOKS -- "return operations" --> OPERATIONS end -IC -->|"exposes store & operations"| HOOKS_LAYER - -subgraph UI["UI components"] +subgraph UI_COMPONENTS["UI Components"] direction LR - FT["FileTree Component"] - EDITOR["Code Editor Component"] - OTHER["Other UI Components"] -end + FILE_TREE["FileTree"] + CODE_EDITOR["Code Editor"] + OTHER_UI["Other Components"] -subgraph OPERATIONS["Operations Object"] - direction TB - OP1["loadDirectory()"] - OP2["watchDirectory()"] - OP3["unwatchDirectory()"] - OP4["selectNode()"] - OP5["toggleDirectory()"] - OP6["refreshDirectory()"] + FILE_TREE -- "trigger user actions" --> USER_ACTIONS["User Actions"] + CODE_EDITOR -- "trigger user actions" --> USER_ACTIONS + OTHER_UI -- "trigger user actions" --> USER_ACTIONS end -HOOKS_LAYER --> UI -UI -- "subscribes to store state" --> FS -UI -- "calls operations" --> OPERATIONS -OPERATIONS -- "calls EventManager methods" --> FEM -OPERATIONS -- "calls store mutations" --> FS +subgraph E2B_REMOTE["E2B Remote"] + REMOTE_SANDBOX["Remote Sandbox"] + FS_EVENTS["Filesystem Events"] -subgraph REMOTE["E2B cloud infrastructure"] - direction TB - SBOX[["Remote Sandbox Instance"]] - FS_API["Filesystem API"] - WATCH_API["Watch API"] - - SBOX --> FS_API - SBOX --> WATCH_API + REMOTE_SANDBOX -- "emits real-time" --> FS_EVENTS end -subgraph EVENT_FLOW["Event Processing"] - direction LR - CREATE["CREATE events"] - REMOVE["REMOVE events"] - RENAME["RENAME events"] - WRITE["WRITE/CHMOD events"] - - CREATE --> REFRESH["refreshDirectory()"] - REMOVE --> DIRECT["direct removeNode()"] - RENAME --> REFRESH - WRITE --> IGNORE["ignored"] -end +%% Context Dependencies +SANDBOX_INSTANCE -- "provided to" --> INSPECT_PROVIDER -FEM -- "sandbox.files.list()" --> FS_API -FEM -- "sandbox.files.watchDir()" --> WATCH_API -WATCH_API -- "filesystem events" --> EVENT_FLOW -EVENT_FLOW -- "processes events" --> FEM +%% Data Flow: User Actions +USER_ACTIONS -- "call hooks that return" --> OPERATIONS +OPERATIONS -- "async calls to" --> EVENT_MANAGER +EVENT_MANAGER -- "API calls to" --> REMOTE_SANDBOX -subgraph ERROR_HANDLING["Error Management"] - direction TB - STORE_ERRORS["Store Error State"] - UI_ERRORS["UI Error Display"] - FALLBACK["Fallback Mechanisms"] - - STORE_ERRORS --> UI_ERRORS - FEM --> STORE_ERRORS - OPERATIONS --> STORE_ERRORS -end +%% Data Flow: Remote Events +FS_EVENTS -- "handled by" --> EVENT_MANAGER +EVENT_MANAGER -- "updates state in" --> FILESYSTEM_STORE +FILESYSTEM_STORE -- "triggers re-renders via" --> HOOKS +HOOKS -- "provide updated state to" --> UI_COMPONENTS -subgraph PERFORMANCE["Performance Optimizations"] - direction TB - DUAL_STORAGE["Dual Storage Pattern"] - GRANULAR_SUBS["Granular Subscriptions"] - IMMER["Immer Middleware"] - VOID_ASYNC["Void Async in Events"] - - FS --> DUAL_STORAGE - HOOKS_LAYER --> GRANULAR_SUBS - FS --> IMMER - EVENT_FLOW --> VOID_ASYNC -end +%% Hook Integration +HOOKS -- "consumed by" --> UI_COMPONENTS %% Styling -classDef providerClass fill:#E3F2FD,stroke:#1976D2,stroke-width:2px -classDef contextClass fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px +classDef contextClass fill:#E3F2FD,stroke:#1976D2,stroke-width:2px classDef storeClass fill:#E8F5E8,stroke:#388E3C,stroke-width:2px classDef managerClass fill:#FFF3E0,stroke:#F57C00,stroke-width:2px classDef hooksClass fill:#FCE4EC,stroke:#C2185B,stroke-width:2px classDef uiClass fill:#F1F8E9,stroke:#689F38,stroke-width:2px classDef remoteClass fill:#FFEBEE,stroke:#D32F2F,stroke-width:2px -class SP,IP providerClass -class SC,IC contextClass -class FS storeClass -class FEM managerClass -class H1,H2,H3,H4,H5,H6,H7,H8,H9,H10,H11,H12 hooksClass -class FT,EDITOR,OTHER uiClass -class SBOX,FS_API,WATCH_API remoteClass \ No newline at end of file +class SANDBOX_PROVIDER,INSPECT_PROVIDER contextClass +class FILESYSTEM_STORE storeClass +class EVENT_MANAGER,OPERATIONS managerClass +class FILESYSTEM_HOOKS,DIRECTORY_HOOKS,NODE_HOOKS hooksClass +class FILE_TREE,CODE_EDITOR,OTHER_UI,USER_ACTIONS uiClass +class REMOTE_SANDBOX,FS_EVENTS remoteClass + +click SANDBOX_PROVIDER href "javascript:void(0)" "Establishes E2B connection, manages auth & lifecycle" +click INSPECT_PROVIDER href "javascript:void(0)" "Orchestrates filesystem inspection subsystem" +click FILESYSTEM_STORE href "javascript:void(0)" "Zustand store - single source of truth for tree state" +click EVENT_MANAGER href "javascript:void(0)" "Bridges E2B filesystem events to store mutations" +click HOOKS href "javascript:void(0)" "Typed selectors and command interfaces for UI" \ No newline at end of file From 364dbdc1675a4a96a72d1dc76cc5c2e471698fef Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 13 Jun 2025 19:22:19 +0200 Subject: [PATCH 14/75] chore: cleanup mermaid chart --- src/features/dashboard/sandbox/overview.mermaid | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/features/dashboard/sandbox/overview.mermaid b/src/features/dashboard/sandbox/overview.mermaid index 4698492f7..465c51255 100644 --- a/src/features/dashboard/sandbox/overview.mermaid +++ b/src/features/dashboard/sandbox/overview.mermaid @@ -1,8 +1,3 @@ ---- -config: - theme: base - look: classic ---- flowchart TD subgraph SANDBOX_CONTEXT["Sandbox Context"] direction TB @@ -92,10 +87,4 @@ class FILESYSTEM_STORE storeClass class EVENT_MANAGER,OPERATIONS managerClass class FILESYSTEM_HOOKS,DIRECTORY_HOOKS,NODE_HOOKS hooksClass class FILE_TREE,CODE_EDITOR,OTHER_UI,USER_ACTIONS uiClass -class REMOTE_SANDBOX,FS_EVENTS remoteClass - -click SANDBOX_PROVIDER href "javascript:void(0)" "Establishes E2B connection, manages auth & lifecycle" -click INSPECT_PROVIDER href "javascript:void(0)" "Orchestrates filesystem inspection subsystem" -click FILESYSTEM_STORE href "javascript:void(0)" "Zustand store - single source of truth for tree state" -click EVENT_MANAGER href "javascript:void(0)" "Bridges E2B filesystem events to store mutations" -click HOOKS href "javascript:void(0)" "Typed selectors and command interfaces for UI" \ No newline at end of file +class REMOTE_SANDBOX,FS_EVENTS remoteClass \ No newline at end of file From 01b2083e51d8b83b5a963e1c09900b019eef2ade Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Wed, 18 Jun 2025 16:39:24 +0200 Subject: [PATCH 15/75] refactor: sandbox inspect application layer to use a single watch handler on root --- .../dashboard/sandbox/inspect/context.tsx | 22 +--- .../inspect/filesystem/events-manager.ts | 113 ++++++++---------- .../sandbox/inspect/filesystem/store.ts | 4 - .../sandbox/inspect/filesystem/types.ts | 2 - .../sandbox/inspect/hooks/use-directory.ts | 2 - .../dashboard/sandbox/overview.mermaid | 7 +- 6 files changed, 58 insertions(+), 92 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 0d017fb47..fe7b32a92 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -42,7 +42,8 @@ export function SandboxInspectProvider({ storeRef.current = createFilesystemStore(rootPath) eventManagerRef.current = new FilesystemEventManager( storeRef.current, - sandbox + sandbox, + rootPath ) } @@ -74,7 +75,6 @@ export function SandboxInspectProvider({ try { await eventManagerRef.current.loadDirectory(normalizedRootPath) - await eventManagerRef.current.startWatching(normalizedRootPath) } catch (error) { console.error('Failed to initialize root directory:', error) state.setError(normalizedRootPath, 'Failed to load root directory') @@ -85,7 +85,7 @@ export function SandboxInspectProvider({ return () => { if (eventManagerRef.current) { - eventManagerRef.current.stopAllWatching() + eventManagerRef.current.stopWatching() } } }, [rootPath, sandbox]) @@ -101,12 +101,6 @@ export function SandboxInspectProvider({ loadDirectory: async (path: string) => { await eventManager.loadDirectory(path) }, - watchDirectory: async (path: string) => { - await eventManager.startWatching(path) - }, - unwatchDirectory: (path: string) => { - eventManager.stopWatching(path) - }, selectNode: (path: string) => { store.getState().setSelected(path) }, @@ -124,16 +118,6 @@ export function SandboxInspectProvider({ if (!node.isLoaded) { await eventManager.loadDirectory(normalizedPath) } - if (!eventManager.isWatching(normalizedPath)) { - try { - await eventManager.startWatching(normalizedPath) - } catch (error) { - console.error( - `Failed to start watching ${normalizedPath}:`, - error - ) - } - } } }, refreshDirectory: async (path: string) => { diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index ca9980b42..669277ffd 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -8,84 +8,89 @@ import { } from 'e2b' import type { FilesystemStore } from './store' import { FilesystemNode } from './types' -import { normalizePath, joinPath } from '@/lib/utils/filesystem' +import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' export class FilesystemEventManager { - private watchHandles = new Map() + private watchHandle?: WatchHandle + private readonly rootPath: string private store: FilesystemStore private sandbox: Sandbox - constructor(store: FilesystemStore, sandbox: Sandbox) { + constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string) { this.store = store this.sandbox = sandbox - } + this.rootPath = normalizePath(rootPath) - async startWatching(path: string): Promise { - const normalizedPath = normalizePath(path) + // Immediately start a single recursive watcher at the root + void this.startRootWatcher() + } - if (this.watchHandles.has(normalizedPath)) { - return - } + private async startRootWatcher(): Promise { + if (this.watchHandle) return try { - const handle = await this.sandbox.files.watchDir( - normalizedPath, - (event) => this.handleFilesystemEvent(event, normalizedPath), - { recursive: false } + this.watchHandle = await this.sandbox.files.watchDir( + this.rootPath, + (event) => this.handleFilesystemEvent(event), + { recursive: true } ) - - this.watchHandles.set(normalizedPath, handle) - this.store.getState().watchedPaths.add(normalizedPath) } catch (error) { - console.error(`Failed to start watching ${normalizedPath}:`, error) + console.error(`Failed to start root watcher on ${this.rootPath}:`, error) throw error } } - stopWatching(path: string): void { - const normalizedPath = normalizePath(path) - const handle = this.watchHandles.get(normalizedPath) - - if (handle) { - handle.stop() - this.watchHandles.delete(normalizedPath) - this.store.getState().watchedPaths.delete(normalizedPath) - } - } - - stopAllWatching(): void { - for (const [path] of this.watchHandles) { - this.stopWatching(path) + stopWatching(): void { + if (this.watchHandle) { + this.watchHandle.stop() + this.watchHandle = undefined } } - private handleFilesystemEvent( - event: FilesystemEvent, - parentPath: string - ): void { + private handleFilesystemEvent(event: FilesystemEvent): void { const { type, name } = event - const normalizedPath = normalizePath(joinPath(parentPath, name)) + + // "name" is relative to the watched root; construct absolute path + const normalizedPath = normalizePath(joinPath(this.rootPath, name)) + const parentDir = normalizePath( + joinPath(this.rootPath, getParentPath(name)) + ) + + const state = this.store.getState() + const parentNode = state.getNode(parentDir) switch (type) { case FilesystemEventType.CREATE: + case FilesystemEventType.RENAME: + if ( + !parentNode || + parentNode.type !== FileType.DIR || + !parentNode.isLoaded + ) { + console.debug( + `Skip refresh for '${normalizedPath}' because parent directory '${parentDir}' does not exist in store` + ) + return + } + console.log( - `Filesystem CREATE event for '${normalizedPath}', refreshing parent '${parentPath}'` + `Filesystem ${type} event for '${normalizedPath}', refreshing parent '${parentDir}'` ) - void this.refreshDirectory(parentPath) + void this.refreshDirectory(parentDir) break case FilesystemEventType.REMOVE: - console.log( - `Filesystem REMOVE event for '${normalizedPath}', removing node from store` - ) - this.handleRemoveEvent(normalizedPath, parentPath) - break + if (!state.getNode(normalizedPath)) { + console.debug( + `Skip remove for '${normalizedPath}' because node does not exist in store` + ) + return + } - case FilesystemEventType.RENAME: console.log( - `Filesystem RENAME event for '${normalizedPath}', refreshing parent '${parentPath}'` + `Filesystem REMOVE event for '${normalizedPath}', removing node from store` ) - void this.refreshDirectory(parentPath) + this.handleRemoveEvent(normalizedPath) break case FilesystemEventType.WRITE: @@ -99,7 +104,7 @@ export class FilesystemEventManager { } } - private handleRemoveEvent(removedPath: string, parentPath: string): void { + private handleRemoveEvent(removedPath: string): void { const state = this.store.getState() const node = state.getNode(removedPath) @@ -112,11 +117,6 @@ export class FilesystemEventManager { state.removeNode(removedPath) console.log(`Successfully removed node '${removedPath}' from store`) - - if (node.type === FileType.DIR && this.isWatching(removedPath)) { - this.stopWatching(removedPath) - console.log(`Stopped watching removed directory '${removedPath}'`) - } } async loadDirectory(path: string): Promise { @@ -187,13 +187,4 @@ export class FilesystemEventManager { await this.loadDirectory(normalizedPath) } - - isWatching(path: string): boolean { - const normalizedPath = normalizePath(path) - return this.watchHandles.has(normalizedPath) - } - - getWatchedPaths(): string[] { - return Array.from(this.watchHandles.keys()) - } } diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 22082ef08..44096ee75 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -18,7 +18,6 @@ interface FilesystemStatics { export interface FilesystemState { nodes: Map selectedPath?: string - watchedPaths: Set loadingPaths: Set errorPaths: Map } @@ -56,7 +55,6 @@ export const createFilesystemStore = (rootPath: string) => rootPath: normalizePath(rootPath), nodes: new Map(), - watchedPaths: new Set(), loadingPaths: new Set(), errorPaths: new Map(), @@ -148,7 +146,6 @@ export const createFilesystemStore = (rootPath: string) => state.nodes.delete(pathToRemove) state.loadingPaths.delete(pathToRemove) state.errorPaths.delete(pathToRemove) - state.watchedPaths.delete(pathToRemove) if (state.selectedPath === pathToRemove) { state.selectedPath = undefined @@ -246,7 +243,6 @@ export const createFilesystemStore = (rootPath: string) => set((state: FilesystemState) => { state.nodes.clear() state.selectedPath = undefined - state.watchedPaths.clear() state.loadingPaths.clear() state.errorPaths.clear() }) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index b8846450d..4f5365a8e 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -23,8 +23,6 @@ export type FilesystemNode = FilesystemDir | FilesystemFile export interface FilesystemOperations { loadDirectory: (path: string) => Promise - watchDirectory: (path: string) => Promise - unwatchDirectory: (path: string) => void selectNode: (path: string) => void toggleDirectory: (path: string) => Promise refreshDirectory: (path: string) => Promise diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts index 5808c6cc0..5cb5ea3e0 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts @@ -44,8 +44,6 @@ export function useDirectoryOperations(path: string) { toggle: () => operations.toggleDirectory(path), load: () => operations.loadDirectory(path), refresh: () => operations.refreshDirectory(path), - watch: () => operations.watchDirectory(path), - unwatch: () => operations.unwatchDirectory(path), }), [operations, path] ) diff --git a/src/features/dashboard/sandbox/overview.mermaid b/src/features/dashboard/sandbox/overview.mermaid index 465c51255..650264b14 100644 --- a/src/features/dashboard/sandbox/overview.mermaid +++ b/src/features/dashboard/sandbox/overview.mermaid @@ -13,15 +13,15 @@ subgraph INSPECT_CONTEXT["Inspect Context"] direction TB INSPECT_PROVIDER["SandboxInspectProvider"] FILESYSTEM_STORE["FilesystemStore"] - EVENT_MANAGER["FilesystemEventManager"] + EVENT_MANAGER["FilesystemEventManager (root recursive watcher)"] OPERATIONS["Operations Object"] INSPECT_PROVIDER -- "creates singleton" --> FILESYSTEM_STORE INSPECT_PROVIDER -- "creates with store + sandbox" --> EVENT_MANAGER INSPECT_PROVIDER -- "exposes interface" --> OPERATIONS - EVENT_MANAGER -- "mutates state via" --> FILESYSTEM_STORE + EVENT_MANAGER -- "writes FS data" --> FILESYSTEM_STORE OPERATIONS -- "delegates to" --> EVENT_MANAGER - OPERATIONS -- "calls mutations on" --> FILESYSTEM_STORE + OPERATIONS -- "writes UI flags" --> FILESYSTEM_STORE end subgraph HOOKS["Hook Layer"] @@ -67,7 +67,6 @@ EVENT_MANAGER -- "API calls to" --> REMOTE_SANDBOX %% Data Flow: Remote Events FS_EVENTS -- "handled by" --> EVENT_MANAGER -EVENT_MANAGER -- "updates state in" --> FILESYSTEM_STORE FILESYSTEM_STORE -- "triggers re-renders via" --> HOOKS HOOKS -- "provide updated state to" --> UI_COMPONENTS From fbea1c64959b0f61e1c81edb54f7a5a6cfdc260b Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Wed, 18 Jun 2025 23:32:59 +0200 Subject: [PATCH 16/75] refactor: prepare state layer for react --- .../dashboard/sandbox/inspect/context.tsx | 105 ++++++++++-------- .../sandbox/inspect/filesystem/store.ts | 17 ++- .../sandbox/inspect/hooks/use-directory.ts | 31 ++++-- .../sandbox/inspect/hooks/use-node.ts | 13 ++- 4 files changed, 103 insertions(+), 63 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index fe7b32a92..b7d96643d 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -37,15 +37,59 @@ export function SandboxInspectProvider({ const { sandbox } = useSandboxContext() const storeRef = useRef(null) const eventManagerRef = useRef(null) + const operationsRef = useRef(null) - if (!storeRef.current && sandbox) { - storeRef.current = createFilesystemStore(rootPath) - eventManagerRef.current = new FilesystemEventManager( - storeRef.current, - sandbox, - rootPath - ) - } + useLayoutEffect(() => { + if (!storeRef.current && sandbox) { + storeRef.current = createFilesystemStore(rootPath) + eventManagerRef.current = new FilesystemEventManager( + storeRef.current, + sandbox, + rootPath + ) + + const eventManager = eventManagerRef.current + const store = storeRef.current + + operationsRef.current = { + loadDirectory: async (path: string) => { + await eventManager.loadDirectory(path) + }, + selectNode: (path: string) => { + store.getState().setSelected(path) + }, + toggleDirectory: async (path: string) => { + const normalizedPath = normalizePath(path) + const state = store.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== FileType.DIR) { + console.log(`Cannot toggle non-directory node at path: ${path}`) + return + } + + const newExpandedState = !node.isExpanded + console.log( + `Toggling directory ${path} to ${newExpandedState ? 'expanded' : 'collapsed'}` + ) + + state.setExpanded(normalizedPath, newExpandedState) + + if (newExpandedState) { + if (!node.isLoaded) { + console.log(`Loading unloaded directory: ${path}`) + await eventManager.loadDirectory(normalizedPath) + } else { + console.log(`Directory already loaded: ${path}`) + } + } + }, + refreshDirectory: async (path: string) => { + await eventManager.refreshDirectory(path) + }, + } + } + }, [sandbox, rootPath]) useLayoutEffect(() => { const initializeRoot = async () => { @@ -90,49 +134,18 @@ export function SandboxInspectProvider({ } }, [rootPath, sandbox]) - const operations = useMemo(() => { - if (!storeRef.current || !eventManagerRef.current) { - throw new Error('Filesystem store or event manager not initialized') - } - const eventManager = eventManagerRef.current - const store = storeRef.current - - return { - loadDirectory: async (path: string) => { - await eventManager.loadDirectory(path) - }, - selectNode: (path: string) => { - store.getState().setSelected(path) - }, - toggleDirectory: async (path: string) => { - const normalizedPath = normalizePath(path) - const state = store.getState() - const node = state.getNode(normalizedPath) - - if (!node || node.type !== FileType.DIR) return - - const newExpandedState = !node.isExpanded - state.setExpanded(normalizedPath, newExpandedState) - - if (newExpandedState) { - if (!node.isLoaded) { - await eventManager.loadDirectory(normalizedPath) - } - } - }, - refreshDirectory: async (path: string) => { - await eventManager.refreshDirectory(path) - }, - } - }, []) - - if (!storeRef.current || !eventManagerRef.current || !sandbox) { + if ( + !storeRef.current || + !eventManagerRef.current || + !sandbox || + !operationsRef.current + ) { return null } const contextValue: SandboxInspectContextValue = { store: storeRef.current, - operations: operations, + operations: operationsRef.current, eventManager: eventManagerRef.current, } diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 44096ee75..aa7789375 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' +import { enableMapSet } from 'immer' import { normalizePath, getParentPath, @@ -10,6 +11,8 @@ import { import { FileType } from 'e2b' import { FilesystemNode } from './types' +enableMapSet() + interface FilesystemStatics { rootPath: string } @@ -49,6 +52,10 @@ export type FilesystemStoreData = FilesystemStatics & FilesystemMutations & FilesystemComputed +// to retain reference-stable arrays of children per directory path +const childrenCache: Map = + new Map() + export const createFilesystemStore = (rootPath: string) => create()( immer((set, get) => ({ @@ -255,9 +262,17 @@ export const createFilesystemStore = (rootPath: string) => if (!node || node.type === FileType.FILE) return [] - return node.children + const cached = childrenCache.get(normalizedPath) + if (cached && cached.ref === node.children) { + return cached.result + } + + const result = node.children .map((childPath) => state.nodes.get(childPath)) .filter((child): child is FilesystemNode => child !== undefined) + + childrenCache.set(normalizedPath, { ref: node.children, result }) + return result }, getNode: (path: string) => { diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts index 5cb5ea3e0..6005d40cc 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts @@ -4,6 +4,7 @@ import { useMemo } from 'react' import { useSandboxInspectContext } from '../context' import { FileType } from 'e2b' import { FilesystemNode } from '../filesystem/types' +import { useStore } from 'zustand' /** * Hook for accessing directory children with automatic updates @@ -11,7 +12,7 @@ import { FilesystemNode } from '../filesystem/types' export function useDirectoryChildren(path: string): FilesystemNode[] { const { store } = useSandboxInspectContext() - return store((state) => state.getChildren(path)) + return useStore(store, (state) => state.getChildren(path)) } /** @@ -20,17 +21,27 @@ export function useDirectoryChildren(path: string): FilesystemNode[] { export function useDirectoryState(path: string) { const { store } = useSandboxInspectContext() - return store((state) => { + const isExpanded = useStore(store, (state) => state.isExpanded(path)) + const isLoading = useStore(store, (state) => state.loadingPaths.has(path)) + const hasError = useStore(store, (state) => state.errorPaths.has(path)) + const error = useStore(store, (state) => state.errorPaths.get(path)) + const isLoaded = useStore(store, (state) => { const node = state.getNode(path) - return { - isExpanded: state.isExpanded(path), - isLoading: state.loadingPaths.has(path), - hasError: state.errorPaths.has(path), - error: state.errorPaths.get(path), - isLoaded: node?.type === FileType.DIR ? !!node?.isLoaded : undefined, - hasChildren: state.hasChildren(path), - } + return node?.type === FileType.DIR ? !!node?.isLoaded : undefined }) + const hasChildren = useStore(store, (state) => state.hasChildren(path)) + + return useMemo( + () => ({ + isExpanded, + isLoading, + hasError, + error, + isLoaded, + hasChildren, + }), + [isExpanded, isLoading, hasError, error, isLoaded, hasChildren] + ) } /** diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts index aac7bd30d..8baf280bf 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react' import { useSandboxInspectContext } from '../context' import type { FilesystemNode } from '../filesystem/types' +import { useStore } from 'zustand' /** * Hook for accessing a specific filesystem node @@ -10,7 +11,7 @@ import type { FilesystemNode } from '../filesystem/types' export function useFilesystemNode(path: string): FilesystemNode | undefined { const { store } = useSandboxInspectContext() - return store((state) => state.getNode(path)) + return useStore(store, (state) => state.getNode(path)) } /** @@ -19,7 +20,7 @@ export function useFilesystemNode(path: string): FilesystemNode | undefined { export function useNodeSelection(path: string) { const { store, operations } = useSandboxInspectContext() - const isSelected = store((state) => state.isSelected(path)) + const isSelected = useStore(store, (state) => state.isSelected(path)) const select = useMemo( () => () => operations.selectNode(path), @@ -51,7 +52,7 @@ export function useNode(path: string) { export function useRootChildren() { const { store } = useSandboxInspectContext() - return store((state) => state.getChildren(state.rootPath)) + return useStore(store, (state) => state.getChildren(state.rootPath)) } /** @@ -60,7 +61,7 @@ export function useRootChildren() { export function useSelectedPath() { const { store } = useSandboxInspectContext() - return store((state) => state.selectedPath) + return useStore(store, (state) => state.selectedPath) } /** @@ -69,7 +70,7 @@ export function useSelectedPath() { export function useLoadingPaths() { const { store } = useSandboxInspectContext() - return store((state) => Array.from(state.loadingPaths)) + return useStore(store, (state) => state.loadingPaths) } /** @@ -78,5 +79,5 @@ export function useLoadingPaths() { export function useErrorPaths() { const { store } = useSandboxInspectContext() - return store((state) => Object.fromEntries(state.errorPaths)) + return useStore(store, (state) => state.errorPaths) } From b3714a0be444e260bcf57f8b30c63e432b0e8250 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 20 Jun 2025 17:54:56 +0200 Subject: [PATCH 17/75] refactor: server side sandbox connection handling --- src/app/api/sandboxes/[id]/list/route.ts | 67 ++++++++++ src/app/api/sandboxes/[id]/watch/route.ts | 88 +++++++++++++ src/features/dashboard/sandbox/context.tsx | 39 +----- .../dashboard/sandbox/inspect/context.tsx | 39 ++++-- .../inspect/filesystem/events-manager.ts | 121 +++++++++++------- .../sandbox/inspect/filesystem/store.ts | 39 ++++-- .../sandbox/inspect/filesystem/types.ts | 6 +- .../sandbox/inspect/hooks/use-directory.ts | 3 +- .../dashboard/sandbox/overview.mermaid | 48 ++++--- src/lib/clients/sandbox-pool.ts | 104 +++++++++++++++ src/lib/clients/watch-dir-pool.ts | 98 ++++++++++++++ src/server/sandboxes/get-sandbox-root.ts | 50 ++++++++ src/types/filesystem.ts | 17 +++ 13 files changed, 593 insertions(+), 126 deletions(-) create mode 100644 src/app/api/sandboxes/[id]/list/route.ts create mode 100644 src/app/api/sandboxes/[id]/watch/route.ts create mode 100644 src/lib/clients/sandbox-pool.ts create mode 100644 src/lib/clients/watch-dir-pool.ts create mode 100644 src/server/sandboxes/get-sandbox-root.ts create mode 100644 src/types/filesystem.ts diff --git a/src/app/api/sandboxes/[id]/list/route.ts b/src/app/api/sandboxes/[id]/list/route.ts new file mode 100644 index 000000000..83724b251 --- /dev/null +++ b/src/app/api/sandboxes/[id]/list/route.ts @@ -0,0 +1,67 @@ +import { NextRequest } from 'next/server' +import { SandboxPool } from '@/lib/clients/sandbox-pool' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { createRouteClient } from '@/lib/clients/supabase/server' +import { FileType } from 'e2b' +import { FsEntry, FsFileType } from '@/types/filesystem' + +export const maxDuration = 60 // quick, single call + +/** + * GET /api/sandboxes/{id}/list?dir=/path&team= + * Returns JSON array of EntryInfo for the directory. + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + + const { searchParams } = new URL(request.url) + const dir = searchParams.get('dir') ?? '/' + const teamId = searchParams.get('team') ?? '' + + const supabase = createRouteClient(request) + const { + data: { session }, + } = await supabase.auth.getSession() + if (!session?.access_token) + return new Response('Unauthorized', { status: 401 }) + + const opts = { + domain: 'xgimi.dev', + headers: { + ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), + }, + } + + let entries: FsEntry[] = [] + let error: unknown + try { + const sandbox = await SandboxPool.acquire(id, opts) + const raw = await sandbox.files.list(dir) + entries = raw.map((e) => ({ + name: e.name, + path: e.path, + type: + e.type === FileType.DIR + ? ('dir' as FsFileType) + : ('file' as FsFileType), + })) + } catch (err) { + error = err + } finally { + await SandboxPool.release(id) + } + + if (error) { + console.error('Dir list error', error) + return new Response('Failed to list directory', { status: 500 }) + } + + return new Response(JSON.stringify(entries), { + headers: { + 'Content-Type': 'application/json', + }, + }) +} diff --git a/src/app/api/sandboxes/[id]/watch/route.ts b/src/app/api/sandboxes/[id]/watch/route.ts new file mode 100644 index 000000000..fb4f450af --- /dev/null +++ b/src/app/api/sandboxes/[id]/watch/route.ts @@ -0,0 +1,88 @@ +import { NextRequest } from 'next/server' +import { WatchDirPool } from '@/lib/clients/watch-dir-pool' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { createRouteClient } from '@/lib/clients/supabase/server' + +export const maxDuration = 900 // 15 minutes + +/** + * SSE endpoint that streams filesystem events for a sandbox directory. + * + * Request: GET /api/sandboxes/{id}/watch?dir=/path + * + * The caller must be authenticated (via Supabase session cookie) so that we + * can forward the JWT to the E2B backend. + */ +export async function GET( + request: NextRequest, + { + params, + }: { + params: Promise<{ id: string }> + } +) { + const { id } = await params + + const { searchParams } = new URL(request.url) + const dir = searchParams.get('dir') ?? '/' + const teamId = searchParams.get('team') ?? '' + + const supabase = createRouteClient(request) + + const { + data: { session }, + } = await supabase.auth.getSession() + if (!session?.access_token) { + return new Response('Unauthorized', { status: 401 }) + } + + const sandboxOpts = { + domain: 'xgimi.dev', + headers: { + ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), + }, + } + + let watcherReleased = false + + const stream = new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder() + + const onEvent = (ev: unknown) => { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`)) + } + + await WatchDirPool.acquire(id, dir, onEvent, sandboxOpts) + + request.signal.addEventListener('abort', () => { + if (!watcherReleased) { + watcherReleased = true + void WatchDirPool.release(id, dir, onEvent) + } + controller.close() + }) + }, + /** + * This runs if the ReadableStream is cancelled *without* the `abort` event + * (for example `response.body.cancel()` or an abrupt GC). At this point we + * no longer have a reference to the original `onEvent` callback, so we + * cannot call `WatchDirPool.release(...)` accurately. Instead we just mark + * the watcher as released; the pool's idle-timer will close the underlying + * gRPC stream after `GRACE_MS` once it sees the ref-count hasn't changed. + */ + cancel() { + if (!watcherReleased) { + watcherReleased = true + } + }, + }) + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + }) +} diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index 71d758632..8b0f3ffc1 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -7,9 +7,7 @@ import React, { useLayoutEffect, useState, } from 'react' -import { Sandbox, SandboxInfo } from 'e2b' -import { supabase } from '@/lib/clients/supabase/client' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { SandboxInfo } from '@/types/api' interface SandboxState { secondsLeft: number @@ -18,7 +16,6 @@ interface SandboxState { interface SandboxContextValue { sandboxInfo: SandboxInfo - sandbox: Sandbox | null state: SandboxState } @@ -35,46 +32,21 @@ export function useSandboxContext() { interface SandboxProviderProps { children: ReactNode sandboxInfo: SandboxInfo - teamId: string } export function SandboxProvider({ children, sandboxInfo, - teamId, }: SandboxProviderProps) { const [secondsLeft, setSecondsLeft] = useState(0) const [isRunning, setIsRunning] = useState(false) - const [sandbox, setSandbox] = useState(null) - - useLayoutEffect(() => { - if (sandbox || !teamId) return - - const connectSandbox = async () => { - const accessToken = await supabase.auth.getSession().then(({ data }) => { - return data.session?.access_token - }) - - if (!accessToken) { - throw new Error('No access token found') - } - - const sbx = await Sandbox.connect(sandboxInfo.sandboxId, { - headers: { - ...SUPABASE_AUTH_HEADERS(accessToken, teamId), - }, - }) - setSandbox(sbx) - } - - connectSandbox() - }, [sandboxInfo.sandboxId, teamId, sandbox]) useLayoutEffect(() => { const interval = setInterval(() => { const now = new Date() + const endAt = new Date(sandboxInfo.endAt) - if (sandboxInfo.endAt <= now) { + if (endAt <= now) { setIsRunning(false) setSecondsLeft(0) clearInterval(interval) @@ -82,7 +54,7 @@ export function SandboxProvider({ setIsRunning(true) } - const diff = sandboxInfo.endAt.getTime() - now.getTime() + const diff = endAt.getTime() - now.getTime() setSecondsLeft(Math.max(0, Math.floor(diff / 1000))) }, 1000) @@ -90,7 +62,7 @@ export function SandboxProvider({ if (!interval) return clearInterval(interval) } - }, [sandboxInfo.sandboxId, sandboxInfo.endAt]) + }, [sandboxInfo.sandboxID, sandboxInfo.endAt]) const state = { secondsLeft, @@ -102,7 +74,6 @@ export function SandboxProvider({ value={{ sandboxInfo, state, - sandbox, }} > {children} diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index b7d96643d..80fdca0cc 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -5,10 +5,10 @@ import React, { useContext, useRef, ReactNode, - useMemo, useLayoutEffect, + useMemo, } from 'react' -import { FileType } from 'e2b' +import { FsEntry } from '@/types/filesystem' import { createFilesystemStore, type FilesystemStore } from './filesystem/store' import { FilesystemNode, FilesystemOperations } from './filesystem/types' import { FilesystemEventManager } from './filesystem/events-manager' @@ -27,24 +27,41 @@ const SandboxInspectContext = createContext( interface SandboxInspectProviderProps { children: ReactNode + teamId: string rootPath: string + seedEntries?: FsEntry[] } export function SandboxInspectProvider({ children, + teamId, rootPath, + seedEntries, }: SandboxInspectProviderProps) { - const { sandbox } = useSandboxContext() + const { sandboxInfo } = useSandboxContext() const storeRef = useRef(null) const eventManagerRef = useRef(null) const operationsRef = useRef(null) + const sandboxId = useMemo( + () => sandboxInfo.sandboxID + '-' + sandboxInfo.clientID, + [sandboxInfo.sandboxID, sandboxInfo.clientID] + ) + useLayoutEffect(() => { - if (!storeRef.current && sandbox) { - storeRef.current = createFilesystemStore(rootPath) + const normalizedRoot = normalizePath(rootPath) + const currentRoot = storeRef.current?.getState().rootPath + + if (!storeRef.current || currentRoot !== normalizedRoot) { + if (eventManagerRef.current) { + eventManagerRef.current.stopWatching() + } + + storeRef.current = createFilesystemStore(rootPath, seedEntries ?? []) eventManagerRef.current = new FilesystemEventManager( storeRef.current, - sandbox, + sandboxId, + teamId, rootPath ) @@ -63,7 +80,7 @@ export function SandboxInspectProvider({ const state = store.getState() const node = state.getNode(normalizedPath) - if (!node || node.type !== FileType.DIR) { + if (!node || node.type !== 'dir') { console.log(`Cannot toggle non-directory node at path: ${path}`) return } @@ -89,7 +106,7 @@ export function SandboxInspectProvider({ }, } } - }, [sandbox, rootPath]) + }, [sandboxId, teamId, seedEntries, rootPath]) useLayoutEffect(() => { const initializeRoot = async () => { @@ -107,7 +124,7 @@ export function SandboxInspectProvider({ const rootNode: FilesystemNode = { name: rootName, path: normalizedRootPath, - type: FileType.DIR, + type: 'dir', isExpanded: true, isLoaded: false, children: [], @@ -132,12 +149,12 @@ export function SandboxInspectProvider({ eventManagerRef.current.stopWatching() } } - }, [rootPath, sandbox]) + }, [sandboxId, teamId, seedEntries, rootPath]) if ( !storeRef.current || !eventManagerRef.current || - !sandbox || + !sandboxId || !operationsRef.current ) { return null diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index 669277ffd..21f43e0ed 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -1,53 +1,46 @@ -import { - FileType, - type Sandbox, - type FilesystemEvent, - type WatchHandle, - type EntryInfo, - FilesystemEventType, -} from 'e2b' +import { FsEvent, FsEntry } from '@/types/filesystem' import type { FilesystemStore } from './store' import { FilesystemNode } from './types' import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' export class FilesystemEventManager { - private watchHandle?: WatchHandle + private unsubscribe?: () => void private readonly rootPath: string private store: FilesystemStore - private sandbox: Sandbox - - constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string) { + private sandboxId: string + private teamId: string + + constructor( + store: FilesystemStore, + sandboxId: string, + teamId: string, + rootPath: string + ) { this.store = store - this.sandbox = sandbox + this.sandboxId = sandboxId + this.teamId = teamId this.rootPath = normalizePath(rootPath) - // Immediately start a single recursive watcher at the root void this.startRootWatcher() } private async startRootWatcher(): Promise { - if (this.watchHandle) return + if (this.unsubscribe) return - try { - this.watchHandle = await this.sandbox.files.watchDir( - this.rootPath, - (event) => this.handleFilesystemEvent(event), - { recursive: true } - ) - } catch (error) { - console.error(`Failed to start root watcher on ${this.rootPath}:`, error) - throw error - } + this.unsubscribe = openWatcher( + this.sandboxId, + this.rootPath, + this.teamId, + (event) => this.handleFilesystemEvent(event) + ) } stopWatching(): void { - if (this.watchHandle) { - this.watchHandle.stop() - this.watchHandle = undefined - } + this.unsubscribe?.() + this.unsubscribe = undefined } - private handleFilesystemEvent(event: FilesystemEvent): void { + private handleFilesystemEvent(event: FsEvent): void { const { type, name } = event // "name" is relative to the watched root; construct absolute path @@ -60,13 +53,9 @@ export class FilesystemEventManager { const parentNode = state.getNode(parentDir) switch (type) { - case FilesystemEventType.CREATE: - case FilesystemEventType.RENAME: - if ( - !parentNode || - parentNode.type !== FileType.DIR || - !parentNode.isLoaded - ) { + case 'create': + case 'rename': + if (!parentNode || parentNode.type !== 'dir' || !parentNode.isLoaded) { console.debug( `Skip refresh for '${normalizedPath}' because parent directory '${parentDir}' does not exist in store` ) @@ -79,7 +68,7 @@ export class FilesystemEventManager { void this.refreshDirectory(parentDir) break - case FilesystemEventType.REMOVE: + case 'remove': if (!state.getNode(normalizedPath)) { console.debug( `Skip remove for '${normalizedPath}' because node does not exist in store` @@ -93,8 +82,8 @@ export class FilesystemEventManager { this.handleRemoveEvent(normalizedPath) break - case FilesystemEventType.WRITE: - case FilesystemEventType.CHMOD: + case 'write': + case 'chmod': console.debug(`Ignoring ${type} event for '${normalizedPath}'`) break @@ -126,7 +115,7 @@ export class FilesystemEventManager { if ( !node || - node.type !== FileType.DIR || + node.type !== 'dir' || node.isLoaded || state.loadingPaths.has(normalizedPath) ) @@ -136,14 +125,14 @@ export class FilesystemEventManager { state.setError(normalizedPath) // clear any previous errors try { - const entries = await this.sandbox.files.list(normalizedPath) + const entries = await listDir(this.sandboxId, normalizedPath, this.teamId) - const nodes: FilesystemNode[] = entries.map((entry: EntryInfo) => { - if (entry.type === FileType.DIR) { + const nodes: FilesystemNode[] = entries.map((entry) => { + if (entry.type === 'dir') { return { name: entry.name, path: entry.path, - type: FileType.DIR, + type: 'dir', isExpanded: false, isSelected: false, isLoaded: false, @@ -153,7 +142,7 @@ export class FilesystemEventManager { return { name: entry.name, path: entry.path, - type: FileType.FILE, + type: 'file', isSelected: false, } } @@ -178,7 +167,7 @@ export class FilesystemEventManager { state.updateNode(normalizedPath, { isLoaded: false }) const node = state.getNode(normalizedPath) - if (node && node.type === FileType.DIR) { + if (node && node.type === 'dir') { const childrenPaths = [...node.children] for (const childPath of childrenPaths) { state.removeNode(childPath) @@ -188,3 +177,41 @@ export class FilesystemEventManager { await this.loadDirectory(normalizedPath) } } + +async function listDir( + sandboxId: string, + dir: string, + teamId: string +): Promise { + const url = `/api/sandboxes/${sandboxId}/list?dir=${encodeURIComponent(dir)}&team=${teamId}` + return fetch(url, { credentials: 'include' }).then((r) => { + if (!r.ok) throw new Error(`List failed ${r.status}`) + return r.json() + }) +} + +function openWatcher( + sandboxId: string, + dir: string, + teamId: string, + onEvent: (e: FsEvent) => void +): () => void { + let es: EventSource | null + + const connect = () => { + es = new EventSource( + `/api/sandboxes/${sandboxId}/watch?dir=${encodeURIComponent(dir)}&team=${teamId}`, + { withCredentials: true } + ) + + es.onmessage = (ev) => onEvent(JSON.parse(ev.data)) + es.onerror = () => { + // auto-reconnect in 1 s + es?.close() + setTimeout(connect, 1_000) + } + } + + connect() + return () => es?.close() +} diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index aa7789375..c9e73d2ab 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -8,7 +8,7 @@ import { getParentPath, isChildPath, } from '@/lib/utils/filesystem' -import { FileType } from 'e2b' +import { FsEntry } from '@/types/filesystem' import { FilesystemNode } from './types' enableMapSet() @@ -56,12 +56,27 @@ export type FilesystemStoreData = FilesystemStatics & const childrenCache: Map = new Map() -export const createFilesystemStore = (rootPath: string) => +const seedEntriesToNodes = (seedEntries: FsEntry[]): FilesystemNode[] => { + return seedEntries.map((entry) => ({ + name: entry.name, + path: entry.path, + type: entry.type ?? 'file', + isExpanded: false, + children: [], + })) +} + +export const createFilesystemStore = ( + rootPath: string, + seedEntries: FsEntry[] +) => create()( immer((set, get) => ({ rootPath: normalizePath(rootPath), - nodes: new Map(), + nodes: new Map( + seedEntriesToNodes(seedEntries).map((node) => [node.path, node]) + ), loadingPaths: new Set(), errorPaths: new Map(), @@ -79,14 +94,14 @@ export const createFilesystemStore = (rootPath: string) => parentNode = { name: parentName, path: normalizedParentPath, - type: FileType.DIR, + type: 'dir', isExpanded: false, children: [], } state.nodes.set(normalizedParentPath, parentNode) } - if (parentNode.type === FileType.FILE) { + if (parentNode.type === 'file') { console.error('Parent node is a file', parentNode) return } @@ -136,7 +151,7 @@ export const createFilesystemStore = (rootPath: string) => const parentPath = getParentPath(normalizedPath) const parentNode = state.nodes.get(parentPath) - if (parentNode && parentNode.type === FileType.DIR) { + if (parentNode && parentNode.type === 'dir') { parentNode.children = parentNode.children.filter( (childPath: string) => childPath !== normalizedPath ) @@ -180,7 +195,7 @@ export const createFilesystemStore = (rootPath: string) => if (!node) return - if (node?.type === FileType.FILE) { + if (node?.type === 'file') { console.error('Cannot expand file', node) return } @@ -222,7 +237,7 @@ export const createFilesystemStore = (rootPath: string) => const node = state.nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return + if (!node || node.type === 'file') return node.isLoading = loading }) @@ -240,7 +255,7 @@ export const createFilesystemStore = (rootPath: string) => const node = state.nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return + if (!node || node.type === 'file') return node.error = error }) @@ -260,7 +275,7 @@ export const createFilesystemStore = (rootPath: string) => const state = get() const node = state.nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return [] + if (!node || node.type === 'file') return [] const cached = childrenCache.get(normalizedPath) if (cached && cached.ref === node.children) { @@ -284,7 +299,7 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) const node = get().nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return false + if (!node || node.type === 'file') return false return !!node.isExpanded }, @@ -302,7 +317,7 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) const node = get().nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return false + if (!node || node.type === 'file') return false return node.children.length > 0 }, diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index 4f5365a8e..a70867333 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -1,7 +1,7 @@ -import { FileType } from 'e2b' +import { FsFileType } from '@/types/filesystem' interface FilesystemDir { - type: FileType.DIR + type: 'dir' name: string path: string children: string[] // paths of children @@ -13,7 +13,7 @@ interface FilesystemDir { } interface FilesystemFile { - type: FileType.FILE + type: 'file' name: string path: string isSelected?: boolean diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts index 6005d40cc..90f25fdca 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts @@ -2,7 +2,6 @@ import { useMemo } from 'react' import { useSandboxInspectContext } from '../context' -import { FileType } from 'e2b' import { FilesystemNode } from '../filesystem/types' import { useStore } from 'zustand' @@ -27,7 +26,7 @@ export function useDirectoryState(path: string) { const error = useStore(store, (state) => state.errorPaths.get(path)) const isLoaded = useStore(store, (state) => { const node = state.getNode(path) - return node?.type === FileType.DIR ? !!node?.isLoaded : undefined + return node?.type === 'dir' ? !!node?.isLoaded : undefined }) const hasChildren = useStore(store, (state) => state.hasChildren(path)) diff --git a/src/features/dashboard/sandbox/overview.mermaid b/src/features/dashboard/sandbox/overview.mermaid index 650264b14..283483ed8 100644 --- a/src/features/dashboard/sandbox/overview.mermaid +++ b/src/features/dashboard/sandbox/overview.mermaid @@ -2,13 +2,24 @@ flowchart TD subgraph SANDBOX_CONTEXT["Sandbox Context"] direction TB SANDBOX_PROVIDER["SandboxProvider"] - SANDBOX_INSTANCE["Sandbox Instance"] SANDBOX_STATE["Runtime State"] - SANDBOX_PROVIDER -- "creates & manages" --> SANDBOX_INSTANCE SANDBOX_PROVIDER -- "tracks lifecycle" --> SANDBOX_STATE end +%% ---------- New Server-side handling ---------- +subgraph SERVER_SIDE["Server Runtime (per Vercel instance)"] + direction TB + SANDBOX_POOL["SandboxPool"] + WATCH_POOL["WatchDirPool"] + LIST_ROUTE["/list Route"] + WATCH_ROUTE["/watch Route (SSE)"] + + SANDBOX_POOL -- "1 per sandbox" --> WATCH_POOL + LIST_ROUTE -- "files.list()" --> SANDBOX_POOL + WATCH_ROUTE -- "watchDir stream" --> WATCH_POOL +end + subgraph INSPECT_CONTEXT["Inspect Context"] direction TB INSPECT_PROVIDER["SandboxInspectProvider"] @@ -17,13 +28,17 @@ subgraph INSPECT_CONTEXT["Inspect Context"] OPERATIONS["Operations Object"] INSPECT_PROVIDER -- "creates singleton" --> FILESYSTEM_STORE - INSPECT_PROVIDER -- "creates with store + sandbox" --> EVENT_MANAGER + INSPECT_PROVIDER -- "creates with store" --> EVENT_MANAGER INSPECT_PROVIDER -- "exposes interface" --> OPERATIONS EVENT_MANAGER -- "writes FS data" --> FILESYSTEM_STORE OPERATIONS -- "delegates to" --> EVENT_MANAGER OPERATIONS -- "writes UI flags" --> FILESYSTEM_STORE end +%% Connections between client and server +EVENT_MANAGER -- "GET /list" --> LIST_ROUTE +EVENT_MANAGER -- "SSE /watch" --> WATCH_ROUTE + subgraph HOOKS["Hook Layer"] direction TB FILESYSTEM_HOOKS["Filesystem Hooks"] @@ -50,25 +65,24 @@ subgraph UI_COMPONENTS["UI Components"] OTHER_UI -- "trigger user actions" --> USER_ACTIONS end -subgraph E2B_REMOTE["E2B Remote"] +subgraph E2B_REMOTE["E2B Cloud"] REMOTE_SANDBOX["Remote Sandbox"] - FS_EVENTS["Filesystem Events"] - - REMOTE_SANDBOX -- "emits real-time" --> FS_EVENTS end -%% Context Dependencies -SANDBOX_INSTANCE -- "provided to" --> INSPECT_PROVIDER +%% External connectivity +REMOTE_SANDBOX -- "SDK REST + unary gRPC" --> SANDBOX_POOL +REMOTE_SANDBOX -- "watchDir server-stream" --> WATCH_POOL + +%% Client-Server boundary +SERVER_SIDE -- "HTTP (JSON) / SSE" --> EVENT_MANAGER %% Data Flow: User Actions -USER_ACTIONS -- "call hooks that return" --> OPERATIONS -OPERATIONS -- "async calls to" --> EVENT_MANAGER -EVENT_MANAGER -- "API calls to" --> REMOTE_SANDBOX +USER_ACTIONS -- "call hooks" --> OPERATIONS +OPERATIONS -- "async list / watch" --> EVENT_MANAGER -%% Data Flow: Remote Events -FS_EVENTS -- "handled by" --> EVENT_MANAGER -FILESYSTEM_STORE -- "triggers re-renders via" --> HOOKS -HOOKS -- "provide updated state to" --> UI_COMPONENTS +%% Flow inside client +FILESYSTEM_STORE -- "triggers re-renders" --> HOOKS +HOOKS -- "provide updated state" --> UI_COMPONENTS %% Hook Integration HOOKS -- "consumed by" --> UI_COMPONENTS @@ -86,4 +100,4 @@ class FILESYSTEM_STORE storeClass class EVENT_MANAGER,OPERATIONS managerClass class FILESYSTEM_HOOKS,DIRECTORY_HOOKS,NODE_HOOKS hooksClass class FILE_TREE,CODE_EDITOR,OTHER_UI,USER_ACTIONS uiClass -class REMOTE_SANDBOX,FS_EVENTS remoteClass \ No newline at end of file +class REMOTE_SANDBOX remoteClass \ No newline at end of file diff --git a/src/lib/clients/sandbox-pool.ts b/src/lib/clients/sandbox-pool.ts new file mode 100644 index 000000000..30682dcad --- /dev/null +++ b/src/lib/clients/sandbox-pool.ts @@ -0,0 +1,104 @@ +import 'server-cli-only' + +import { Sandbox, type SandboxOpts } from 'e2b' + +/** + * How long we keep the connection alive after the last consumer released it. + * A short grace period avoids connect/disconnect thrashing when the browser + * refreshes or multiple API calls arrive in quick succession. + */ +const GRACE_MS = 10_000 + +interface Entry { + /** Pending or resolved connect promise */ + promise: Promise + /** Resolved sandbox instance (set after promise fulfils) */ + sandbox?: Sandbox + /** Number of active users of this connection */ + ref: number + /** Handle for delayed close */ + timer?: ReturnType +} + +// --------------------------------------------- +// Global singleton (per Node/Edge instance) +// --------------------------------------------- +// eslint-disable-next-line no-var +declare global { + // `var` is required for global augmentation – suppressed for eslint + // eslint-disable-next-line no-var + var __SBX_POOL: Map | undefined +} + +const POOL: Map = (globalThis.__SBX_POOL ??= new Map< + string, + Entry +>()) + +export class SandboxPool { + /** + * Acquire (or create) a shared sandbox connection for `sandboxId`. + * Each caller MUST call `release()` when finished. + */ + static async acquire( + sandboxId: string, + opts: SandboxOpts + ): Promise { + let entry = POOL.get(sandboxId) + + if (entry) { + entry.ref += 1 + clearTimeout(entry.timer) + } else { + const promise = Sandbox.connect(sandboxId, opts) as Promise + entry = { promise, ref: 1 } + POOL.set(sandboxId, entry) + + // Cache resolved instance, drop entry if connect fails + promise + .then((sbx) => { + entry!.sandbox = sbx + }) + .catch(() => { + POOL.delete(sandboxId) + }) + } + + return entry.promise as Promise + } + + /** + * Release one reference obtained via `acquire()`. The connection is closed + * after `GRACE_MS` when no other consumers remain. + */ + static async release(sandboxId: string): Promise { + const entry = POOL.get(sandboxId) + if (!entry) return + + entry.ref = Math.max(0, entry.ref - 1) + + if (entry.ref === 0 && !entry.timer) { + entry.timer = setTimeout(async () => { + if (entry.ref === 0) { + try { + const closable = entry.sandbox as unknown as { + close?: () => Promise + dispose?: () => Promise + } + if (closable?.close) await closable.close() + else if (closable?.dispose) await closable.dispose() + } finally { + POOL.delete(sandboxId) + } + } + }, GRACE_MS) + } + } + + /** + * Lightweight helper useful for metrics or debugging. + */ + static status() { + return Array.from(POOL.entries()).map(([id, { ref }]) => ({ id, ref })) + } +} diff --git a/src/lib/clients/watch-dir-pool.ts b/src/lib/clients/watch-dir-pool.ts new file mode 100644 index 000000000..f8ec589ea --- /dev/null +++ b/src/lib/clients/watch-dir-pool.ts @@ -0,0 +1,98 @@ +import 'server-cli-only' + +import { WatchHandle, FilesystemEvent } from 'e2b' +import { SandboxPool } from './sandbox-pool' + +const GRACE_MS = 5_000 + +interface Entry { + promise: Promise + handle?: WatchHandle + consumers: Set<(e: FilesystemEvent) => void> + ref: number + timer?: ReturnType +} + +// Using `var` in the global augmentation is required – ESLint rule disabled locally +declare global { + // eslint-disable-next-line no-var + var __WATCH_POOL: Map | undefined +} + +const POOL: Map = (globalThis.__WATCH_POOL ??= new Map< + string, + Entry +>()) + +function makeKey(sandboxId: string, dir: string) { + return `${sandboxId}:${dir}` +} + +export class WatchDirPool { + /** + * Acquire (or create) a shared WatchHandle. Multiple callers are + * fanned-out via an internal consumer list—no mutation of the SDK types. + */ + static async acquire( + sandboxId: string, + dir: string, + onEvent: (ev: FilesystemEvent) => void, + sandboxOpts: Parameters[1] + ): Promise { + const key = makeKey(sandboxId, dir) + let entry = POOL.get(key) + + if (entry) { + entry.ref += 1 + entry.consumers.add(onEvent) + clearTimeout(entry.timer) + } else { + entry = { + ref: 1, + consumers: new Set([onEvent as (ev: FilesystemEvent) => void]), + promise: (async () => { + const sbx = await SandboxPool.acquire(sandboxId, sandboxOpts) + const handle = await sbx.files.watchDir( + dir, + (ev) => entry!.consumers.forEach((fn) => fn(ev)), + { recursive: true } + ) + entry!.handle = handle + return handle + })(), + } + POOL.set(key, entry) + } + + return entry.promise + } + + /** + * Release one reference. When the last reference is gone the underlying + * stream is closed after GRACE_MS. + */ + static async release( + sandboxId: string, + dir: string, + onEvent: (ev: FilesystemEvent) => void + ): Promise { + const key = makeKey(sandboxId, dir) + const entry = POOL.get(key) + if (!entry) return + + entry.ref = Math.max(0, entry.ref - 1) + entry.consumers.delete(onEvent) + + if (entry.ref === 0 && !entry.timer) { + entry.timer = setTimeout(async () => { + if (entry.ref === 0) { + try { + await entry.handle?.stop() + } finally { + POOL.delete(key) + } + } + }, GRACE_MS) + } + } +} diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts new file mode 100644 index 000000000..2ec0f28c6 --- /dev/null +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -0,0 +1,50 @@ +// src/server/sandboxes/get-sandbox-root.ts +import { z } from 'zod' +import { authActionClient } from '@/lib/clients/action' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { ERROR_CODES } from '@/configs/logs' +import { logError } from '@/lib/clients/logger' +import { returnServerError } from '@/lib/utils/action' +import { SandboxPool } from '@/lib/clients/sandbox-pool' +import { FsFileType } from '@/types/filesystem' +import { FileType } from 'e2b' + +export const GetSandboxRootSchema = z.object({ + teamId: z.string().uuid(), + sandboxId: z.string(), + rootPath: z.string().default('/'), +}) + +export const getSandboxRoot = authActionClient + .schema(GetSandboxRootSchema) + .metadata({ serverFunctionName: 'getSandboxRoot' }) + .action(async ({ parsedInput, ctx }) => { + const { teamId, sandboxId, rootPath } = parsedInput + const { session } = ctx + + const headers = SUPABASE_AUTH_HEADERS(session.access_token, teamId) + + let entries + try { + const sandbox = await SandboxPool.acquire(sandboxId, { + domain: 'xgimi.dev', + headers, + }) + const raw = await sandbox.files.list(rootPath) + entries = raw.map((e) => ({ + name: e.name, + path: e.path, + type: + e.type === FileType.DIR + ? ('dir' as FsFileType) + : ('file' as FsFileType), + })) + } catch (err) { + logError(ERROR_CODES.INFRA, 'files.list', 500, err) + return returnServerError('Failed to list sandbox directory.') + } + + return { + entries, + } + }) diff --git a/src/types/filesystem.ts b/src/types/filesystem.ts new file mode 100644 index 000000000..cd6caece7 --- /dev/null +++ b/src/types/filesystem.ts @@ -0,0 +1,17 @@ +// NOTE: We need to maintain duplicate types of the e2b sdk, in order to avoid having the whole sdk inside the client bundle. +// The issue here mainly stems from the FileType and FilesystemEvent enums. + +export type FsFileType = 'file' | 'dir' + +export interface FsEntry { + name: string + path: string + type: FsFileType +} + +export type FsEventType = 'create' | 'write' | 'remove' | 'rename' | 'chmod' + +export interface FsEvent { + name: string + type: FsEventType +} From 27c2f01efb2d5867986255006774dcfd37be1cfd Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 20 Jun 2025 18:07:09 +0200 Subject: [PATCH 18/75] chore: reduce watch route timeout to 10 min --- src/app/api/sandboxes/[id]/watch/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/sandboxes/[id]/watch/route.ts b/src/app/api/sandboxes/[id]/watch/route.ts index fb4f450af..c16f5ed5f 100644 --- a/src/app/api/sandboxes/[id]/watch/route.ts +++ b/src/app/api/sandboxes/[id]/watch/route.ts @@ -3,7 +3,7 @@ import { WatchDirPool } from '@/lib/clients/watch-dir-pool' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { createRouteClient } from '@/lib/clients/supabase/server' -export const maxDuration = 900 // 15 minutes +export const maxDuration = 600 // 10 minutes /** * SSE endpoint that streams filesystem events for a sandbox directory. From 2bda457a988a826b7bb4be0c9423c873af1157da Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 20 Jun 2025 18:10:36 +0200 Subject: [PATCH 19/75] chore: re-organize/comment pools --- src/lib/clients/sandbox-pool.ts | 9 +-------- src/lib/clients/watch-dir-pool.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/lib/clients/sandbox-pool.ts b/src/lib/clients/sandbox-pool.ts index 30682dcad..7187c16bd 100644 --- a/src/lib/clients/sandbox-pool.ts +++ b/src/lib/clients/sandbox-pool.ts @@ -21,7 +21,7 @@ interface Entry { } // --------------------------------------------- -// Global singleton (per Node/Edge instance) +// Global singleton (per Node) // --------------------------------------------- // eslint-disable-next-line no-var declare global { @@ -94,11 +94,4 @@ export class SandboxPool { }, GRACE_MS) } } - - /** - * Lightweight helper useful for metrics or debugging. - */ - static status() { - return Array.from(POOL.entries()).map(([id, { ref }]) => ({ id, ref })) - } } diff --git a/src/lib/clients/watch-dir-pool.ts b/src/lib/clients/watch-dir-pool.ts index f8ec589ea..aeccda496 100644 --- a/src/lib/clients/watch-dir-pool.ts +++ b/src/lib/clients/watch-dir-pool.ts @@ -3,18 +3,28 @@ import 'server-cli-only' import { WatchHandle, FilesystemEvent } from 'e2b' import { SandboxPool } from './sandbox-pool' +// Grace period in milliseconds before cleaning up unused watch handles const GRACE_MS = 5_000 interface Entry { + // Promise that resolves to the watch handle once created promise: Promise + // The actual watch handle once available handle?: WatchHandle + // Set of callback functions from all consumers watching this directory consumers: Set<(e: FilesystemEvent) => void> + // Reference count of active consumers ref: number + // Timer for cleanup when ref count reaches 0 timer?: ReturnType } -// Using `var` in the global augmentation is required – ESLint rule disabled locally +// --------------------------------------------- +// Global singleton (per Node) +// --------------------------------------------- +// eslint-disable-next-line no-var declare global { + // `var` is required for global augmentation – suppressed for eslint // eslint-disable-next-line no-var var __WATCH_POOL: Map | undefined } From a52f6163e048d1847dfb9386851b9997b10973f5 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 20 Jun 2025 18:21:39 +0200 Subject: [PATCH 20/75] fix: correctly close SandboxPool connections in WatchDirPool and callers --- src/lib/clients/watch-dir-pool.ts | 1 + src/server/sandboxes/get-sandbox-root.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/lib/clients/watch-dir-pool.ts b/src/lib/clients/watch-dir-pool.ts index aeccda496..eae094d31 100644 --- a/src/lib/clients/watch-dir-pool.ts +++ b/src/lib/clients/watch-dir-pool.ts @@ -98,6 +98,7 @@ export class WatchDirPool { if (entry.ref === 0) { try { await entry.handle?.stop() + await SandboxPool.release(sandboxId) } finally { POOL.delete(key) } diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index 2ec0f28c6..be2df7330 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -42,6 +42,8 @@ export const getSandboxRoot = authActionClient } catch (err) { logError(ERROR_CODES.INFRA, 'files.list', 500, err) return returnServerError('Failed to list sandbox directory.') + } finally { + await SandboxPool.release(sandboxId) } return { From 15fbe44b60e60f8142153d2e50c6cda62f31771a Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Fri, 20 Jun 2025 20:33:08 +0200 Subject: [PATCH 21/75] refactor: inspect context to correctly populate store based on seedEntries --- .../dashboard/sandbox/inspect/context.tsx | 162 +++++++++--------- .../sandbox/inspect/filesystem/store.ts | 19 +- src/lib/clients/sandbox-pool.ts | 16 +- src/lib/clients/watch-dir-pool.ts | 15 ++ 4 files changed, 116 insertions(+), 96 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 80fdca0cc..5fd9e26f3 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -18,7 +18,7 @@ import { useSandboxContext } from '../context' interface SandboxInspectContextValue { store: FilesystemStore operations: FilesystemOperations - eventManager: FilesystemEventManager + eventManager: FilesystemEventManager | null } const SandboxInspectContext = createContext( @@ -39,38 +39,82 @@ export function SandboxInspectProvider({ seedEntries, }: SandboxInspectProviderProps) { const { sandboxInfo } = useSandboxContext() - const storeRef = useRef(null) - const eventManagerRef = useRef(null) - const operationsRef = useRef(null) + const storeRef = useRef(null) + const eventManagerRef = useRef(null) + const operationsRef = useRef(null) const sandboxId = useMemo( () => sandboxInfo.sandboxID + '-' + sandboxInfo.clientID, [sandboxInfo.sandboxID, sandboxInfo.clientID] ) - useLayoutEffect(() => { + /* + * ---------- synchronous store initialisation ---------- + * We want the tree to render immediately using the "seedEntries" streamed from the + * server component (see page.tsx). We therefore build / populate the Zustand store + * right here during render, instead of doing it later inside an effect. + */ + { const normalizedRoot = normalizePath(rootPath) - const currentRoot = storeRef.current?.getState().rootPath + const needsNewStore = + !storeRef.current || + storeRef.current.getState().rootPath !== normalizedRoot - if (!storeRef.current || currentRoot !== normalizedRoot) { + if (needsNewStore) { + // stop previous watcher (if any) if (eventManagerRef.current) { eventManagerRef.current.stopWatching() + eventManagerRef.current = null } - storeRef.current = createFilesystemStore(rootPath, seedEntries ?? []) - eventManagerRef.current = new FilesystemEventManager( - storeRef.current, - sandboxId, - teamId, - rootPath - ) + storeRef.current = createFilesystemStore(rootPath) - const eventManager = eventManagerRef.current - const store = storeRef.current + const state = storeRef.current.getState() + const rootName = + normalizedRoot === '/' ? '/' : normalizedRoot.split('/').pop() || '' + + state.addNodes(getParentPath(normalizedRoot), [ + { + name: rootName, + path: normalizedRoot, + type: 'dir', + isExpanded: true, + isLoaded: true, + children: [], + }, + ]) + + if (seedEntries && seedEntries.length) { + const seedNodes: FilesystemNode[] = seedEntries.map((entry) => { + const base = { + name: entry.name, + path: normalizePath(entry.path), + } + + if (entry.type === 'dir') { + return { + ...base, + type: 'dir' as const, + isExpanded: false, + isLoaded: false, + children: [], + } + } + + return { + ...base, + type: 'file' as const, + } + }) + + state.addNodes(normalizedRoot, seedNodes) + } + + const store = storeRef.current operationsRef.current = { loadDirectory: async (path: string) => { - await eventManager.loadDirectory(path) + await eventManagerRef.current?.loadDirectory(path) }, selectNode: (path: string) => { store.getState().setSelected(path) @@ -80,84 +124,46 @@ export function SandboxInspectProvider({ const state = store.getState() const node = state.getNode(normalizedPath) - if (!node || node.type !== 'dir') { - console.log(`Cannot toggle non-directory node at path: ${path}`) - return - } + if (!node || node.type !== 'dir') return const newExpandedState = !node.isExpanded - console.log( - `Toggling directory ${path} to ${newExpandedState ? 'expanded' : 'collapsed'}` - ) - state.setExpanded(normalizedPath, newExpandedState) - if (newExpandedState) { - if (!node.isLoaded) { - console.log(`Loading unloaded directory: ${path}`) - await eventManager.loadDirectory(normalizedPath) - } else { - console.log(`Directory already loaded: ${path}`) - } + if (newExpandedState && !node.isLoaded) { + await eventManagerRef.current?.loadDirectory(normalizedPath) } }, refreshDirectory: async (path: string) => { - await eventManager.refreshDirectory(path) + await eventManagerRef.current?.refreshDirectory(path) }, } } - }, [sandboxId, teamId, seedEntries, rootPath]) + } + /* + * ---------- watcher (side-effect) initialisation / cleanup ---------- + */ useLayoutEffect(() => { - const initializeRoot = async () => { - if (!storeRef.current || !eventManagerRef.current) return - - const state = storeRef.current.getState() - const normalizedRootPath = normalizePath(rootPath) - - if (!state.getNode(normalizedRootPath)) { - const rootName = - normalizedRootPath === '/' - ? '/' - : normalizedRootPath.split('/').pop() || '' - - const rootNode: FilesystemNode = { - name: rootName, - path: normalizedRootPath, - type: 'dir', - isExpanded: true, - isLoaded: false, - children: [], - } - - const parentPath = getParentPath(normalizedRootPath) - state.addNodes(parentPath, [rootNode]) - } + if (!storeRef.current) return - try { - await eventManagerRef.current.loadDirectory(normalizedRootPath) - } catch (error) { - console.error('Failed to initialize root directory:', error) - state.setError(normalizedRootPath, 'Failed to load root directory') - } + // (re)create the event-manager when sandbox / team / root changes + if (eventManagerRef.current) { + eventManagerRef.current.stopWatching() } - - initializeRoot() + eventManagerRef.current = new FilesystemEventManager( + storeRef.current, + sandboxId, + teamId, + rootPath + ) return () => { - if (eventManagerRef.current) { - eventManagerRef.current.stopWatching() - } + eventManagerRef.current?.stopWatching() } - }, [sandboxId, teamId, seedEntries, rootPath]) - - if ( - !storeRef.current || - !eventManagerRef.current || - !sandboxId || - !operationsRef.current - ) { - return null + }, [sandboxId, teamId, rootPath]) + + if (!storeRef.current || !operationsRef.current) { + return null // should never happen, but satisfies type-checker } const contextValue: SandboxInspectContextValue = { diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index c9e73d2ab..92ca3aae4 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -56,27 +56,12 @@ export type FilesystemStoreData = FilesystemStatics & const childrenCache: Map = new Map() -const seedEntriesToNodes = (seedEntries: FsEntry[]): FilesystemNode[] => { - return seedEntries.map((entry) => ({ - name: entry.name, - path: entry.path, - type: entry.type ?? 'file', - isExpanded: false, - children: [], - })) -} - -export const createFilesystemStore = ( - rootPath: string, - seedEntries: FsEntry[] -) => +export const createFilesystemStore = (rootPath: string) => create()( immer((set, get) => ({ rootPath: normalizePath(rootPath), - nodes: new Map( - seedEntriesToNodes(seedEntries).map((node) => [node.path, node]) - ), + nodes: new Map(), loadingPaths: new Set(), errorPaths: new Map(), diff --git a/src/lib/clients/sandbox-pool.ts b/src/lib/clients/sandbox-pool.ts index 7187c16bd..b6461d4af 100644 --- a/src/lib/clients/sandbox-pool.ts +++ b/src/lib/clients/sandbox-pool.ts @@ -1,6 +1,8 @@ import 'server-cli-only' import { Sandbox, type SandboxOpts } from 'e2b' +import { VERBOSE } from '@/configs/flags' +import { logDebug } from './logger' /** * How long we keep the connection alive after the last consumer released it. @@ -49,7 +51,10 @@ export class SandboxPool { if (entry) { entry.ref += 1 clearTimeout(entry.timer) + if (VERBOSE) + logDebug('SandboxPool.acquire reuse', sandboxId, 'refs', entry.ref) } else { + if (VERBOSE) logDebug('SandboxPool.acquire connect', sandboxId) const promise = Sandbox.connect(sandboxId, opts) as Promise entry = { promise, ref: 1 } POOL.set(sandboxId, entry) @@ -58,12 +63,15 @@ export class SandboxPool { promise .then((sbx) => { entry!.sandbox = sbx + if (VERBOSE) logDebug('SandboxPool connected', sandboxId) }) - .catch(() => { + .catch((err) => { + if (VERBOSE) logDebug('SandboxPool connect FAILED', sandboxId, err) POOL.delete(sandboxId) }) } + if (VERBOSE) logDebug('SandboxPool.acquire return', sandboxId, 'promise') return entry.promise as Promise } @@ -77,9 +85,14 @@ export class SandboxPool { entry.ref = Math.max(0, entry.ref - 1) + if (VERBOSE) logDebug('SandboxPool.release', sandboxId, 'refs', entry.ref) + if (entry.ref === 0 && !entry.timer) { + if (VERBOSE) + logDebug('SandboxPool schedule close', sandboxId, `in ${GRACE_MS}ms`) entry.timer = setTimeout(async () => { if (entry.ref === 0) { + if (VERBOSE) logDebug('SandboxPool closing', sandboxId) try { const closable = entry.sandbox as unknown as { close?: () => Promise @@ -89,6 +102,7 @@ export class SandboxPool { else if (closable?.dispose) await closable.dispose() } finally { POOL.delete(sandboxId) + if (VERBOSE) logDebug('SandboxPool closed', sandboxId) } } }, GRACE_MS) diff --git a/src/lib/clients/watch-dir-pool.ts b/src/lib/clients/watch-dir-pool.ts index eae094d31..8253966c9 100644 --- a/src/lib/clients/watch-dir-pool.ts +++ b/src/lib/clients/watch-dir-pool.ts @@ -2,6 +2,8 @@ import 'server-cli-only' import { WatchHandle, FilesystemEvent } from 'e2b' import { SandboxPool } from './sandbox-pool' +import { VERBOSE } from '@/configs/flags' +import { logDebug } from './logger' // Grace period in milliseconds before cleaning up unused watch handles const GRACE_MS = 5_000 @@ -52,22 +54,29 @@ export class WatchDirPool { const key = makeKey(sandboxId, dir) let entry = POOL.get(key) + if (VERBOSE) logDebug('WatchDirPool.acquire', key) + if (entry) { entry.ref += 1 entry.consumers.add(onEvent) clearTimeout(entry.timer) + if (VERBOSE) logDebug('WatchDirPool reuse', key, 'refs', entry.ref) } else { + if (VERBOSE) logDebug('WatchDirPool create watcher', key) entry = { ref: 1, consumers: new Set([onEvent as (ev: FilesystemEvent) => void]), promise: (async () => { const sbx = await SandboxPool.acquire(sandboxId, sandboxOpts) + if (VERBOSE) + logDebug('WatchDirPool connected to sandbox', sandboxId, 'dir', dir) const handle = await sbx.files.watchDir( dir, (ev) => entry!.consumers.forEach((fn) => fn(ev)), { recursive: true } ) entry!.handle = handle + if (VERBOSE) logDebug('WatchDirPool watcher ready', key) return handle })(), } @@ -93,14 +102,20 @@ export class WatchDirPool { entry.ref = Math.max(0, entry.ref - 1) entry.consumers.delete(onEvent) + if (VERBOSE) logDebug('WatchDirPool.release', key, 'refs', entry.ref) + if (entry.ref === 0 && !entry.timer) { + if (VERBOSE) + logDebug('WatchDirPool schedule stop', key, `in ${GRACE_MS}ms`) entry.timer = setTimeout(async () => { if (entry.ref === 0) { + if (VERBOSE) logDebug('WatchDirPool stopping', key) try { await entry.handle?.stop() await SandboxPool.release(sandboxId) } finally { POOL.delete(key) + if (VERBOSE) logDebug('WatchDirPool stopped', key) } } }, GRACE_MS) From b98f927f75180d230cd1318cb0489ac7ed310c5b Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Sat, 21 Jun 2025 00:24:43 +0200 Subject: [PATCH 22/75] feat: add E2B_DOMAIN to environment configuration and enhance watch route logging --- .env.example | 4 +++ src/app/api/sandboxes/[id]/list/route.ts | 3 ++- src/app/api/sandboxes/[id]/watch/route.ts | 30 +++++++++++++++++++++-- src/lib/clients/watch-dir-pool.ts | 5 ++-- src/lib/env.ts | 1 + src/server/sandboxes/get-sandbox-root.ts | 1 - 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index d8ee01842..4a7407250 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,10 @@ SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key ### Visit: https://github.com/e2b-dev/infra/blob/main/README.md for a self-hosting guide INFRA_API_URL=https://api.e2b.dev +### Default domain for the E2B SDK +### Used for Sandbox Details Page +E2B_DOMAIN=e2b.dev + ### KV database configuration KV_REST_API_TOKEN= KV_REST_API_URL= diff --git a/src/app/api/sandboxes/[id]/list/route.ts b/src/app/api/sandboxes/[id]/list/route.ts index 83724b251..ff021d1c0 100644 --- a/src/app/api/sandboxes/[id]/list/route.ts +++ b/src/app/api/sandboxes/[id]/list/route.ts @@ -6,6 +6,8 @@ import { FileType } from 'e2b' import { FsEntry, FsFileType } from '@/types/filesystem' export const maxDuration = 60 // quick, single call +export const dynamic = 'force-dynamic' +export const fetchCache = 'force-no-store' /** * GET /api/sandboxes/{id}/list?dir=/path&team= @@ -29,7 +31,6 @@ export async function GET( return new Response('Unauthorized', { status: 401 }) const opts = { - domain: 'xgimi.dev', headers: { ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), }, diff --git a/src/app/api/sandboxes/[id]/watch/route.ts b/src/app/api/sandboxes/[id]/watch/route.ts index c16f5ed5f..11e1813c8 100644 --- a/src/app/api/sandboxes/[id]/watch/route.ts +++ b/src/app/api/sandboxes/[id]/watch/route.ts @@ -2,6 +2,8 @@ import { NextRequest } from 'next/server' import { WatchDirPool } from '@/lib/clients/watch-dir-pool' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { createRouteClient } from '@/lib/clients/supabase/server' +import { VERBOSE } from '@/configs/flags' +import { logDebug } from '@/lib/clients/logger' export const maxDuration = 600 // 10 minutes @@ -27,6 +29,8 @@ export async function GET( const dir = searchParams.get('dir') ?? '/' const teamId = searchParams.get('team') ?? '' + if (VERBOSE) logDebug('WatchRoute.init', { id, dir, teamId }) + const supabase = createRouteClient(request) const { @@ -37,13 +41,15 @@ export async function GET( } const sandboxOpts = { - domain: 'xgimi.dev', headers: { ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), }, } + if (VERBOSE) logDebug('WatchRoute.sandboxOpts') + let watcherReleased = false + let ping: ReturnType | undefined const stream = new ReadableStream({ async start(controller) { @@ -53,13 +59,30 @@ export async function GET( controller.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`)) } + if (VERBOSE) logDebug('WatchRoute.acquireWatcher') + await WatchDirPool.acquire(id, dir, onEvent, sandboxOpts) + if (VERBOSE) logDebug('WatchRoute.watcherReady') + + // periodic comment ping to keep intermediary proxies / clients happy + ping = setInterval(() => { + try { + controller.enqueue(encoder.encode(`: ping\n\n`)) + } catch (err) { + // controller was closed—stop pings to avoid uncaught exceptions + if (VERBOSE) logDebug('WatchRoute.pingError', err) + if (ping) clearInterval(ping) + } + }, 15_000) + request.signal.addEventListener('abort', () => { if (!watcherReleased) { watcherReleased = true - void WatchDirPool.release(id, dir, onEvent) + if (VERBOSE) logDebug('WatchRoute.abort') + // Do NOT release; keep watcher alive for GRACE_MS so quick tab switches reuse it } + if (ping) clearInterval(ping) controller.close() }) }, @@ -74,7 +97,10 @@ export async function GET( cancel() { if (!watcherReleased) { watcherReleased = true + if (VERBOSE) logDebug('WatchRoute.cancelStream') + void WatchDirPool.release(id, dir, () => {}) } + if (ping) clearInterval(ping) }, }) diff --git a/src/lib/clients/watch-dir-pool.ts b/src/lib/clients/watch-dir-pool.ts index 8253966c9..961220deb 100644 --- a/src/lib/clients/watch-dir-pool.ts +++ b/src/lib/clients/watch-dir-pool.ts @@ -5,8 +5,9 @@ import { SandboxPool } from './sandbox-pool' import { VERBOSE } from '@/configs/flags' import { logDebug } from './logger' -// Grace period in milliseconds before cleaning up unused watch handles -const GRACE_MS = 5_000 +// Grace period in milliseconds before cleaning up unused watch handles – +// 30 s gives background tabs enough time to reconnect after throttling. +const GRACE_MS = 30_000 interface Entry { // Promise that resolves to the watch handle once created diff --git a/src/lib/env.ts b/src/lib/env.ts index e74da865a..84a179a88 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -5,6 +5,7 @@ export const serverSchema = z.object({ INFRA_API_URL: z.string().url(), KV_REST_API_TOKEN: z.string().min(1), KV_REST_API_URL: z.string().url(), + E2B_DOMAIN: z.string(), BILLING_API_URL: z.string().url().optional(), OTEL_SERVICE_NAME: z.string().optional(), diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index be2df7330..af83ad785 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -27,7 +27,6 @@ export const getSandboxRoot = authActionClient let entries try { const sandbox = await SandboxPool.acquire(sandboxId, { - domain: 'xgimi.dev', headers, }) const raw = await sandbox.files.list(rootPath) From 7e0665fddac2dda86f0fd47ca88def3b27209012 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 23 Jun 2025 15:51:08 +0200 Subject: [PATCH 23/75] fix: missing test env --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bcb3efcf0..3865ed911 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: SUPABASE_SERVICE_ROLE_KEY: test-service-role-key INFRA_API_URL: https://api.e2b-test.dev BILLING_API_URL: https://billing.e2b-test.dev + E2B_DOMAIN: e2b-test.dev NEXT_PUBLIC_POSTHOG_KEY: test-posthog-key NEXT_PUBLIC_SUPABASE_URL: https://test-supabase-url.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY: test-supabase-anon-key From ee28b6fe7ff21294b6606a195db16b4f64cfe73e Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 23 Jun 2025 17:47:28 +0200 Subject: [PATCH 24/75] improve: loggign & watchDir release in watch route --- src/app/api/sandboxes/[id]/watch/route.ts | 2 +- src/lib/clients/watch-dir-pool.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/api/sandboxes/[id]/watch/route.ts b/src/app/api/sandboxes/[id]/watch/route.ts index 11e1813c8..d06fcbd30 100644 --- a/src/app/api/sandboxes/[id]/watch/route.ts +++ b/src/app/api/sandboxes/[id]/watch/route.ts @@ -80,7 +80,7 @@ export async function GET( if (!watcherReleased) { watcherReleased = true if (VERBOSE) logDebug('WatchRoute.abort') - // Do NOT release; keep watcher alive for GRACE_MS so quick tab switches reuse it + void WatchDirPool.release(id, dir, onEvent) } if (ping) clearInterval(ping) controller.close() diff --git a/src/lib/clients/watch-dir-pool.ts b/src/lib/clients/watch-dir-pool.ts index 961220deb..91c4e076a 100644 --- a/src/lib/clients/watch-dir-pool.ts +++ b/src/lib/clients/watch-dir-pool.ts @@ -61,23 +61,23 @@ export class WatchDirPool { entry.ref += 1 entry.consumers.add(onEvent) clearTimeout(entry.timer) - if (VERBOSE) logDebug('WatchDirPool reuse', key, 'refs', entry.ref) + if (VERBOSE) logDebug('WatchDirPool.reuse', key, 'refs', entry.ref) } else { - if (VERBOSE) logDebug('WatchDirPool create watcher', key) + if (VERBOSE) logDebug('WatchDirPool.createWatcher', key) entry = { ref: 1, consumers: new Set([onEvent as (ev: FilesystemEvent) => void]), promise: (async () => { const sbx = await SandboxPool.acquire(sandboxId, sandboxOpts) if (VERBOSE) - logDebug('WatchDirPool connected to sandbox', sandboxId, 'dir', dir) + logDebug('WatchDirPool.connectedToSandbox', sandboxId, 'dir', dir) const handle = await sbx.files.watchDir( dir, (ev) => entry!.consumers.forEach((fn) => fn(ev)), { recursive: true } ) entry!.handle = handle - if (VERBOSE) logDebug('WatchDirPool watcher ready', key) + if (VERBOSE) logDebug('WatchDirPool.watcherReady', key) return handle })(), } @@ -107,16 +107,16 @@ export class WatchDirPool { if (entry.ref === 0 && !entry.timer) { if (VERBOSE) - logDebug('WatchDirPool schedule stop', key, `in ${GRACE_MS}ms`) + logDebug('WatchDirPool.scheduleStop', key, `in ${GRACE_MS}ms`) entry.timer = setTimeout(async () => { if (entry.ref === 0) { - if (VERBOSE) logDebug('WatchDirPool stopping', key) + if (VERBOSE) logDebug('WatchDirPool.stopping', key) try { await entry.handle?.stop() await SandboxPool.release(sandboxId) } finally { POOL.delete(key) - if (VERBOSE) logDebug('WatchDirPool stopped', key) + if (VERBOSE) logDebug('WatchDirPool.stopped', key) } } }, GRACE_MS) From 4391c6ad96ee2811a301af15676717d471be6938 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 23 Jun 2025 18:30:32 +0200 Subject: [PATCH 25/75] improve: sandbox inspect watch route and pooling --- src/app/api/sandboxes/[id]/watch/route.ts | 37 ++++++++++--------- .../inspect/filesystem/events-manager.ts | 10 ++--- src/lib/clients/watch-dir-pool.ts | 2 +- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/app/api/sandboxes/[id]/watch/route.ts b/src/app/api/sandboxes/[id]/watch/route.ts index d06fcbd30..da4fe44d3 100644 --- a/src/app/api/sandboxes/[id]/watch/route.ts +++ b/src/app/api/sandboxes/[id]/watch/route.ts @@ -50,12 +50,14 @@ export async function GET( let watcherReleased = false let ping: ReturnType | undefined + let onEvent: (ev: unknown) => void + let cleanup: () => void const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder() - const onEvent = (ev: unknown) => { + onEvent = (ev: unknown) => { controller.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`)) } @@ -65,25 +67,30 @@ export async function GET( if (VERBOSE) logDebug('WatchRoute.watcherReady') + // helper that performs a full teardown exactly once + cleanup = () => { + if (!watcherReleased) { + watcherReleased = true + if (VERBOSE) logDebug('WatchRoute.cleanup') + void WatchDirPool.release(id, dir, onEvent) + } + if (ping) clearInterval(ping) + controller.close() + } + // periodic comment ping to keep intermediary proxies / clients happy ping = setInterval(() => { try { controller.enqueue(encoder.encode(`: ping\n\n`)) } catch (err) { - // controller was closed—stop pings to avoid uncaught exceptions if (VERBOSE) logDebug('WatchRoute.pingError', err) - if (ping) clearInterval(ping) + cleanup() } - }, 15_000) + }, 5_000) request.signal.addEventListener('abort', () => { - if (!watcherReleased) { - watcherReleased = true - if (VERBOSE) logDebug('WatchRoute.abort') - void WatchDirPool.release(id, dir, onEvent) - } - if (ping) clearInterval(ping) - controller.close() + if (VERBOSE) logDebug('WatchRoute.abort') + cleanup() }) }, /** @@ -95,12 +102,8 @@ export async function GET( * gRPC stream after `GRACE_MS` once it sees the ref-count hasn't changed. */ cancel() { - if (!watcherReleased) { - watcherReleased = true - if (VERBOSE) logDebug('WatchRoute.cancelStream') - void WatchDirPool.release(id, dir, () => {}) - } - if (ping) clearInterval(ping) + if (VERBOSE) logDebug('WatchRoute.cancelStream') + cleanup?.() }, }) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index 21f43e0ed..167270902 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -199,12 +199,12 @@ function openWatcher( let es: EventSource | null const connect = () => { - es = new EventSource( - `/api/sandboxes/${sandboxId}/watch?dir=${encodeURIComponent(dir)}&team=${teamId}`, - { withCredentials: true } - ) + const url = `/api/sandboxes/${sandboxId}/watch?dir=${encodeURIComponent(dir)}&team=${teamId}` + es = new EventSource(url, { withCredentials: true }) - es.onmessage = (ev) => onEvent(JSON.parse(ev.data)) + es.onmessage = (ev) => { + onEvent(JSON.parse(ev.data)) + } es.onerror = () => { // auto-reconnect in 1 s es?.close() diff --git a/src/lib/clients/watch-dir-pool.ts b/src/lib/clients/watch-dir-pool.ts index 91c4e076a..1a0b8717c 100644 --- a/src/lib/clients/watch-dir-pool.ts +++ b/src/lib/clients/watch-dir-pool.ts @@ -7,7 +7,7 @@ import { logDebug } from './logger' // Grace period in milliseconds before cleaning up unused watch handles – // 30 s gives background tabs enough time to reconnect after throttling. -const GRACE_MS = 30_000 +const GRACE_MS = 10_000 interface Entry { // Promise that resolves to the watch handle once created From 7531f78602b3caa9bc0c972aace9f8dde2b712b3 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 24 Jun 2025 14:47:44 +0200 Subject: [PATCH 26/75] refactor: handle sandbox connection client side but fetch root + details server side --- src/app/api/sandboxes/[id]/list/route.ts | 68 ---------- src/app/api/sandboxes/[id]/watch/route.ts | 117 ---------------- .../dashboard/sandbox/inspect/context.tsx | 72 ++++++---- .../inspect/filesystem/events-manager.ts | 121 +++++++---------- .../sandbox/inspect/filesystem/store.ts | 26 ++-- .../sandbox/inspect/filesystem/types.ts | 6 +- .../dashboard/sandbox/overview.mermaid | 117 +++++++++------- src/lib/clients/sandbox-pool.ts | 111 ---------------- src/lib/clients/watch-dir-pool.ts | 125 ------------------ src/server/sandboxes/get-sandbox-root.ts | 14 +- src/types/filesystem.ts | 17 --- 11 files changed, 183 insertions(+), 611 deletions(-) delete mode 100644 src/app/api/sandboxes/[id]/list/route.ts delete mode 100644 src/app/api/sandboxes/[id]/watch/route.ts delete mode 100644 src/lib/clients/sandbox-pool.ts delete mode 100644 src/lib/clients/watch-dir-pool.ts delete mode 100644 src/types/filesystem.ts diff --git a/src/app/api/sandboxes/[id]/list/route.ts b/src/app/api/sandboxes/[id]/list/route.ts deleted file mode 100644 index ff021d1c0..000000000 --- a/src/app/api/sandboxes/[id]/list/route.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { NextRequest } from 'next/server' -import { SandboxPool } from '@/lib/clients/sandbox-pool' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { createRouteClient } from '@/lib/clients/supabase/server' -import { FileType } from 'e2b' -import { FsEntry, FsFileType } from '@/types/filesystem' - -export const maxDuration = 60 // quick, single call -export const dynamic = 'force-dynamic' -export const fetchCache = 'force-no-store' - -/** - * GET /api/sandboxes/{id}/list?dir=/path&team= - * Returns JSON array of EntryInfo for the directory. - */ -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { - const { id } = await params - - const { searchParams } = new URL(request.url) - const dir = searchParams.get('dir') ?? '/' - const teamId = searchParams.get('team') ?? '' - - const supabase = createRouteClient(request) - const { - data: { session }, - } = await supabase.auth.getSession() - if (!session?.access_token) - return new Response('Unauthorized', { status: 401 }) - - const opts = { - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - } - - let entries: FsEntry[] = [] - let error: unknown - try { - const sandbox = await SandboxPool.acquire(id, opts) - const raw = await sandbox.files.list(dir) - entries = raw.map((e) => ({ - name: e.name, - path: e.path, - type: - e.type === FileType.DIR - ? ('dir' as FsFileType) - : ('file' as FsFileType), - })) - } catch (err) { - error = err - } finally { - await SandboxPool.release(id) - } - - if (error) { - console.error('Dir list error', error) - return new Response('Failed to list directory', { status: 500 }) - } - - return new Response(JSON.stringify(entries), { - headers: { - 'Content-Type': 'application/json', - }, - }) -} diff --git a/src/app/api/sandboxes/[id]/watch/route.ts b/src/app/api/sandboxes/[id]/watch/route.ts deleted file mode 100644 index da4fe44d3..000000000 --- a/src/app/api/sandboxes/[id]/watch/route.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { NextRequest } from 'next/server' -import { WatchDirPool } from '@/lib/clients/watch-dir-pool' -import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { createRouteClient } from '@/lib/clients/supabase/server' -import { VERBOSE } from '@/configs/flags' -import { logDebug } from '@/lib/clients/logger' - -export const maxDuration = 600 // 10 minutes - -/** - * SSE endpoint that streams filesystem events for a sandbox directory. - * - * Request: GET /api/sandboxes/{id}/watch?dir=/path - * - * The caller must be authenticated (via Supabase session cookie) so that we - * can forward the JWT to the E2B backend. - */ -export async function GET( - request: NextRequest, - { - params, - }: { - params: Promise<{ id: string }> - } -) { - const { id } = await params - - const { searchParams } = new URL(request.url) - const dir = searchParams.get('dir') ?? '/' - const teamId = searchParams.get('team') ?? '' - - if (VERBOSE) logDebug('WatchRoute.init', { id, dir, teamId }) - - const supabase = createRouteClient(request) - - const { - data: { session }, - } = await supabase.auth.getSession() - if (!session?.access_token) { - return new Response('Unauthorized', { status: 401 }) - } - - const sandboxOpts = { - headers: { - ...SUPABASE_AUTH_HEADERS(session.access_token, teamId), - }, - } - - if (VERBOSE) logDebug('WatchRoute.sandboxOpts') - - let watcherReleased = false - let ping: ReturnType | undefined - let onEvent: (ev: unknown) => void - let cleanup: () => void - - const stream = new ReadableStream({ - async start(controller) { - const encoder = new TextEncoder() - - onEvent = (ev: unknown) => { - controller.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`)) - } - - if (VERBOSE) logDebug('WatchRoute.acquireWatcher') - - await WatchDirPool.acquire(id, dir, onEvent, sandboxOpts) - - if (VERBOSE) logDebug('WatchRoute.watcherReady') - - // helper that performs a full teardown exactly once - cleanup = () => { - if (!watcherReleased) { - watcherReleased = true - if (VERBOSE) logDebug('WatchRoute.cleanup') - void WatchDirPool.release(id, dir, onEvent) - } - if (ping) clearInterval(ping) - controller.close() - } - - // periodic comment ping to keep intermediary proxies / clients happy - ping = setInterval(() => { - try { - controller.enqueue(encoder.encode(`: ping\n\n`)) - } catch (err) { - if (VERBOSE) logDebug('WatchRoute.pingError', err) - cleanup() - } - }, 5_000) - - request.signal.addEventListener('abort', () => { - if (VERBOSE) logDebug('WatchRoute.abort') - cleanup() - }) - }, - /** - * This runs if the ReadableStream is cancelled *without* the `abort` event - * (for example `response.body.cancel()` or an abrupt GC). At this point we - * no longer have a reference to the original `onEvent` callback, so we - * cannot call `WatchDirPool.release(...)` accurately. Instead we just mark - * the watcher as released; the pool's idle-timer will close the underlying - * gRPC stream after `GRACE_MS` once it sees the ref-count hasn't changed. - */ - cancel() { - if (VERBOSE) logDebug('WatchRoute.cancelStream') - cleanup?.() - }, - }) - - return new Response(stream, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - }, - }) -} diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 5fd9e26f3..afcafebeb 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -7,13 +7,18 @@ import React, { ReactNode, useLayoutEffect, useMemo, + useState, } from 'react' -import { FsEntry } from '@/types/filesystem' import { createFilesystemStore, type FilesystemStore } from './filesystem/store' import { FilesystemNode, FilesystemOperations } from './filesystem/types' import { FilesystemEventManager } from './filesystem/events-manager' import { getParentPath, normalizePath } from '@/lib/utils/filesystem' import { useSandboxContext } from '../context' +import Sandbox, { EntryInfo, FileType } from 'e2b' +import { SUPABASE_AUTH_HEADERS } from '@/configs/api' +import { supabase } from '@/lib/clients/supabase/client' +import { useRouter } from 'next/navigation' +import { AUTH_URLS } from '@/configs/urls' interface SandboxInspectContextValue { store: FilesystemStore @@ -27,26 +32,24 @@ const SandboxInspectContext = createContext( interface SandboxInspectProviderProps { children: ReactNode - teamId: string rootPath: string - seedEntries?: FsEntry[] + teamId: string + seedEntries?: EntryInfo[] } export function SandboxInspectProvider({ children, - teamId, rootPath, seedEntries, + teamId, }: SandboxInspectProviderProps) { const { sandboxInfo } = useSandboxContext() const storeRef = useRef(null) const eventManagerRef = useRef(null) const operationsRef = useRef(null) + const [sandbox, setSandbox] = useState(null) - const sandboxId = useMemo( - () => sandboxInfo.sandboxID + '-' + sandboxInfo.clientID, - [sandboxInfo.sandboxID, sandboxInfo.clientID] - ) + const router = useRouter() /* * ---------- synchronous store initialisation ---------- @@ -78,7 +81,7 @@ export function SandboxInspectProvider({ { name: rootName, path: normalizedRoot, - type: 'dir', + type: FileType.DIR, isExpanded: true, isLoaded: true, children: [], @@ -92,10 +95,10 @@ export function SandboxInspectProvider({ path: normalizePath(entry.path), } - if (entry.type === 'dir') { + if (entry.type === FileType.DIR) { return { ...base, - type: 'dir' as const, + type: FileType.DIR, isExpanded: false, isLoaded: false, children: [], @@ -104,7 +107,7 @@ export function SandboxInspectProvider({ return { ...base, - type: 'file' as const, + type: FileType.FILE, } }) @@ -124,7 +127,7 @@ export function SandboxInspectProvider({ const state = store.getState() const node = state.getNode(normalizedPath) - if (!node || node.type !== 'dir') return + if (!node || node.type !== FileType.DIR) return const newExpandedState = !node.isExpanded state.setExpanded(normalizedPath, newExpandedState) @@ -144,25 +147,44 @@ export function SandboxInspectProvider({ * ---------- watcher (side-effect) initialisation / cleanup ---------- */ useLayoutEffect(() => { - if (!storeRef.current) return + const connectSandbox = async () => { + if (!storeRef.current) return + + // (re)create the event-manager when sandbox / team / root changes + if (eventManagerRef.current) { + eventManagerRef.current.stopWatching() + } - // (re)create the event-manager when sandbox / team / root changes - if (eventManagerRef.current) { - eventManagerRef.current.stopWatching() + const { data } = await supabase.auth.getSession() + + if (!data || !data.session) { + router.replace(AUTH_URLS.SIGN_IN) + return + } + + const sandbox = await Sandbox.connect(sandboxInfo.sandboxID, { + headers: { + ...SUPABASE_AUTH_HEADERS(data.session?.access_token, teamId), + }, + }) + + setSandbox(sandbox) + + eventManagerRef.current = new FilesystemEventManager( + storeRef.current, + sandbox, + rootPath + ) } - eventManagerRef.current = new FilesystemEventManager( - storeRef.current, - sandboxId, - teamId, - rootPath - ) + + connectSandbox() return () => { eventManagerRef.current?.stopWatching() } - }, [sandboxId, teamId, rootPath]) + }, [sandboxInfo.sandboxID, teamId, rootPath, router]) - if (!storeRef.current || !operationsRef.current) { + if (!storeRef.current || !operationsRef.current || !sandbox) { return null // should never happen, but satisfies type-checker } diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index 167270902..669277ffd 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -1,46 +1,53 @@ -import { FsEvent, FsEntry } from '@/types/filesystem' +import { + FileType, + type Sandbox, + type FilesystemEvent, + type WatchHandle, + type EntryInfo, + FilesystemEventType, +} from 'e2b' import type { FilesystemStore } from './store' import { FilesystemNode } from './types' import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' export class FilesystemEventManager { - private unsubscribe?: () => void + private watchHandle?: WatchHandle private readonly rootPath: string private store: FilesystemStore - private sandboxId: string - private teamId: string - - constructor( - store: FilesystemStore, - sandboxId: string, - teamId: string, - rootPath: string - ) { + private sandbox: Sandbox + + constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string) { this.store = store - this.sandboxId = sandboxId - this.teamId = teamId + this.sandbox = sandbox this.rootPath = normalizePath(rootPath) + // Immediately start a single recursive watcher at the root void this.startRootWatcher() } private async startRootWatcher(): Promise { - if (this.unsubscribe) return + if (this.watchHandle) return - this.unsubscribe = openWatcher( - this.sandboxId, - this.rootPath, - this.teamId, - (event) => this.handleFilesystemEvent(event) - ) + try { + this.watchHandle = await this.sandbox.files.watchDir( + this.rootPath, + (event) => this.handleFilesystemEvent(event), + { recursive: true } + ) + } catch (error) { + console.error(`Failed to start root watcher on ${this.rootPath}:`, error) + throw error + } } stopWatching(): void { - this.unsubscribe?.() - this.unsubscribe = undefined + if (this.watchHandle) { + this.watchHandle.stop() + this.watchHandle = undefined + } } - private handleFilesystemEvent(event: FsEvent): void { + private handleFilesystemEvent(event: FilesystemEvent): void { const { type, name } = event // "name" is relative to the watched root; construct absolute path @@ -53,9 +60,13 @@ export class FilesystemEventManager { const parentNode = state.getNode(parentDir) switch (type) { - case 'create': - case 'rename': - if (!parentNode || parentNode.type !== 'dir' || !parentNode.isLoaded) { + case FilesystemEventType.CREATE: + case FilesystemEventType.RENAME: + if ( + !parentNode || + parentNode.type !== FileType.DIR || + !parentNode.isLoaded + ) { console.debug( `Skip refresh for '${normalizedPath}' because parent directory '${parentDir}' does not exist in store` ) @@ -68,7 +79,7 @@ export class FilesystemEventManager { void this.refreshDirectory(parentDir) break - case 'remove': + case FilesystemEventType.REMOVE: if (!state.getNode(normalizedPath)) { console.debug( `Skip remove for '${normalizedPath}' because node does not exist in store` @@ -82,8 +93,8 @@ export class FilesystemEventManager { this.handleRemoveEvent(normalizedPath) break - case 'write': - case 'chmod': + case FilesystemEventType.WRITE: + case FilesystemEventType.CHMOD: console.debug(`Ignoring ${type} event for '${normalizedPath}'`) break @@ -115,7 +126,7 @@ export class FilesystemEventManager { if ( !node || - node.type !== 'dir' || + node.type !== FileType.DIR || node.isLoaded || state.loadingPaths.has(normalizedPath) ) @@ -125,14 +136,14 @@ export class FilesystemEventManager { state.setError(normalizedPath) // clear any previous errors try { - const entries = await listDir(this.sandboxId, normalizedPath, this.teamId) + const entries = await this.sandbox.files.list(normalizedPath) - const nodes: FilesystemNode[] = entries.map((entry) => { - if (entry.type === 'dir') { + const nodes: FilesystemNode[] = entries.map((entry: EntryInfo) => { + if (entry.type === FileType.DIR) { return { name: entry.name, path: entry.path, - type: 'dir', + type: FileType.DIR, isExpanded: false, isSelected: false, isLoaded: false, @@ -142,7 +153,7 @@ export class FilesystemEventManager { return { name: entry.name, path: entry.path, - type: 'file', + type: FileType.FILE, isSelected: false, } } @@ -167,7 +178,7 @@ export class FilesystemEventManager { state.updateNode(normalizedPath, { isLoaded: false }) const node = state.getNode(normalizedPath) - if (node && node.type === 'dir') { + if (node && node.type === FileType.DIR) { const childrenPaths = [...node.children] for (const childPath of childrenPaths) { state.removeNode(childPath) @@ -177,41 +188,3 @@ export class FilesystemEventManager { await this.loadDirectory(normalizedPath) } } - -async function listDir( - sandboxId: string, - dir: string, - teamId: string -): Promise { - const url = `/api/sandboxes/${sandboxId}/list?dir=${encodeURIComponent(dir)}&team=${teamId}` - return fetch(url, { credentials: 'include' }).then((r) => { - if (!r.ok) throw new Error(`List failed ${r.status}`) - return r.json() - }) -} - -function openWatcher( - sandboxId: string, - dir: string, - teamId: string, - onEvent: (e: FsEvent) => void -): () => void { - let es: EventSource | null - - const connect = () => { - const url = `/api/sandboxes/${sandboxId}/watch?dir=${encodeURIComponent(dir)}&team=${teamId}` - es = new EventSource(url, { withCredentials: true }) - - es.onmessage = (ev) => { - onEvent(JSON.parse(ev.data)) - } - es.onerror = () => { - // auto-reconnect in 1 s - es?.close() - setTimeout(connect, 1_000) - } - } - - connect() - return () => es?.close() -} diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 92ca3aae4..7d625ace9 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -8,8 +8,8 @@ import { getParentPath, isChildPath, } from '@/lib/utils/filesystem' -import { FsEntry } from '@/types/filesystem' import { FilesystemNode } from './types' +import { FileType } from 'e2b' enableMapSet() @@ -79,14 +79,14 @@ export const createFilesystemStore = (rootPath: string) => parentNode = { name: parentName, path: normalizedParentPath, - type: 'dir', + type: FileType.DIR, isExpanded: false, children: [], } state.nodes.set(normalizedParentPath, parentNode) } - if (parentNode.type === 'file') { + if (parentNode.type === FileType.FILE) { console.error('Parent node is a file', parentNode) return } @@ -118,8 +118,10 @@ export const createFilesystemStore = (rootPath: string) => if (!nodeA || !nodeB) return 0 // directories first - if (nodeA.type === 'dir' && nodeB.type === 'file') return -1 - if (nodeA.type === 'file' && nodeB.type === 'dir') return 1 + if (nodeA.type === FileType.DIR && nodeB.type === FileType.FILE) + return -1 + if (nodeA.type === FileType.FILE && nodeB.type === FileType.DIR) + return 1 // then alphabetically return nodeA.name.localeCompare(nodeB.name) @@ -136,7 +138,7 @@ export const createFilesystemStore = (rootPath: string) => const parentPath = getParentPath(normalizedPath) const parentNode = state.nodes.get(parentPath) - if (parentNode && parentNode.type === 'dir') { + if (parentNode && parentNode.type === FileType.DIR) { parentNode.children = parentNode.children.filter( (childPath: string) => childPath !== normalizedPath ) @@ -180,7 +182,7 @@ export const createFilesystemStore = (rootPath: string) => if (!node) return - if (node?.type === 'file') { + if (node?.type === FileType.FILE) { console.error('Cannot expand file', node) return } @@ -222,7 +224,7 @@ export const createFilesystemStore = (rootPath: string) => const node = state.nodes.get(normalizedPath) - if (!node || node.type === 'file') return + if (!node || node.type === FileType.FILE) return node.isLoading = loading }) @@ -240,7 +242,7 @@ export const createFilesystemStore = (rootPath: string) => const node = state.nodes.get(normalizedPath) - if (!node || node.type === 'file') return + if (!node || node.type === FileType.FILE) return node.error = error }) @@ -260,7 +262,7 @@ export const createFilesystemStore = (rootPath: string) => const state = get() const node = state.nodes.get(normalizedPath) - if (!node || node.type === 'file') return [] + if (!node || node.type === FileType.FILE) return [] const cached = childrenCache.get(normalizedPath) if (cached && cached.ref === node.children) { @@ -284,7 +286,7 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) const node = get().nodes.get(normalizedPath) - if (!node || node.type === 'file') return false + if (!node || node.type === FileType.FILE) return false return !!node.isExpanded }, @@ -302,7 +304,7 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) const node = get().nodes.get(normalizedPath) - if (!node || node.type === 'file') return false + if (!node || node.type === FileType.FILE) return false return node.children.length > 0 }, diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index a70867333..4f5365a8e 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -1,7 +1,7 @@ -import { FsFileType } from '@/types/filesystem' +import { FileType } from 'e2b' interface FilesystemDir { - type: 'dir' + type: FileType.DIR name: string path: string children: string[] // paths of children @@ -13,7 +13,7 @@ interface FilesystemDir { } interface FilesystemFile { - type: 'file' + type: FileType.FILE name: string path: string isSelected?: boolean diff --git a/src/features/dashboard/sandbox/overview.mermaid b/src/features/dashboard/sandbox/overview.mermaid index 283483ed8..f38f160ad 100644 --- a/src/features/dashboard/sandbox/overview.mermaid +++ b/src/features/dashboard/sandbox/overview.mermaid @@ -1,4 +1,24 @@ flowchart TD + +%% -------------------------- +%% Server-side Components +%% -------------------------- +subgraph SERVER_FETCH["Server-side Fetch"] + direction TB + LAYOUT["layout.tsx (server)"] + PAGE["page.tsx (server)"] + DETAILS_ACTION["getSandboxDetails (action)"] + ROOT_ACTION["getSandboxRoot (action)"] + + LAYOUT -- "calls" --> DETAILS_ACTION + PAGE -- "calls" --> ROOT_ACTION + DETAILS_ACTION -- "returns sandboxInfo" --> SANDBOX_PROVIDER + ROOT_ACTION -- "returns root entries" --> INSPECT_PROVIDER +end + +%% -------------------------- +%% Client Contexts +%% -------------------------- subgraph SANDBOX_CONTEXT["Sandbox Context"] direction TB SANDBOX_PROVIDER["SandboxProvider"] @@ -7,38 +27,28 @@ subgraph SANDBOX_CONTEXT["Sandbox Context"] SANDBOX_PROVIDER -- "tracks lifecycle" --> SANDBOX_STATE end -%% ---------- New Server-side handling ---------- -subgraph SERVER_SIDE["Server Runtime (per Vercel instance)"] - direction TB - SANDBOX_POOL["SandboxPool"] - WATCH_POOL["WatchDirPool"] - LIST_ROUTE["/list Route"] - WATCH_ROUTE["/watch Route (SSE)"] - - SANDBOX_POOL -- "1 per sandbox" --> WATCH_POOL - LIST_ROUTE -- "files.list()" --> SANDBOX_POOL - WATCH_ROUTE -- "watchDir stream" --> WATCH_POOL -end - subgraph INSPECT_CONTEXT["Inspect Context"] direction TB INSPECT_PROVIDER["SandboxInspectProvider"] + SANDBOX_INSTANCE["Sandbox Instance (connected)"] FILESYSTEM_STORE["FilesystemStore"] EVENT_MANAGER["FilesystemEventManager (root recursive watcher)"] OPERATIONS["Operations Object"] + INSPECT_PROVIDER -- "connects" --> SANDBOX_INSTANCE INSPECT_PROVIDER -- "creates singleton" --> FILESYSTEM_STORE - INSPECT_PROVIDER -- "creates with store" --> EVENT_MANAGER + INSPECT_PROVIDER -- "creates with store + sandbox" --> EVENT_MANAGER INSPECT_PROVIDER -- "exposes interface" --> OPERATIONS + + SANDBOX_INSTANCE -- "used by" --> EVENT_MANAGER EVENT_MANAGER -- "writes FS data" --> FILESYSTEM_STORE - OPERATIONS -- "delegates to" --> EVENT_MANAGER - OPERATIONS -- "writes UI flags" --> FILESYSTEM_STORE + OPERATIONS -- "delegates to" --> EVENT_MANAGER + OPERATIONS -- "writes UI flags" --> FILESYSTEM_STORE end -%% Connections between client and server -EVENT_MANAGER -- "GET /list" --> LIST_ROUTE -EVENT_MANAGER -- "SSE /watch" --> WATCH_ROUTE - +%% -------------------------- +%% Hook Layer +%% -------------------------- subgraph HOOKS["Hook Layer"] direction TB FILESYSTEM_HOOKS["Filesystem Hooks"] @@ -46,58 +56,67 @@ subgraph HOOKS["Hook Layer"] NODE_HOOKS["Node Hooks"] FILESYSTEM_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE - DIRECTORY_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE - NODE_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE + DIRECTORY_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE + NODE_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE FILESYSTEM_HOOKS -- "return operations" --> OPERATIONS - DIRECTORY_HOOKS -- "return operations" --> OPERATIONS - NODE_HOOKS -- "return operations" --> OPERATIONS + DIRECTORY_HOOKS -- "return operations" --> OPERATIONS + NODE_HOOKS -- "return operations" --> OPERATIONS end +%% -------------------------- +%% UI Components +%% -------------------------- subgraph UI_COMPONENTS["UI Components"] direction LR FILE_TREE["FileTree"] CODE_EDITOR["Code Editor"] OTHER_UI["Other Components"] - FILE_TREE -- "trigger user actions" --> USER_ACTIONS["User Actions"] + FILE_TREE -- "trigger user actions" --> USER_ACTIONS["User Actions"] CODE_EDITOR -- "trigger user actions" --> USER_ACTIONS - OTHER_UI -- "trigger user actions" --> USER_ACTIONS + OTHER_UI -- "trigger user actions" --> USER_ACTIONS end -subgraph E2B_REMOTE["E2B Cloud"] +%% -------------------------- +%% Remote (E2B) +%% -------------------------- +subgraph E2B_REMOTE["E2B Remote"] REMOTE_SANDBOX["Remote Sandbox"] -end + FS_EVENTS["Filesystem Events"] -%% External connectivity -REMOTE_SANDBOX -- "SDK REST + unary gRPC" --> SANDBOX_POOL -REMOTE_SANDBOX -- "watchDir server-stream" --> WATCH_POOL - -%% Client-Server boundary -SERVER_SIDE -- "HTTP (JSON) / SSE" --> EVENT_MANAGER + REMOTE_SANDBOX -- "emits real-time" --> FS_EVENTS +end +%% -------------------------- %% Data Flow: User Actions -USER_ACTIONS -- "call hooks" --> OPERATIONS -OPERATIONS -- "async list / watch" --> EVENT_MANAGER - -%% Flow inside client -FILESYSTEM_STORE -- "triggers re-renders" --> HOOKS -HOOKS -- "provide updated state" --> UI_COMPONENTS - -%% Hook Integration -HOOKS -- "consumed by" --> UI_COMPONENTS - +%% -------------------------- +USER_ACTIONS -- "call hooks that return" --> OPERATIONS +OPERATIONS -- "async calls to" --> EVENT_MANAGER +EVENT_MANAGER -- "API calls to" --> REMOTE_SANDBOX + +%% -------------------------- +%% Data Flow: Remote Events +%% -------------------------- +FS_EVENTS -- "handled by" --> EVENT_MANAGER +FILESYSTEM_STORE -- "triggers re-renders via" --> HOOKS +HOOKS -- "provide updated state to" --> UI_COMPONENTS + +%% -------------------------- %% Styling +%% -------------------------- classDef contextClass fill:#E3F2FD,stroke:#1976D2,stroke-width:2px -classDef storeClass fill:#E8F5E8,stroke:#388E3C,stroke-width:2px +classDef storeClass fill:#E8F5E8,stroke:#388E3C,stroke-width:2px classDef managerClass fill:#FFF3E0,stroke:#F57C00,stroke-width:2px -classDef hooksClass fill:#FCE4EC,stroke:#C2185B,stroke-width:2px -classDef uiClass fill:#F1F8E9,stroke:#689F38,stroke-width:2px -classDef remoteClass fill:#FFEBEE,stroke:#D32F2F,stroke-width:2px +classDef hooksClass fill:#FCE4EC,stroke:#C2185B,stroke-width:2px +classDef uiClass fill:#F1F8E9,stroke:#689F38,stroke-width:2px +classDef remoteClass fill:#FFEBEE,stroke:#D32F2F,stroke-width:2px +classDef serverClass fill:#ECEFF1,stroke:#455A64,stroke-width:2px class SANDBOX_PROVIDER,INSPECT_PROVIDER contextClass class FILESYSTEM_STORE storeClass class EVENT_MANAGER,OPERATIONS managerClass class FILESYSTEM_HOOKS,DIRECTORY_HOOKS,NODE_HOOKS hooksClass class FILE_TREE,CODE_EDITOR,OTHER_UI,USER_ACTIONS uiClass -class REMOTE_SANDBOX remoteClass \ No newline at end of file +class REMOTE_SANDBOX,FS_EVENTS remoteClass +class LAYOUT,PAGE,DETAILS_ACTION,ROOT_ACTION serverClass \ No newline at end of file diff --git a/src/lib/clients/sandbox-pool.ts b/src/lib/clients/sandbox-pool.ts deleted file mode 100644 index b6461d4af..000000000 --- a/src/lib/clients/sandbox-pool.ts +++ /dev/null @@ -1,111 +0,0 @@ -import 'server-cli-only' - -import { Sandbox, type SandboxOpts } from 'e2b' -import { VERBOSE } from '@/configs/flags' -import { logDebug } from './logger' - -/** - * How long we keep the connection alive after the last consumer released it. - * A short grace period avoids connect/disconnect thrashing when the browser - * refreshes or multiple API calls arrive in quick succession. - */ -const GRACE_MS = 10_000 - -interface Entry { - /** Pending or resolved connect promise */ - promise: Promise - /** Resolved sandbox instance (set after promise fulfils) */ - sandbox?: Sandbox - /** Number of active users of this connection */ - ref: number - /** Handle for delayed close */ - timer?: ReturnType -} - -// --------------------------------------------- -// Global singleton (per Node) -// --------------------------------------------- -// eslint-disable-next-line no-var -declare global { - // `var` is required for global augmentation – suppressed for eslint - // eslint-disable-next-line no-var - var __SBX_POOL: Map | undefined -} - -const POOL: Map = (globalThis.__SBX_POOL ??= new Map< - string, - Entry ->()) - -export class SandboxPool { - /** - * Acquire (or create) a shared sandbox connection for `sandboxId`. - * Each caller MUST call `release()` when finished. - */ - static async acquire( - sandboxId: string, - opts: SandboxOpts - ): Promise { - let entry = POOL.get(sandboxId) - - if (entry) { - entry.ref += 1 - clearTimeout(entry.timer) - if (VERBOSE) - logDebug('SandboxPool.acquire reuse', sandboxId, 'refs', entry.ref) - } else { - if (VERBOSE) logDebug('SandboxPool.acquire connect', sandboxId) - const promise = Sandbox.connect(sandboxId, opts) as Promise - entry = { promise, ref: 1 } - POOL.set(sandboxId, entry) - - // Cache resolved instance, drop entry if connect fails - promise - .then((sbx) => { - entry!.sandbox = sbx - if (VERBOSE) logDebug('SandboxPool connected', sandboxId) - }) - .catch((err) => { - if (VERBOSE) logDebug('SandboxPool connect FAILED', sandboxId, err) - POOL.delete(sandboxId) - }) - } - - if (VERBOSE) logDebug('SandboxPool.acquire return', sandboxId, 'promise') - return entry.promise as Promise - } - - /** - * Release one reference obtained via `acquire()`. The connection is closed - * after `GRACE_MS` when no other consumers remain. - */ - static async release(sandboxId: string): Promise { - const entry = POOL.get(sandboxId) - if (!entry) return - - entry.ref = Math.max(0, entry.ref - 1) - - if (VERBOSE) logDebug('SandboxPool.release', sandboxId, 'refs', entry.ref) - - if (entry.ref === 0 && !entry.timer) { - if (VERBOSE) - logDebug('SandboxPool schedule close', sandboxId, `in ${GRACE_MS}ms`) - entry.timer = setTimeout(async () => { - if (entry.ref === 0) { - if (VERBOSE) logDebug('SandboxPool closing', sandboxId) - try { - const closable = entry.sandbox as unknown as { - close?: () => Promise - dispose?: () => Promise - } - if (closable?.close) await closable.close() - else if (closable?.dispose) await closable.dispose() - } finally { - POOL.delete(sandboxId) - if (VERBOSE) logDebug('SandboxPool closed', sandboxId) - } - } - }, GRACE_MS) - } - } -} diff --git a/src/lib/clients/watch-dir-pool.ts b/src/lib/clients/watch-dir-pool.ts deleted file mode 100644 index 1a0b8717c..000000000 --- a/src/lib/clients/watch-dir-pool.ts +++ /dev/null @@ -1,125 +0,0 @@ -import 'server-cli-only' - -import { WatchHandle, FilesystemEvent } from 'e2b' -import { SandboxPool } from './sandbox-pool' -import { VERBOSE } from '@/configs/flags' -import { logDebug } from './logger' - -// Grace period in milliseconds before cleaning up unused watch handles – -// 30 s gives background tabs enough time to reconnect after throttling. -const GRACE_MS = 10_000 - -interface Entry { - // Promise that resolves to the watch handle once created - promise: Promise - // The actual watch handle once available - handle?: WatchHandle - // Set of callback functions from all consumers watching this directory - consumers: Set<(e: FilesystemEvent) => void> - // Reference count of active consumers - ref: number - // Timer for cleanup when ref count reaches 0 - timer?: ReturnType -} - -// --------------------------------------------- -// Global singleton (per Node) -// --------------------------------------------- -// eslint-disable-next-line no-var -declare global { - // `var` is required for global augmentation – suppressed for eslint - // eslint-disable-next-line no-var - var __WATCH_POOL: Map | undefined -} - -const POOL: Map = (globalThis.__WATCH_POOL ??= new Map< - string, - Entry ->()) - -function makeKey(sandboxId: string, dir: string) { - return `${sandboxId}:${dir}` -} - -export class WatchDirPool { - /** - * Acquire (or create) a shared WatchHandle. Multiple callers are - * fanned-out via an internal consumer list—no mutation of the SDK types. - */ - static async acquire( - sandboxId: string, - dir: string, - onEvent: (ev: FilesystemEvent) => void, - sandboxOpts: Parameters[1] - ): Promise { - const key = makeKey(sandboxId, dir) - let entry = POOL.get(key) - - if (VERBOSE) logDebug('WatchDirPool.acquire', key) - - if (entry) { - entry.ref += 1 - entry.consumers.add(onEvent) - clearTimeout(entry.timer) - if (VERBOSE) logDebug('WatchDirPool.reuse', key, 'refs', entry.ref) - } else { - if (VERBOSE) logDebug('WatchDirPool.createWatcher', key) - entry = { - ref: 1, - consumers: new Set([onEvent as (ev: FilesystemEvent) => void]), - promise: (async () => { - const sbx = await SandboxPool.acquire(sandboxId, sandboxOpts) - if (VERBOSE) - logDebug('WatchDirPool.connectedToSandbox', sandboxId, 'dir', dir) - const handle = await sbx.files.watchDir( - dir, - (ev) => entry!.consumers.forEach((fn) => fn(ev)), - { recursive: true } - ) - entry!.handle = handle - if (VERBOSE) logDebug('WatchDirPool.watcherReady', key) - return handle - })(), - } - POOL.set(key, entry) - } - - return entry.promise - } - - /** - * Release one reference. When the last reference is gone the underlying - * stream is closed after GRACE_MS. - */ - static async release( - sandboxId: string, - dir: string, - onEvent: (ev: FilesystemEvent) => void - ): Promise { - const key = makeKey(sandboxId, dir) - const entry = POOL.get(key) - if (!entry) return - - entry.ref = Math.max(0, entry.ref - 1) - entry.consumers.delete(onEvent) - - if (VERBOSE) logDebug('WatchDirPool.release', key, 'refs', entry.ref) - - if (entry.ref === 0 && !entry.timer) { - if (VERBOSE) - logDebug('WatchDirPool.scheduleStop', key, `in ${GRACE_MS}ms`) - entry.timer = setTimeout(async () => { - if (entry.ref === 0) { - if (VERBOSE) logDebug('WatchDirPool.stopping', key) - try { - await entry.handle?.stop() - await SandboxPool.release(sandboxId) - } finally { - POOL.delete(key) - if (VERBOSE) logDebug('WatchDirPool.stopped', key) - } - } - }, GRACE_MS) - } - } -} diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index af83ad785..ae23b587e 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -5,9 +5,7 @@ import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { ERROR_CODES } from '@/configs/logs' import { logError } from '@/lib/clients/logger' import { returnServerError } from '@/lib/utils/action' -import { SandboxPool } from '@/lib/clients/sandbox-pool' -import { FsFileType } from '@/types/filesystem' -import { FileType } from 'e2b' +import Sandbox from 'e2b' export const GetSandboxRootSchema = z.object({ teamId: z.string().uuid(), @@ -25,24 +23,20 @@ export const getSandboxRoot = authActionClient const headers = SUPABASE_AUTH_HEADERS(session.access_token, teamId) let entries + try { - const sandbox = await SandboxPool.acquire(sandboxId, { + const sandbox = await Sandbox.connect(sandboxId, { headers, }) const raw = await sandbox.files.list(rootPath) entries = raw.map((e) => ({ name: e.name, path: e.path, - type: - e.type === FileType.DIR - ? ('dir' as FsFileType) - : ('file' as FsFileType), + type: e.type, })) } catch (err) { logError(ERROR_CODES.INFRA, 'files.list', 500, err) return returnServerError('Failed to list sandbox directory.') - } finally { - await SandboxPool.release(sandboxId) } return { diff --git a/src/types/filesystem.ts b/src/types/filesystem.ts deleted file mode 100644 index cd6caece7..000000000 --- a/src/types/filesystem.ts +++ /dev/null @@ -1,17 +0,0 @@ -// NOTE: We need to maintain duplicate types of the e2b sdk, in order to avoid having the whole sdk inside the client bundle. -// The issue here mainly stems from the FileType and FilesystemEvent enums. - -export type FsFileType = 'file' | 'dir' - -export interface FsEntry { - name: string - path: string - type: FsFileType -} - -export type FsEventType = 'create' | 'write' | 'remove' | 'rename' | 'chmod' - -export interface FsEvent { - name: string - type: FsEventType -} From 528d26774446b7686b5306b832700146f90cafed Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 24 Jun 2025 14:56:46 +0200 Subject: [PATCH 27/75] remove: rename E2B_DOMAIN env to NEXT_PUBLIC_E2B_DOMAIN for client side usage --- .env.example | 2 +- src/features/dashboard/sandbox/inspect/context.tsx | 1 + src/lib/env.ts | 2 +- src/server/sandboxes/get-sandbox-root.ts | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 4a7407250..6078881cf 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,7 @@ INFRA_API_URL=https://api.e2b.dev ### Default domain for the E2B SDK ### Used for Sandbox Details Page -E2B_DOMAIN=e2b.dev +NEXT_PUBLIC_E2B_DOMAIN=e2b.dev ### KV database configuration KV_REST_API_TOKEN= diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index afcafebeb..3e8e63f80 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -163,6 +163,7 @@ export function SandboxInspectProvider({ } const sandbox = await Sandbox.connect(sandboxInfo.sandboxID, { + domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, headers: { ...SUPABASE_AUTH_HEADERS(data.session?.access_token, teamId), }, diff --git a/src/lib/env.ts b/src/lib/env.ts index 84a179a88..05c49c88f 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -5,7 +5,7 @@ export const serverSchema = z.object({ INFRA_API_URL: z.string().url(), KV_REST_API_TOKEN: z.string().min(1), KV_REST_API_URL: z.string().url(), - E2B_DOMAIN: z.string(), + NEXT_PUBLIC_E2B_DOMAIN: z.string(), BILLING_API_URL: z.string().url().optional(), OTEL_SERVICE_NAME: z.string().optional(), diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index ae23b587e..3755fbf57 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -26,6 +26,7 @@ export const getSandboxRoot = authActionClient try { const sandbox = await Sandbox.connect(sandboxId, { + domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, headers, }) const raw = await sandbox.files.list(rootPath) From dfb18dd169921c53c1459ee90af64827b850e73a Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 24 Jun 2025 16:20:13 +0200 Subject: [PATCH 28/75] fix: connect to correct sandboxId on client and pass secure connection option --- src/features/dashboard/sandbox/inspect/context.tsx | 9 +++++++-- src/server/sandboxes/get-sandbox-root.ts | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 3e8e63f80..c126aef6b 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -51,6 +51,10 @@ export function SandboxInspectProvider({ const router = useRouter() + const sandboxId = useMemo(() => { + return sandboxInfo.sandboxID + '-' + sandboxInfo.clientID + }, [sandboxInfo.sandboxID, sandboxInfo.clientID]) + /* * ---------- synchronous store initialisation ---------- * We want the tree to render immediately using the "seedEntries" streamed from the @@ -162,11 +166,12 @@ export function SandboxInspectProvider({ return } - const sandbox = await Sandbox.connect(sandboxInfo.sandboxID, { + const sandbox = await Sandbox.connect(sandboxId, { domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, headers: { ...SUPABASE_AUTH_HEADERS(data.session?.access_token, teamId), }, + secure: true, }) setSandbox(sandbox) @@ -183,7 +188,7 @@ export function SandboxInspectProvider({ return () => { eventManagerRef.current?.stopWatching() } - }, [sandboxInfo.sandboxID, teamId, rootPath, router]) + }, [sandboxId, teamId, rootPath, router]) if (!storeRef.current || !operationsRef.current || !sandbox) { return null // should never happen, but satisfies type-checker diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index 3755fbf57..dea2ad763 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -28,6 +28,7 @@ export const getSandboxRoot = authActionClient const sandbox = await Sandbox.connect(sandboxId, { domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, headers, + secure: true, }) const raw = await sandbox.files.list(rootPath) entries = raw.map((e) => ({ From 90b2b40fe97ffa462f74d82f4ca8ce2bb5b8b3de Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 24 Jun 2025 18:37:54 +0200 Subject: [PATCH 29/75] feat: load directory debounce and unlimited watchDir timeout --- .../dashboard/sandbox/inspect/context.tsx | 5 +- .../inspect/filesystem/events-manager.ts | 76 ++++++++++++++++--- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index c126aef6b..f375312b2 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -47,7 +47,6 @@ export function SandboxInspectProvider({ const storeRef = useRef(null) const eventManagerRef = useRef(null) const operationsRef = useRef(null) - const [sandbox, setSandbox] = useState(null) const router = useRouter() @@ -174,8 +173,6 @@ export function SandboxInspectProvider({ secure: true, }) - setSandbox(sandbox) - eventManagerRef.current = new FilesystemEventManager( storeRef.current, sandbox, @@ -190,7 +187,7 @@ export function SandboxInspectProvider({ } }, [sandboxId, teamId, rootPath, router]) - if (!storeRef.current || !operationsRef.current || !sandbox) { + if (!storeRef.current || !operationsRef.current) { return null // should never happen, but satisfies type-checker } diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index 669277ffd..780b2b78f 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -16,6 +16,19 @@ export class FilesystemEventManager { private store: FilesystemStore private sandbox: Sandbox + // ms delay used when batching rapid load requests + private static readonly LOAD_DEBOUNCE_MS = 300 + + private loadTimers: Map> = new Map() + private pendingLoads: Map< + string, + { + promise: Promise + resolve: () => void + reject: (err: unknown) => void + } + > = new Map() + constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string) { this.store = store this.sandbox = sandbox @@ -32,7 +45,7 @@ export class FilesystemEventManager { this.watchHandle = await this.sandbox.files.watchDir( this.rootPath, (event) => this.handleFilesystemEvent(event), - { recursive: true } + { recursive: true, timeout: 0, timeoutMs: 0 } ) } catch (error) { console.error(`Failed to start root watcher on ${this.rootPath}:`, error) @@ -120,6 +133,49 @@ export class FilesystemEventManager { } async loadDirectory(path: string): Promise { + console.log('LOAD_DIRECTORY', path) + + const normalizedPath = normalizePath(path) + + // if there is already a scheduled load for this path, reset the timer and return its promise + const existingTimer = this.loadTimers.get(normalizedPath) + if (existingTimer) { + clearTimeout(existingTimer) + } + + let pending = this.pendingLoads.get(normalizedPath) + if (!pending) { + let res!: () => void + let rej!: (err: unknown) => void + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + pending = { promise, resolve: res, reject: rej } + this.pendingLoads.set(normalizedPath, pending) + } + + const timer = setTimeout(async () => { + // once the timer fires, perform the actual load then resolve/reject all waiters + this.loadTimers.delete(normalizedPath) + try { + await this.loadDirectoryImmediate(normalizedPath) + pending!.resolve() + } catch (err) { + pending!.reject(err) + } finally { + this.pendingLoads.delete(normalizedPath) + } + }, FilesystemEventManager.LOAD_DEBOUNCE_MS) + + this.loadTimers.set(normalizedPath, timer) + + return pending.promise + } + + private async loadDirectoryImmediate(path: string): Promise { + console.log('LOAD_DIRECTORY_IMMEDIATE', path) + const normalizedPath = normalizePath(path) const state = this.store.getState() const node = state.getNode(normalizedPath) @@ -160,6 +216,15 @@ export class FilesystemEventManager { }) state.addNodes(normalizedPath, nodes) + + const newChildrenSet = new Set(nodes.map((n) => normalizePath(n.path))) + + for (const childPath of [...node.children]) { + if (!newChildrenSet.has(childPath)) { + state.removeNode(childPath) + } + } + state.updateNode(normalizedPath, { isLoaded: true }) } catch (error) { const errorMessage = @@ -175,16 +240,9 @@ export class FilesystemEventManager { const normalizedPath = normalizePath(path) const state = this.store.getState() + // mark directory as stale but keep existing children until fresh data arrives state.updateNode(normalizedPath, { isLoaded: false }) - const node = state.getNode(normalizedPath) - if (node && node.type === FileType.DIR) { - const childrenPaths = [...node.children] - for (const childPath of childrenPaths) { - state.removeNode(childPath) - } - } - await this.loadDirectory(normalizedPath) } } From 5363cecf45b044baaf29ee313be8247b705382af Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 24 Jun 2025 18:55:24 +0200 Subject: [PATCH 30/75] feat: add sorting direction state and normalize order on node insert based on sorting function --- .../inspect/filesystem/events-manager.ts | 6 +-- .../sandbox/inspect/filesystem/store.ts | 42 ++++++++++++------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index 780b2b78f..9eda27af8 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -34,7 +34,7 @@ export class FilesystemEventManager { this.sandbox = sandbox this.rootPath = normalizePath(rootPath) - // Immediately start a single recursive watcher at the root + // immediately start a single recursive watcher at the root void this.startRootWatcher() } @@ -133,8 +133,6 @@ export class FilesystemEventManager { } async loadDirectory(path: string): Promise { - console.log('LOAD_DIRECTORY', path) - const normalizedPath = normalizePath(path) // if there is already a scheduled load for this path, reset the timer and return its promise @@ -174,8 +172,6 @@ export class FilesystemEventManager { } private async loadDirectoryImmediate(path: string): Promise { - console.log('LOAD_DIRECTORY_IMMEDIATE', path) - const normalizedPath = normalizePath(path) const state = this.store.getState() const node = state.getNode(normalizedPath) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 7d625ace9..b3bc74b93 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -23,6 +23,7 @@ export interface FilesystemState { selectedPath?: string loadingPaths: Set errorPaths: Map + sortingDirection: 'asc' | 'desc' } // mutations/actions that modify state @@ -56,6 +57,24 @@ export type FilesystemStoreData = FilesystemStatics & const childrenCache: Map = new Map() +function compareFilesystemNodes( + nodeA: FilesystemNode | undefined, + nodeB: FilesystemNode | undefined, + direction: 'asc' | 'desc' = 'asc' +): number { + if (!nodeA || !nodeB) return 0 + + if (nodeA.type === FileType.DIR && nodeB.type === FileType.FILE) return -1 + if (nodeA.type === FileType.FILE && nodeB.type === FileType.DIR) return 1 + + const cmp = nodeA.name.localeCompare(nodeB.name, undefined, { + sensitivity: 'base', + numeric: true, + }) + + return direction === 'asc' ? cmp : -cmp +} + export const createFilesystemStore = (rootPath: string) => create()( immer((set, get) => ({ @@ -64,6 +83,7 @@ export const createFilesystemStore = (rootPath: string) => nodes: new Map(), loadingPaths: new Set(), errorPaths: new Map(), + sortingDirection: 'asc' as 'asc' | 'desc', addNodes: (parentPath: string, nodes: FilesystemNode[]) => { const normalizedParentPath = normalizePath(parentPath) @@ -111,21 +131,13 @@ export const createFilesystemStore = (rootPath: string) => } } - parentNode.children.sort((a: string, b: string) => { - const nodeA = state.nodes.get(a) - const nodeB = state.nodes.get(b) - - if (!nodeA || !nodeB) return 0 - - // directories first - if (nodeA.type === FileType.DIR && nodeB.type === FileType.FILE) - return -1 - if (nodeA.type === FileType.FILE && nodeB.type === FileType.DIR) - return 1 - - // then alphabetically - return nodeA.name.localeCompare(nodeB.name) - }) + parentNode.children.sort((a: string, b: string) => + compareFilesystemNodes( + state.nodes.get(a), + state.nodes.get(b), + state.sortingDirection + ) + ) }) }, From fbac8236be08792293f627b8a1e3d0936936fb25 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 24 Jun 2025 19:34:07 +0200 Subject: [PATCH 31/75] chore: update e2b sdk --- .../dashboard/sandbox/inspect/filesystem/events-manager.ts | 2 +- src/server/sandboxes/get-sandbox-root.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index 9eda27af8..9ef1d738b 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -45,7 +45,7 @@ export class FilesystemEventManager { this.watchHandle = await this.sandbox.files.watchDir( this.rootPath, (event) => this.handleFilesystemEvent(event), - { recursive: true, timeout: 0, timeoutMs: 0 } + { recursive: true, timeoutMs: 0 } ) } catch (error) { console.error(`Failed to start root watcher on ${this.rootPath}:`, error) diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index dea2ad763..149a93530 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -37,8 +37,8 @@ export const getSandboxRoot = authActionClient type: e.type, })) } catch (err) { - logError(ERROR_CODES.INFRA, 'files.list', 500, err) - return returnServerError('Failed to list sandbox directory.') + logError(ERROR_CODES.E2B_SDK, 'files.list', err) + return returnServerError('Failed to list root directory.') } return { From a3bb3f58e30521884c921002f557baec6cb209e5 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 24 Jun 2025 19:40:53 +0200 Subject: [PATCH 32/75] fix: missing test env --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3865ed911..f31eaf29e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: SUPABASE_SERVICE_ROLE_KEY: test-service-role-key INFRA_API_URL: https://api.e2b-test.dev BILLING_API_URL: https://billing.e2b-test.dev - E2B_DOMAIN: e2b-test.dev + NEXT_PUBLIC_E2B_DOMAIN: e2b-test.dev NEXT_PUBLIC_POSTHOG_KEY: test-posthog-key NEXT_PUBLIC_SUPABASE_URL: https://test-supabase-url.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY: test-supabase-anon-key From 6d41d940e37ed1b9f7940aa5ca22b3134347cb9a Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Wed, 25 Jun 2025 17:36:57 +0200 Subject: [PATCH 33/75] fix: debounce to not act as a delay on first call --- .../inspect/filesystem/events-manager.ts | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts index 9ef1d738b..6dacdebf5 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts @@ -16,8 +16,7 @@ export class FilesystemEventManager { private store: FilesystemStore private sandbox: Sandbox - // ms delay used when batching rapid load requests - private static readonly LOAD_DEBOUNCE_MS = 300 + private static readonly LOAD_DEBOUNCE_MS = 250 private loadTimers: Map> = new Map() private pendingLoads: Map< @@ -135,12 +134,6 @@ export class FilesystemEventManager { async loadDirectory(path: string): Promise { const normalizedPath = normalizePath(path) - // if there is already a scheduled load for this path, reset the timer and return its promise - const existingTimer = this.loadTimers.get(normalizedPath) - if (existingTimer) { - clearTimeout(existingTimer) - } - let pending = this.pendingLoads.get(normalizedPath) if (!pending) { let res!: () => void @@ -153,20 +146,34 @@ export class FilesystemEventManager { this.pendingLoads.set(normalizedPath, pending) } - const timer = setTimeout(async () => { - // once the timer fires, perform the actual load then resolve/reject all waiters - this.loadTimers.delete(normalizedPath) - try { - await this.loadDirectoryImmediate(normalizedPath) - pending!.resolve() - } catch (err) { - pending!.reject(err) - } finally { - this.pendingLoads.delete(normalizedPath) - } - }, FilesystemEventManager.LOAD_DEBOUNCE_MS) + const state = this.store.getState() + + const isAlreadyLoading = state.loadingPaths.has(normalizedPath) + const existingTimer = this.loadTimers.get(normalizedPath) + + if (isAlreadyLoading || existingTimer) { + if (existingTimer) clearTimeout(existingTimer) + + const timer = setTimeout(async () => { + this.loadTimers.delete(normalizedPath) + try { + await this.loadDirectoryImmediate(normalizedPath) + pending!.resolve() + } catch (err) { + pending!.reject(err) + } finally { + this.pendingLoads.delete(normalizedPath) + } + }, FilesystemEventManager.LOAD_DEBOUNCE_MS) + + this.loadTimers.set(normalizedPath, timer) + return pending.promise + } - this.loadTimers.set(normalizedPath, timer) + void this.loadDirectoryImmediate(normalizedPath) + .then(() => pending!.resolve()) + .catch((err) => pending!.reject(err)) + .finally(() => this.pendingLoads.delete(normalizedPath)) return pending.promise } From 5ed78d06a85d158ab4fceb0034a0a011e0cf029b Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Wed, 25 Jun 2025 23:24:09 +0200 Subject: [PATCH 34/75] chore: clean up sandbox root fetch --- src/server/sandboxes/get-sandbox-root.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index 149a93530..8cc730df0 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -1,4 +1,3 @@ -// src/server/sandboxes/get-sandbox-root.ts import { z } from 'zod' import { authActionClient } from '@/lib/clients/action' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' @@ -22,26 +21,17 @@ export const getSandboxRoot = authActionClient const headers = SUPABASE_AUTH_HEADERS(session.access_token, teamId) - let entries - try { const sandbox = await Sandbox.connect(sandboxId, { domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, headers, - secure: true, }) - const raw = await sandbox.files.list(rootPath) - entries = raw.map((e) => ({ - name: e.name, - path: e.path, - type: e.type, - })) + + return { + entries: await sandbox.files.list(rootPath), + } } catch (err) { logError(ERROR_CODES.E2B_SDK, 'files.list', err) return returnServerError('Failed to list root directory.') } - - return { - entries, - } }) From 46281d98efb1a3a02f8c701ff1b5d9302ebc1eb4 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 11:53:34 +0200 Subject: [PATCH 35/75] chore: refactor events-manager -> sandbox-manager --- .../dashboard/sandbox/inspect/context.tsx | 30 +++++++++---------- .../events-manager.ts => sandbox-manager.ts} | 8 ++--- 2 files changed, 19 insertions(+), 19 deletions(-) rename src/features/dashboard/sandbox/inspect/{filesystem/events-manager.ts => sandbox-manager.ts} (97%) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index f375312b2..85ddc9774 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -11,7 +11,7 @@ import React, { } from 'react' import { createFilesystemStore, type FilesystemStore } from './filesystem/store' import { FilesystemNode, FilesystemOperations } from './filesystem/types' -import { FilesystemEventManager } from './filesystem/events-manager' +import { SandboxManager } from './sandbox-manager' import { getParentPath, normalizePath } from '@/lib/utils/filesystem' import { useSandboxContext } from '../context' import Sandbox, { EntryInfo, FileType } from 'e2b' @@ -23,7 +23,7 @@ import { AUTH_URLS } from '@/configs/urls' interface SandboxInspectContextValue { store: FilesystemStore operations: FilesystemOperations - eventManager: FilesystemEventManager | null + sandboxManager: SandboxManager | null } const SandboxInspectContext = createContext( @@ -45,7 +45,7 @@ export function SandboxInspectProvider({ }: SandboxInspectProviderProps) { const { sandboxInfo } = useSandboxContext() const storeRef = useRef(null) - const eventManagerRef = useRef(null) + const sandboxManagerRef = useRef(null) const operationsRef = useRef(null) const router = useRouter() @@ -68,9 +68,9 @@ export function SandboxInspectProvider({ if (needsNewStore) { // stop previous watcher (if any) - if (eventManagerRef.current) { - eventManagerRef.current.stopWatching() - eventManagerRef.current = null + if (sandboxManagerRef.current) { + sandboxManagerRef.current.stopWatching() + sandboxManagerRef.current = null } storeRef.current = createFilesystemStore(rootPath) @@ -120,7 +120,7 @@ export function SandboxInspectProvider({ const store = storeRef.current operationsRef.current = { loadDirectory: async (path: string) => { - await eventManagerRef.current?.loadDirectory(path) + await sandboxManagerRef.current?.loadDirectory(path) }, selectNode: (path: string) => { store.getState().setSelected(path) @@ -136,11 +136,11 @@ export function SandboxInspectProvider({ state.setExpanded(normalizedPath, newExpandedState) if (newExpandedState && !node.isLoaded) { - await eventManagerRef.current?.loadDirectory(normalizedPath) + await sandboxManagerRef.current?.loadDirectory(normalizedPath) } }, refreshDirectory: async (path: string) => { - await eventManagerRef.current?.refreshDirectory(path) + await sandboxManagerRef.current?.refreshDirectory(path) }, } } @@ -153,9 +153,9 @@ export function SandboxInspectProvider({ const connectSandbox = async () => { if (!storeRef.current) return - // (re)create the event-manager when sandbox / team / root changes - if (eventManagerRef.current) { - eventManagerRef.current.stopWatching() + // (re)create the sandbox-manager when sandbox / team / root changes + if (sandboxManagerRef.current) { + sandboxManagerRef.current.stopWatching() } const { data } = await supabase.auth.getSession() @@ -173,7 +173,7 @@ export function SandboxInspectProvider({ secure: true, }) - eventManagerRef.current = new FilesystemEventManager( + sandboxManagerRef.current = new SandboxManager( storeRef.current, sandbox, rootPath @@ -183,7 +183,7 @@ export function SandboxInspectProvider({ connectSandbox() return () => { - eventManagerRef.current?.stopWatching() + sandboxManagerRef.current?.stopWatching() } }, [sandboxId, teamId, rootPath, router]) @@ -194,7 +194,7 @@ export function SandboxInspectProvider({ const contextValue: SandboxInspectContextValue = { store: storeRef.current, operations: operationsRef.current, - eventManager: eventManagerRef.current, + sandboxManager: sandboxManagerRef.current, } return ( diff --git a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts similarity index 97% rename from src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts rename to src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 6dacdebf5..f95455b32 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -6,11 +6,11 @@ import { type EntryInfo, FilesystemEventType, } from 'e2b' -import type { FilesystemStore } from './store' -import { FilesystemNode } from './types' +import type { FilesystemStore } from './filesystem/store' +import { FilesystemNode } from './filesystem/types' import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' -export class FilesystemEventManager { +export class SandboxManager { private watchHandle?: WatchHandle private readonly rootPath: string private store: FilesystemStore @@ -164,7 +164,7 @@ export class FilesystemEventManager { } finally { this.pendingLoads.delete(normalizedPath) } - }, FilesystemEventManager.LOAD_DEBOUNCE_MS) + }, SandboxManager.LOAD_DEBOUNCE_MS) this.loadTimers.set(normalizedPath, timer) return pending.promise From 3c217a3b6404113ab9d6bfbdf6c4815fca366468 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 12:05:33 +0200 Subject: [PATCH 36/75] chore: clean-up sandbox context --- src/features/dashboard/sandbox/context.tsx | 46 +--------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index 8b0f3ffc1..5fac4f238 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -1,22 +1,10 @@ 'use client' -import React, { - createContext, - useContext, - ReactNode, - useLayoutEffect, - useState, -} from 'react' +import React, { createContext, useContext, ReactNode } from 'react' import { SandboxInfo } from '@/types/api' -interface SandboxState { - secondsLeft: number - isRunning: boolean -} - interface SandboxContextValue { sandboxInfo: SandboxInfo - state: SandboxState } const SandboxContext = createContext(null) @@ -38,42 +26,10 @@ export function SandboxProvider({ children, sandboxInfo, }: SandboxProviderProps) { - const [secondsLeft, setSecondsLeft] = useState(0) - const [isRunning, setIsRunning] = useState(false) - - useLayoutEffect(() => { - const interval = setInterval(() => { - const now = new Date() - const endAt = new Date(sandboxInfo.endAt) - - if (endAt <= now) { - setIsRunning(false) - setSecondsLeft(0) - clearInterval(interval) - } else { - setIsRunning(true) - } - - const diff = endAt.getTime() - now.getTime() - setSecondsLeft(Math.max(0, Math.floor(diff / 1000))) - }, 1000) - - return () => { - if (!interval) return - clearInterval(interval) - } - }, [sandboxInfo.sandboxID, sandboxInfo.endAt]) - - const state = { - secondsLeft, - isRunning, - } - return ( {children} From 078f144032a5edbc874a4b9ad4d4cfa1d53cfeca Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 12:18:29 +0200 Subject: [PATCH 37/75] refactor: sandbox-manager debounce handling --- .../sandbox/inspect/sandbox-manager.ts | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index f95455b32..05ccdccaa 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -18,6 +18,20 @@ export class SandboxManager { private static readonly LOAD_DEBOUNCE_MS = 250 + /** + * Small utility to create a deferred promise (aka Promise with exposed + * resolve/reject). + */ + private static createDeferred() { + let resolve!: (value: T | PromiseLike) => void + let reject!: (reason?: unknown) => void + const promise: Promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } + } + private loadTimers: Map> = new Map() private pendingLoads: Map< string, @@ -118,17 +132,8 @@ export class SandboxManager { private handleRemoveEvent(removedPath: string): void { const state = this.store.getState() - const node = state.getNode(removedPath) - - if (!node) { - console.debug( - `Node '${removedPath}' not found in store, skipping removal` - ) - return - } state.removeNode(removedPath) - console.log(`Successfully removed node '${removedPath}' from store`) } async loadDirectory(path: string): Promise { @@ -136,13 +141,7 @@ export class SandboxManager { let pending = this.pendingLoads.get(normalizedPath) if (!pending) { - let res!: () => void - let rej!: (err: unknown) => void - const promise = new Promise((resolve, reject) => { - res = resolve - rej = reject - }) - pending = { promise, resolve: res, reject: rej } + pending = SandboxManager.createDeferred() this.pendingLoads.set(normalizedPath, pending) } @@ -158,9 +157,9 @@ export class SandboxManager { this.loadTimers.delete(normalizedPath) try { await this.loadDirectoryImmediate(normalizedPath) - pending!.resolve() + pending.resolve() } catch (err) { - pending!.reject(err) + pending.reject(err) } finally { this.pendingLoads.delete(normalizedPath) } @@ -171,8 +170,8 @@ export class SandboxManager { } void this.loadDirectoryImmediate(normalizedPath) - .then(() => pending!.resolve()) - .catch((err) => pending!.reject(err)) + .then(() => pending.resolve()) + .catch((err) => pending.reject(err)) .finally(() => this.pendingLoads.delete(normalizedPath)) return pending.promise From a1f335d640ca31070acfd807403cbba4c4ec6046 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 12:57:03 +0200 Subject: [PATCH 38/75] feat: file viewing / content loading state --- .../dashboard/sandbox/inspect/context.tsx | 5 ++- .../sandbox/inspect/filesystem/store.ts | 38 ++++++++++++++++++- .../sandbox/inspect/filesystem/types.ts | 1 + .../sandbox/inspect/sandbox-manager.ts | 20 ++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 85ddc9774..ecc26285a 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -23,7 +23,6 @@ import { AUTH_URLS } from '@/configs/urls' interface SandboxInspectContextValue { store: FilesystemStore operations: FilesystemOperations - sandboxManager: SandboxManager | null } const SandboxInspectContext = createContext( @@ -142,6 +141,9 @@ export function SandboxInspectProvider({ refreshDirectory: async (path: string) => { await sandboxManagerRef.current?.refreshDirectory(path) }, + readFile: async (path: string) => { + await sandboxManagerRef.current?.readFile(path) + }, } } } @@ -194,7 +196,6 @@ export function SandboxInspectProvider({ const contextValue: SandboxInspectContextValue = { store: storeRef.current, operations: operationsRef.current, - sandboxManager: sandboxManagerRef.current, } return ( diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index b3bc74b93..bffabfce5 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -17,6 +17,12 @@ interface FilesystemStatics { rootPath: string } +interface FileContentState { + content?: string + type?: string + isLoading: boolean +} + // mutable state export interface FilesystemState { nodes: Map @@ -24,6 +30,7 @@ export interface FilesystemState { loadingPaths: Set errorPaths: Map sortingDirection: 'asc' | 'desc' + fileContents: Map } // mutations/actions that modify state @@ -35,6 +42,8 @@ export interface FilesystemMutations { setSelected: (path: string) => void setLoading: (path: string, loading: boolean) => void setError: (path: string, error?: string) => void + setFileContent: (path: string, updates: Partial) => void + resetFileContent: (path: string) => void reset: () => void } @@ -45,6 +54,7 @@ export interface FilesystemComputed { isExpanded: (path: string) => boolean isSelected: (path: string) => boolean hasChildren: (path: string) => boolean + getFileContent: (path: string) => FileContentState | undefined } // combined store type @@ -53,7 +63,7 @@ export type FilesystemStoreData = FilesystemStatics & FilesystemMutations & FilesystemComputed -// to retain reference-stable arrays of children per directory path +// to retain reference-stable arrays of children per directory path const childrenCache: Map = new Map() @@ -84,6 +94,7 @@ export const createFilesystemStore = (rootPath: string) => loadingPaths: new Set(), errorPaths: new Map(), sortingDirection: 'asc' as 'asc' | 'desc', + fileContents: new Map(), addNodes: (parentPath: string, nodes: FilesystemNode[]) => { const normalizedParentPath = normalizePath(parentPath) @@ -260,12 +271,32 @@ export const createFilesystemStore = (rootPath: string) => }) }, + setFileContent: (path: string, updates: Partial) => { + const normalizedPath = normalizePath(path) + set((state: FilesystemState) => { + const existing = + state.fileContents.get(normalizedPath) ?? ({} as FileContentState) + state.fileContents.set(normalizedPath, { + ...existing, + ...updates, + }) + }) + }, + + resetFileContent: (path: string) => { + const normalizedPath = normalizePath(path) + set((state: FilesystemState) => { + state.fileContents.delete(normalizedPath) + }) + }, + reset: () => { set((state: FilesystemState) => { state.nodes.clear() state.selectedPath = undefined state.loadingPaths.clear() state.errorPaths.clear() + state.fileContents.clear() }) }, @@ -320,6 +351,11 @@ export const createFilesystemStore = (rootPath: string) => return node.children.length > 0 }, + + getFileContent: (path: string) => { + const normalizedPath = normalizePath(path) + return get().fileContents.get(normalizedPath) + }, })) ) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index 4f5365a8e..b78e0febc 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -26,4 +26,5 @@ export interface FilesystemOperations { selectNode: (path: string) => void toggleDirectory: (path: string) => Promise refreshDirectory: (path: string) => Promise + readFile: (path: string) => Promise } diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 05ccdccaa..64eb55391 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -247,4 +247,24 @@ export class SandboxManager { await this.loadDirectory(normalizedPath) } + + async readFile(path: string): Promise { + const normalizedPath = normalizePath(path) + + const state = this.store.getState() + + state.setFileContent(normalizedPath, { isLoading: true }) + + try { + const content = await this.sandbox.files.read(normalizedPath) + + state.setFileContent(normalizedPath, { + content, + isLoading: false, + }) + } catch (err) { + console.error(`Failed to read file ${normalizedPath}:`, err) + state.setFileContent(normalizedPath, { isLoading: false }) + } + } } From 6b91c0c5ebad672e6c71a1990ad249543ee9ef0e Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 13:13:52 +0200 Subject: [PATCH 39/75] refactor: file node selection state + refresh file after write event --- src/features/dashboard/sandbox/inspect/context.tsx | 13 +++++++++---- .../dashboard/sandbox/inspect/filesystem/store.ts | 7 ++++++- .../dashboard/sandbox/inspect/filesystem/types.ts | 3 +-- .../dashboard/sandbox/inspect/sandbox-manager.ts | 5 +++++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index ecc26285a..cc4c943cc 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -121,7 +121,15 @@ export function SandboxInspectProvider({ loadDirectory: async (path: string) => { await sandboxManagerRef.current?.loadDirectory(path) }, - selectNode: (path: string) => { + selectNode: async (path: string) => { + const node = store.getState().getNode(path) + + if (!node) return + + if (node.type === FileType.FILE) { + void sandboxManagerRef.current?.readFile(path) + } + store.getState().setSelected(path) }, toggleDirectory: async (path: string) => { @@ -141,9 +149,6 @@ export function SandboxInspectProvider({ refreshDirectory: async (path: string) => { await sandboxManagerRef.current?.refreshDirectory(path) }, - readFile: async (path: string) => { - await sandboxManagerRef.current?.readFile(path) - }, } } } diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index bffabfce5..e479dbf9f 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -155,7 +155,7 @@ export const createFilesystemStore = (rootPath: string) => removeNode: (path: string) => { const normalizedPath = normalizePath(path) - set((state: FilesystemState) => { + set((state: FilesystemStoreData) => { const node = state.nodes.get(normalizedPath) if (!node) return @@ -182,6 +182,11 @@ export const createFilesystemStore = (rootPath: string) => if (state.selectedPath === pathToRemove) { state.selectedPath = undefined } + + const nodeToRemove = state.nodes.get(pathToRemove) + if (nodeToRemove?.type === FileType.FILE) { + state.resetFileContent(pathToRemove) + } } }) }, diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index b78e0febc..3f7d5b814 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -23,8 +23,7 @@ export type FilesystemNode = FilesystemDir | FilesystemFile export interface FilesystemOperations { loadDirectory: (path: string) => Promise - selectNode: (path: string) => void toggleDirectory: (path: string) => Promise refreshDirectory: (path: string) => Promise - readFile: (path: string) => Promise + selectNode: (path: string) => void } diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 64eb55391..1e5d034d4 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -120,6 +120,11 @@ export class SandboxManager { break case FilesystemEventType.WRITE: + if (state.getNode(normalizedPath)?.type === FileType.FILE) { + void this.readFile(normalizedPath) + } + break + case FilesystemEventType.CHMOD: console.debug(`Ignoring ${type} event for '${normalizedPath}'`) break From a50f99a96864a53f4c94f52546a7b231dc810ac8 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 14:30:52 +0200 Subject: [PATCH 40/75] refactor: selection / state --- .../dashboard/sandbox/inspect/context.tsx | 3 + .../sandbox/inspect/filesystem/store.ts | 11 +--- .../sandbox/inspect/filesystem/types.ts | 3 + .../sandbox/inspect/hooks/use-content.ts | 18 +++++ .../sandbox/inspect/sandbox-manager.ts | 66 ++++++++++--------- 5 files changed, 61 insertions(+), 40 deletions(-) create mode 100644 src/features/dashboard/sandbox/inspect/hooks/use-content.ts diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index cc4c943cc..de9a886e4 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -149,6 +149,9 @@ export function SandboxInspectProvider({ refreshDirectory: async (path: string) => { await sandboxManagerRef.current?.refreshDirectory(path) }, + refreshFile: async (path: string) => { + await sandboxManagerRef.current?.readFile(path) + }, } } } diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index e479dbf9f..868d9777e 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -19,8 +19,6 @@ interface FilesystemStatics { interface FileContentState { content?: string - type?: string - isLoading: boolean } // mutable state @@ -182,11 +180,6 @@ export const createFilesystemStore = (rootPath: string) => if (state.selectedPath === pathToRemove) { state.selectedPath = undefined } - - const nodeToRemove = state.nodes.get(pathToRemove) - if (nodeToRemove?.type === FileType.FILE) { - state.resetFileContent(pathToRemove) - } } }) }, @@ -252,7 +245,7 @@ export const createFilesystemStore = (rootPath: string) => const node = state.nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return + if (!node) return node.isLoading = loading }) @@ -270,7 +263,7 @@ export const createFilesystemStore = (rootPath: string) => const node = state.nodes.get(normalizedPath) - if (!node || node.type === FileType.FILE) return + if (!node) return node.error = error }) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index 3f7d5b814..ed255606f 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -16,7 +16,9 @@ interface FilesystemFile { type: FileType.FILE name: string path: string + error?: string isSelected?: boolean + isLoading?: boolean } export type FilesystemNode = FilesystemDir | FilesystemFile @@ -26,4 +28,5 @@ export interface FilesystemOperations { toggleDirectory: (path: string) => Promise refreshDirectory: (path: string) => Promise selectNode: (path: string) => void + refreshFile: (path: string) => Promise } diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-content.ts b/src/features/dashboard/sandbox/inspect/hooks/use-content.ts new file mode 100644 index 000000000..999d177dd --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-content.ts @@ -0,0 +1,18 @@ +import { useStore } from 'zustand/react' +import { useSandboxInspectContext } from '../context' +import { useCallback } from 'react' + +export function useContent(path: string) { + const { store, operations } = useSandboxInspectContext() + + const content = useStore(store, (state) => state.getFileContent(path)) + + const refresh = useCallback(async () => { + await operations.refreshFile(path) + }, [path, operations]) + + return { + content, + refresh, + } +} diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 1e5d034d4..00e836a0a 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -88,45 +88,26 @@ export class SandboxManager { switch (type) { case FilesystemEventType.CREATE: case FilesystemEventType.RENAME: - if ( - !parentNode || - parentNode.type !== FileType.DIR || - !parentNode.isLoaded - ) { - console.debug( - `Skip refresh for '${normalizedPath}' because parent directory '${parentDir}' does not exist in store` - ) - return + const node = state.getNode(normalizedPath) + + if (node?.type === FileType.FILE && parentNode?.type === FileType.DIR) { + void this.refreshDirectory(parentDir) + break } - console.log( - `Filesystem ${type} event for '${normalizedPath}', refreshing parent '${parentDir}'` - ) - void this.refreshDirectory(parentDir) + void this.loadDirectory(normalizedPath) + break case FilesystemEventType.REMOVE: - if (!state.getNode(normalizedPath)) { - console.debug( - `Skip remove for '${normalizedPath}' because node does not exist in store` - ) - return - } - - console.log( - `Filesystem REMOVE event for '${normalizedPath}', removing node from store` - ) this.handleRemoveEvent(normalizedPath) break case FilesystemEventType.WRITE: - if (state.getNode(normalizedPath)?.type === FileType.FILE) { - void this.readFile(normalizedPath) - } + void this.readFile(normalizedPath) break case FilesystemEventType.CHMOD: - console.debug(`Ignoring ${type} event for '${normalizedPath}'`) break default: @@ -137,13 +118,26 @@ export class SandboxManager { private handleRemoveEvent(removedPath: string): void { const state = this.store.getState() + const node = state.getNode(removedPath) + + if (!node) return state.removeNode(removedPath) + + if (node?.type === FileType.FILE) { + state.resetFileContent(removedPath) + } } async loadDirectory(path: string): Promise { const normalizedPath = normalizePath(path) + const node = this.store.getState().getNode(normalizedPath) + + if (node?.type === FileType.FILE) { + return + } + let pending = this.pendingLoads.get(normalizedPath) if (!pending) { pending = SandboxManager.createDeferred() @@ -247,6 +241,9 @@ export class SandboxManager { const normalizedPath = normalizePath(path) const state = this.store.getState() + const node = state.getNode(normalizedPath) + if (!node || node.type !== FileType.DIR) return + // mark directory as stale but keep existing children until fresh data arrives state.updateNode(normalizedPath, { isLoaded: false }) @@ -255,21 +252,28 @@ export class SandboxManager { async readFile(path: string): Promise { const normalizedPath = normalizePath(path) - const state = this.store.getState() + const node = state.getNode(normalizedPath) - state.setFileContent(normalizedPath, { isLoading: true }) + if (!node || node.type !== FileType.FILE) return try { + state.setLoading(normalizedPath, true) + const content = await this.sandbox.files.read(normalizedPath) state.setFileContent(normalizedPath, { content, - isLoading: false, }) } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to read file' + console.error(`Failed to read file ${normalizedPath}:`, err) - state.setFileContent(normalizedPath, { isLoading: false }) + + state.setError(normalizedPath, errorMessage) + } finally { + state.setLoading(normalizedPath, false) } } } From 8d098f444c29e9cfd9175d248e55afb98d5dd2a6 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 14:58:28 +0200 Subject: [PATCH 41/75] feat: add content / file hooks --- .../sandbox/inspect/hooks/use-content.ts | 4 +- .../sandbox/inspect/hooks/use-file.tsx | 55 +++++++++++++++++++ .../sandbox/inspect/hooks/use-filesystem.ts | 12 ---- 3 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 src/features/dashboard/sandbox/inspect/hooks/use-file.tsx delete mode 100644 src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-content.ts b/src/features/dashboard/sandbox/inspect/hooks/use-content.ts index 999d177dd..27c444883 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-content.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-content.ts @@ -5,14 +5,14 @@ import { useCallback } from 'react' export function useContent(path: string) { const { store, operations } = useSandboxInspectContext() - const content = useStore(store, (state) => state.getFileContent(path)) + const contentState = useStore(store, (state) => state.getFileContent(path)) const refresh = useCallback(async () => { await operations.refreshFile(path) }, [path, operations]) return { - content, + ...contentState, refresh, } } diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx new file mode 100644 index 000000000..6d3e8c1a8 --- /dev/null +++ b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx @@ -0,0 +1,55 @@ +'use client' + +import { useMemo } from 'react' +import { useSandboxInspectContext } from '../context' +import { useStore } from 'zustand' + +/** + * Hook for accessing file state (loading, error) + */ +export function useFileState(path: string) { + const { store } = useSandboxInspectContext() + + const isLoading = useStore(store, (state) => state.loadingPaths.has(path)) + const hasError = useStore(store, (state) => state.errorPaths.has(path)) + const error = useStore(store, (state) => state.errorPaths.get(path)) + const isSelected = useStore(store, (state) => state.isSelected(path)) + + return useMemo( + () => ({ + isLoading, + hasError, + error, + isSelected, + }), + [isLoading, hasError, error, isSelected] + ) +} + +/** + * Hook for file operations + */ +export function useFileOperations(path: string) { + const { operations } = useSandboxInspectContext() + + return useMemo( + () => ({ + refresh: () => operations.refreshFile(path), + select: () => operations.selectNode(path), + }), + [operations, path] + ) +} + +/** + * Combined hook for file data and operations + */ +export function useFile(path: string) { + const state = useFileState(path) + const ops = useFileOperations(path) + + return { + ...state, + ...ops, + } +} diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts b/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts deleted file mode 100644 index 3718a80c0..000000000 --- a/src/features/dashboard/sandbox/inspect/hooks/use-filesystem.ts +++ /dev/null @@ -1,12 +0,0 @@ -'use client' - -import { useSandboxInspectContext } from '../context' -import type { FilesystemOperations } from '../filesystem/types' - -/** - * Main hook for accessing filesystem operations - */ -export function useFilesystem(): FilesystemOperations { - const { operations } = useSandboxInspectContext() - return operations -} From 446cffb4d53c2114ac9c02722e4a5f36381d2626 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 15:02:50 +0200 Subject: [PATCH 42/75] refactor: await file read after selection --- src/features/dashboard/sandbox/inspect/context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index de9a886e4..f4d0c0eb4 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -127,7 +127,7 @@ export function SandboxInspectProvider({ if (!node) return if (node.type === FileType.FILE) { - void sandboxManagerRef.current?.readFile(path) + await sandboxManagerRef.current?.readFile(path) } store.getState().setSelected(path) From 345074ef28726f9c220ebd7ceb97861fc36f80cc Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 17:05:49 +0200 Subject: [PATCH 43/75] refactor: ensure file / node states are using separate store props --- .../sandbox/inspect/filesystem/store.ts | 27 +------------------ .../sandbox/inspect/filesystem/types.ts | 6 ----- .../sandbox/inspect/hooks/use-file.tsx | 3 +++ 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 868d9777e..77a169646 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -216,19 +216,6 @@ export const createFilesystemStore = (rootPath: string) => const normalizedPath = normalizePath(path) set((state: FilesystemState) => { - if (state.selectedPath) { - const prevNode = state.nodes.get(state.selectedPath) - - if (!prevNode) return - - prevNode.isSelected = false - } - - const node = state.nodes.get(normalizedPath) - - if (!node) return - - node.isSelected = true state.selectedPath = normalizedPath }) }, @@ -242,12 +229,6 @@ export const createFilesystemStore = (rootPath: string) => } else { state.loadingPaths.delete(normalizedPath) } - - const node = state.nodes.get(normalizedPath) - - if (!node) return - - node.isLoading = loading }) }, @@ -260,12 +241,6 @@ export const createFilesystemStore = (rootPath: string) => } else { state.errorPaths.delete(normalizedPath) } - - const node = state.nodes.get(normalizedPath) - - if (!node) return - - node.error = error }) }, @@ -338,7 +313,7 @@ export const createFilesystemStore = (rootPath: string) => if (!node) return false - return !!node.isSelected + return get().selectedPath === normalizedPath }, hasChildren: (path: string) => { diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index ed255606f..f2ac0a6da 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -7,18 +7,12 @@ interface FilesystemDir { children: string[] // paths of children isExpanded?: boolean isLoaded?: boolean - isSelected?: boolean - isLoading?: boolean - error?: string } interface FilesystemFile { type: FileType.FILE name: string path: string - error?: string - isSelected?: boolean - isLoading?: boolean } export type FilesystemNode = FilesystemDir | FilesystemFile diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx index 6d3e8c1a8..0c2470aa1 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx +++ b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx @@ -3,6 +3,7 @@ import { useMemo } from 'react' import { useSandboxInspectContext } from '../context' import { useStore } from 'zustand' +import { useFilesystemNode } from './use-node' /** * Hook for accessing file state (loading, error) @@ -45,10 +46,12 @@ export function useFileOperations(path: string) { * Combined hook for file data and operations */ export function useFile(path: string) { + const node = useFilesystemNode(path) const state = useFileState(path) const ops = useFileOperations(path) return { + ...node, ...state, ...ops, } From e550d5f24ff05fccc9ff621e243836f7893cf765 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 30 Jun 2025 17:13:01 +0200 Subject: [PATCH 44/75] fix: useFile return --- src/features/dashboard/sandbox/inspect/hooks/use-file.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx index 0c2470aa1..8dd9f464b 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx +++ b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx @@ -4,6 +4,7 @@ import { useMemo } from 'react' import { useSandboxInspectContext } from '../context' import { useStore } from 'zustand' import { useFilesystemNode } from './use-node' +import { FileType } from 'e2b' /** * Hook for accessing file state (loading, error) @@ -50,6 +51,10 @@ export function useFile(path: string) { const state = useFileState(path) const ops = useFileOperations(path) + if (!node || node.type !== FileType.FILE) { + throw new Error(`Node at path ${path} is not a file`) + } + return { ...node, ...state, From f6e5b70fb7e12b61d9c3dab2405b313a3e1685d2 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 1 Jul 2025 16:56:51 +0200 Subject: [PATCH 45/75] refactor: handle different file content encodings --- .../sandbox/inspect/filesystem/store.ts | 24 +++++++++++--- .../sandbox/inspect/hooks/use-content.ts | 2 +- .../sandbox/inspect/sandbox-manager.ts | 21 ++++++------ src/lib/utils/inspect.ts | 33 +++++++++++++++++++ 4 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 src/lib/utils/inspect.ts diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 77a169646..23503db50 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -17,10 +17,26 @@ interface FilesystemStatics { rootPath: string } -interface FileContentState { - content?: string +interface Utf8FileContentState { + content: string + encoding: 'utf-8' } +interface BinaryFileContentState { + dataUri: string + encoding: 'binary' +} + +interface ImageFileContentState { + dataUri: string + encoding: 'image' +} + +export type FileContentState = + | Utf8FileContentState + | BinaryFileContentState + | ImageFileContentState + // mutable state export interface FilesystemState { nodes: Map @@ -40,7 +56,7 @@ export interface FilesystemMutations { setSelected: (path: string) => void setLoading: (path: string, loading: boolean) => void setError: (path: string, error?: string) => void - setFileContent: (path: string, updates: Partial) => void + setFileContent: (path: string, updates: FileContentState) => void resetFileContent: (path: string) => void reset: () => void } @@ -244,7 +260,7 @@ export const createFilesystemStore = (rootPath: string) => }) }, - setFileContent: (path: string, updates: Partial) => { + setFileContent: (path, updates) => { const normalizedPath = normalizePath(path) set((state: FilesystemState) => { const existing = diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-content.ts b/src/features/dashboard/sandbox/inspect/hooks/use-content.ts index 27c444883..b462f7781 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-content.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-content.ts @@ -12,7 +12,7 @@ export function useContent(path: string) { }, [path, operations]) return { - ...contentState, + state: contentState, refresh, } } diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 00e836a0a..49cfdd13d 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -9,6 +9,7 @@ import { import type { FilesystemStore } from './filesystem/store' import { FilesystemNode } from './filesystem/types' import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' +import { determineFileContentState } from '@/lib/utils/inspect' export class SandboxManager { private watchHandle?: WatchHandle @@ -88,9 +89,7 @@ export class SandboxManager { switch (type) { case FilesystemEventType.CREATE: case FilesystemEventType.RENAME: - const node = state.getNode(normalizedPath) - - if (node?.type === FileType.FILE && parentNode?.type === FileType.DIR) { + if (parentNode?.type === FileType.DIR) { void this.refreshDirectory(parentDir) break } @@ -260,18 +259,18 @@ export class SandboxManager { try { state.setLoading(normalizedPath, true) - const content = await this.sandbox.files.read(normalizedPath) - - state.setFileContent(normalizedPath, { - content, + const blob = await this.sandbox.files.read(normalizedPath, { + format: 'blob', + requestTimeoutMs: 30_000, }) - } catch (err) { - const errorMessage = - err instanceof Error ? err.message : 'Failed to read file' + const contentState = await determineFileContentState(blob) + + state.setFileContent(normalizedPath, contentState) + } catch (err) { console.error(`Failed to read file ${normalizedPath}:`, err) - state.setError(normalizedPath, errorMessage) + state.setError(normalizedPath, 'Failed to read file') } finally { state.setLoading(normalizedPath, false) } diff --git a/src/lib/utils/inspect.ts b/src/lib/utils/inspect.ts new file mode 100644 index 000000000..bab016752 --- /dev/null +++ b/src/lib/utils/inspect.ts @@ -0,0 +1,33 @@ +import { FileContentState } from '@/features/dashboard/sandbox/inspect/filesystem/store' + +export type FileEncoding = 'utf-8' | 'binary' | 'image' + +export async function determineFileContentState( + blob: Blob +): Promise { + const mimeType = blob.type ?? '' + + if (mimeType.startsWith('image/')) { + const dataUri = await new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsDataURL(blob) + }) + + return { encoding: 'image', dataUri } + } + + const buffer = await blob.arrayBuffer() + const data = new Uint8Array(buffer) + + try { + const content = new TextDecoder('utf-8', { fatal: true }).decode(data) + return { encoding: 'utf-8', content } + } catch { + return { + encoding: 'binary', + dataUri: `data:application/octet-stream;base64,${btoa(String.fromCharCode(...data))}`, + } + } +} From c2c966216f96f0db5e05b09a3ca344b0c37beb77 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 1 Jul 2025 18:14:08 +0200 Subject: [PATCH 46/75] fix: stable reference children caching --- .../sandbox/inspect/filesystem/store.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 23503db50..7b31a4b6b 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -77,7 +77,10 @@ export type FilesystemStoreData = FilesystemStatics & FilesystemMutations & FilesystemComputed -// to retain reference-stable arrays of children per directory path +// Retain reference-stable arrays of children per directory path. A cached array +// is only reused while the underlying `children` array reference on the node +// stays the same; any mutation that replaces `children` with a new array +// automatically invalidates the cache. const childrenCache: Map = new Map() @@ -136,9 +139,9 @@ export const createFilesystemStore = (rootPath: string) => return } - if (!parentNode.children) { - parentNode.children = [] - } + const existingChildren = parentNode.children ?? [] + + const childrenSet = new Set(existingChildren) for (const node of nodes) { const normalizedPath = normalizePath(node.path) @@ -148,21 +151,23 @@ export const createFilesystemStore = (rootPath: string) => path: normalizedPath, }) - if ( - normalizedPath !== normalizedParentPath && - !parentNode.children.includes(normalizedPath) - ) { - parentNode.children.push(normalizedPath) + if (normalizedPath !== normalizedParentPath) { + childrenSet.add(normalizedPath) } } - parentNode.children.sort((a: string, b: string) => + const newChildren = Array.from(childrenSet) + newChildren.sort((a: string, b: string) => compareFilesystemNodes( state.nodes.get(a), state.nodes.get(b), state.sortingDirection ) ) + + parentNode.children = newChildren + + childrenCache.delete(normalizedParentPath) }) }, @@ -179,6 +184,8 @@ export const createFilesystemStore = (rootPath: string) => parentNode.children = parentNode.children.filter( (childPath: string) => childPath !== normalizedPath ) + + childrenCache.delete(parentPath) } const toRemove = [normalizedPath] From bfa119b9755c37b4173079313ad3e385c5ea9459 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 1 Jul 2025 18:25:36 +0200 Subject: [PATCH 47/75] refactor: filesystem store --- .../sandbox/inspect/filesystem/store.ts | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 7b31a4b6b..79abc5a88 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -7,6 +7,7 @@ import { normalizePath, getParentPath, isChildPath, + getBasename, } from '@/lib/utils/filesystem' import { FilesystemNode } from './types' import { FileType } from 'e2b' @@ -120,10 +121,8 @@ export const createFilesystemStore = (rootPath: string) => let parentNode = state.nodes.get(normalizedParentPath) if (!parentNode) { - const parentName = - normalizedParentPath === '/' - ? '/' - : normalizedParentPath.split('/').pop() || '' + const parentName = getBasename(normalizedParentPath) + parentNode = { name: parentName, path: normalizedParentPath, @@ -135,13 +134,10 @@ export const createFilesystemStore = (rootPath: string) => } if (parentNode.type === FileType.FILE) { - console.error('Parent node is a file', parentNode) - return + throw new Error('Parent node is a file') } - const existingChildren = parentNode.children ?? [] - - const childrenSet = new Set(existingChildren) + const childrenSet = new Set(parentNode.children) for (const node of nodes) { const normalizedPath = normalizePath(node.path) @@ -188,20 +184,18 @@ export const createFilesystemStore = (rootPath: string) => childrenCache.delete(parentPath) } - const toRemove = [normalizedPath] for (const [nodePath] of state.nodes) { - if (isChildPath(normalizedPath, nodePath)) { - toRemove.push(nodePath) - } - } - - for (const pathToRemove of toRemove) { - state.nodes.delete(pathToRemove) - state.loadingPaths.delete(pathToRemove) - state.errorPaths.delete(pathToRemove) - - if (state.selectedPath === pathToRemove) { - state.selectedPath = undefined + if ( + nodePath === normalizedPath || + isChildPath(normalizedPath, nodePath) + ) { + state.nodes.delete(nodePath) + state.loadingPaths.delete(nodePath) + state.errorPaths.delete(nodePath) + + if (state.selectedPath === nodePath) { + state.selectedPath = undefined + } } } }) @@ -270,12 +264,7 @@ export const createFilesystemStore = (rootPath: string) => setFileContent: (path, updates) => { const normalizedPath = normalizePath(path) set((state: FilesystemState) => { - const existing = - state.fileContents.get(normalizedPath) ?? ({} as FileContentState) - state.fileContents.set(normalizedPath, { - ...existing, - ...updates, - }) + state.fileContents.set(normalizedPath, updates) }) }, From 5c26e97378977c4932b5c766c15bc3561830f69e Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Wed, 2 Jul 2025 13:23:12 +0200 Subject: [PATCH 48/75] feat: add resetSelected operation and update file selection logic --- src/features/dashboard/sandbox/inspect/context.tsx | 6 +++++- .../dashboard/sandbox/inspect/filesystem/types.ts | 1 + .../dashboard/sandbox/inspect/hooks/use-file.tsx | 13 ++++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index f4d0c0eb4..2ff500273 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -7,7 +7,6 @@ import React, { ReactNode, useLayoutEffect, useMemo, - useState, } from 'react' import { createFilesystemStore, type FilesystemStore } from './filesystem/store' import { FilesystemNode, FilesystemOperations } from './filesystem/types' @@ -132,6 +131,11 @@ export function SandboxInspectProvider({ store.getState().setSelected(path) }, + resetSelected: () => { + store.setState((state) => { + state.selectedPath = undefined + }) + }, toggleDirectory: async (path: string) => { const normalizedPath = normalizePath(path) const state = store.getState() diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index f2ac0a6da..b63610c5f 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -22,5 +22,6 @@ export interface FilesystemOperations { toggleDirectory: (path: string) => Promise refreshDirectory: (path: string) => Promise selectNode: (path: string) => void + resetSelected: () => void refreshFile: (path: string) => Promise } diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx index 8dd9f464b..185baab57 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx +++ b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react' import { useSandboxInspectContext } from '../context' import { useStore } from 'zustand' -import { useFilesystemNode } from './use-node' +import { useFilesystemNode, useSelectedPath } from './use-node' import { FileType } from 'e2b' /** @@ -33,13 +33,20 @@ export function useFileState(path: string) { */ export function useFileOperations(path: string) { const { operations } = useSandboxInspectContext() + const selectedPath = useSelectedPath() return useMemo( () => ({ refresh: () => operations.refreshFile(path), - select: () => operations.selectNode(path), + toggle: () => { + if (selectedPath === path) { + operations.resetSelected() + } else { + operations.selectNode(path) + } + }, }), - [operations, path] + [operations, path, selectedPath] ) } From 25162d80847364777d177a11c7e98af87c294da4 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Wed, 2 Jul 2025 13:32:26 +0200 Subject: [PATCH 49/75] chore: move file content state logic to filesystem utility and remove inspect module --- src/lib/utils/filesystem.ts | 34 ++++++++++++++++++++++++++++++++++ src/lib/utils/inspect.ts | 33 --------------------------------- 2 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 src/lib/utils/inspect.ts diff --git a/src/lib/utils/filesystem.ts b/src/lib/utils/filesystem.ts index 7936ef962..352ad9c2d 100644 --- a/src/lib/utils/filesystem.ts +++ b/src/lib/utils/filesystem.ts @@ -1,3 +1,5 @@ +import { FileContentState } from '@/features/dashboard/sandbox/inspect/filesystem/store' + /** * Normalize a path by removing duplicate slashes and resolving . and .. segments */ @@ -103,3 +105,35 @@ export function getPathDepth(path: string): number { export function isRootPath(path: string): boolean { return normalizePath(path) === '/' } + +export type FileEncoding = 'utf-8' | 'binary' | 'image' + +export async function determineFileContentState( + blob: Blob +): Promise { + const mimeType = blob.type ?? '' + + if (mimeType.startsWith('image/')) { + const dataUri = await new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsDataURL(blob) + }) + + return { encoding: 'image', dataUri } + } + + const buffer = await blob.arrayBuffer() + const data = new Uint8Array(buffer) + + try { + const content = new TextDecoder('utf-8', { fatal: true }).decode(data) + return { encoding: 'utf-8', content } + } catch { + return { + encoding: 'binary', + dataUri: `data:application/octet-stream;base64,${btoa(String.fromCharCode(...data))}`, + } + } +} diff --git a/src/lib/utils/inspect.ts b/src/lib/utils/inspect.ts deleted file mode 100644 index bab016752..000000000 --- a/src/lib/utils/inspect.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { FileContentState } from '@/features/dashboard/sandbox/inspect/filesystem/store' - -export type FileEncoding = 'utf-8' | 'binary' | 'image' - -export async function determineFileContentState( - blob: Blob -): Promise { - const mimeType = blob.type ?? '' - - if (mimeType.startsWith('image/')) { - const dataUri = await new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onloadend = () => resolve(reader.result as string) - reader.onerror = () => reject(reader.error) - reader.readAsDataURL(blob) - }) - - return { encoding: 'image', dataUri } - } - - const buffer = await blob.arrayBuffer() - const data = new Uint8Array(buffer) - - try { - const content = new TextDecoder('utf-8', { fatal: true }).decode(data) - return { encoding: 'utf-8', content } - } catch { - return { - encoding: 'binary', - dataUri: `data:application/octet-stream;base64,${btoa(String.fromCharCode(...data))}`, - } - } -} From 2d67890506765727438ee715498306a0c9a4a089 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Wed, 2 Jul 2025 15:38:15 +0200 Subject: [PATCH 50/75] fix: update import path for determineFileContentState to filesystem utility --- src/features/dashboard/sandbox/inspect/sandbox-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 49cfdd13d..f3a6bda26 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -9,7 +9,7 @@ import { import type { FilesystemStore } from './filesystem/store' import { FilesystemNode } from './filesystem/types' import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' -import { determineFileContentState } from '@/lib/utils/inspect' +import { determineFileContentState } from '@/lib/utils/filesystem' export class SandboxManager { private watchHandle?: WatchHandle From ffa1c1fc2098e833cd9a0ce114eeb37a9a6f6147 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 15:01:23 +0200 Subject: [PATCH 51/75] feat: implement debounced file read logic in SandboxManager --- .../sandbox/inspect/sandbox-manager.ts | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index f3a6bda26..c171a58a7 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -18,6 +18,7 @@ export class SandboxManager { private sandbox: Sandbox private static readonly LOAD_DEBOUNCE_MS = 250 + private static readonly READ_DEBOUNCE_MS = 250 /** * Small utility to create a deferred promise (aka Promise with exposed @@ -43,6 +44,16 @@ export class SandboxManager { } > = new Map() + private readTimers: Map> = new Map() + private pendingReads: Map< + string, + { + promise: Promise + resolve: () => void + reject: (err: unknown) => void + } + > = new Map() + constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string) { this.store = store this.sandbox = sandbox @@ -256,14 +267,59 @@ export class SandboxManager { if (!node || node.type !== FileType.FILE) return + let pending = this.pendingReads.get(normalizedPath) + if (!pending) { + pending = SandboxManager.createDeferred() + this.pendingReads.set(normalizedPath, pending) + } + + const isAlreadyLoading = state.loadingPaths.has(normalizedPath) + const existingTimer = this.readTimers.get(normalizedPath) + + if (isAlreadyLoading || existingTimer) { + if (existingTimer) clearTimeout(existingTimer) + + const timer = setTimeout(async () => { + this.readTimers.delete(normalizedPath) + try { + await this.readFileImmediate(normalizedPath) + pending.resolve() + } catch (err) { + pending.reject(err) + } finally { + this.pendingReads.delete(normalizedPath) + } + }, SandboxManager.READ_DEBOUNCE_MS) + + this.readTimers.set(normalizedPath, timer) + return pending.promise + } + + void this.readFileImmediate(normalizedPath) + .then(() => pending.resolve()) + .catch((err) => pending.reject(err)) + .finally(() => this.pendingReads.delete(normalizedPath)) + + return pending.promise + } + + private async readFileImmediate(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== FileType.FILE) return + try { state.setLoading(normalizedPath, true) - const blob = await this.sandbox.files.read(normalizedPath, { - format: 'blob', + const bytes = await this.sandbox.files.read(normalizedPath, { + format: 'bytes', requestTimeoutMs: 30_000, }) + const blob = new Blob([bytes]) + const contentState = await determineFileContentState(blob) state.setFileContent(normalizedPath, contentState) From b5e018448a20841ab38c44a6523b09362a939268 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 15:26:27 +0200 Subject: [PATCH 52/75] feat: add file download functionality and improve file content state handling --- .../dashboard/sandbox/inspect/context.tsx | 12 +++++++++- .../sandbox/inspect/filesystem/store.ts | 19 ++++++++-------- .../sandbox/inspect/filesystem/types.ts | 1 + .../sandbox/inspect/hooks/use-file.tsx | 5 +---- .../sandbox/inspect/hooks/use-node.ts | 10 +++++++-- .../sandbox/inspect/sandbox-manager.ts | 22 +++++++++++++++++++ src/lib/utils/filesystem.ts | 11 +++------- 7 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 2ff500273..4500c8ce2 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -156,6 +156,17 @@ export function SandboxInspectProvider({ refreshFile: async (path: string) => { await sandboxManagerRef.current?.readFile(path) }, + downloadFile: async (path: string) => { + const downloadUrl = + await sandboxManagerRef.current?.getDownloadUrl(path) + + if (!downloadUrl) return + + const a = document.createElement('a') + a.href = downloadUrl + a.download = path.split('/').pop() || '' + a.click() + }, } } } @@ -184,7 +195,6 @@ export function SandboxInspectProvider({ headers: { ...SUPABASE_AUTH_HEADERS(data.session?.access_token, teamId), }, - secure: true, }) sandboxManagerRef.current = new SandboxManager( diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 79abc5a88..cde99d37e 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -18,26 +18,27 @@ interface FilesystemStatics { rootPath: string } -interface Utf8FileContentState { - content: string - encoding: 'utf-8' +interface ContentFileContentState { + text: string + type: 'text' } -interface BinaryFileContentState { - dataUri: string - encoding: 'binary' +interface UnreadableFileContentState { + type: 'unreadable' } interface ImageFileContentState { dataUri: string - encoding: 'image' + type: 'image' } export type FileContentState = - | Utf8FileContentState - | BinaryFileContentState + | ContentFileContentState + | UnreadableFileContentState | ImageFileContentState +export type FileContentStateType = FileContentState['type'] + // mutable state export interface FilesystemState { nodes: Map diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index b63610c5f..2e5b757e5 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -24,4 +24,5 @@ export interface FilesystemOperations { selectNode: (path: string) => void resetSelected: () => void refreshFile: (path: string) => Promise + downloadFile: (path: string) => Promise } diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx index 185baab57..460148066 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx +++ b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx @@ -45,6 +45,7 @@ export function useFileOperations(path: string) { operations.selectNode(path) } }, + download: () => operations.downloadFile(path), }), [operations, path, selectedPath] ) @@ -58,10 +59,6 @@ export function useFile(path: string) { const state = useFileState(path) const ops = useFileOperations(path) - if (!node || node.type !== FileType.FILE) { - throw new Error(`Node at path ${path} is not a file`) - } - return { ...node, ...state, diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts index 8baf280bf..cf38ae9bd 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts @@ -8,10 +8,16 @@ import { useStore } from 'zustand' /** * Hook for accessing a specific filesystem node */ -export function useFilesystemNode(path: string): FilesystemNode | undefined { +export function useFilesystemNode(path: string): FilesystemNode { const { store } = useSandboxInspectContext() - return useStore(store, (state) => state.getNode(path)) + const node = useStore(store, (state) => state.getNode(path)) + + if (!node) { + throw new Error(`Node at path ${path} not found`) + } + + return node } /** diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index c171a58a7..403a5dea0 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -327,8 +327,30 @@ export class SandboxManager { console.error(`Failed to read file ${normalizedPath}:`, err) state.setError(normalizedPath, 'Failed to read file') + state.setFileContent(normalizedPath, { type: 'unreadable' }) } finally { state.setLoading(normalizedPath, false) } } + + async getDownloadUrl(path: string): Promise { + const normalizedPath = normalizePath(path) + const state = this.store.getState() + const node = state.getNode(normalizedPath) + + if (!node || node.type !== FileType.FILE) { + console.error( + `Failed to get download URL for file. Invalid node: ${node} ${normalizedPath}` + ) + state.setError(normalizedPath, 'Node is not a directory.') + + return '' + } + + const downloadUrl = await this.sandbox.downloadUrl(normalizedPath) + + console.log('downloadUrl', downloadUrl) + + return downloadUrl + } } diff --git a/src/lib/utils/filesystem.ts b/src/lib/utils/filesystem.ts index 352ad9c2d..7dc3e63f1 100644 --- a/src/lib/utils/filesystem.ts +++ b/src/lib/utils/filesystem.ts @@ -106,8 +106,6 @@ export function isRootPath(path: string): boolean { return normalizePath(path) === '/' } -export type FileEncoding = 'utf-8' | 'binary' | 'image' - export async function determineFileContentState( blob: Blob ): Promise { @@ -121,7 +119,7 @@ export async function determineFileContentState( reader.readAsDataURL(blob) }) - return { encoding: 'image', dataUri } + return { type: 'image', dataUri } } const buffer = await blob.arrayBuffer() @@ -129,11 +127,8 @@ export async function determineFileContentState( try { const content = new TextDecoder('utf-8', { fatal: true }).decode(data) - return { encoding: 'utf-8', content } + return { type: 'text', text: content } } catch { - return { - encoding: 'binary', - dataUri: `data:application/octet-stream;base64,${btoa(String.fromCharCode(...data))}`, - } + return { type: 'unreadable' } } } From 966d09fef2b2c1da1f8bbfdd7eb72e6828221399 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 15:37:22 +0200 Subject: [PATCH 53/75] feat: enhance error handling in SandboxManager with user-friendly messages --- .../sandbox/inspect/sandbox-manager.ts | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 403a5dea0..27e1ec7a9 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -21,17 +21,12 @@ export class SandboxManager { private static readonly READ_DEBOUNCE_MS = 250 /** - * Small utility to create a deferred promise (aka Promise with exposed - * resolve/reject). + * Mapping from substrings found in error messages to user-friendly messages. + * Extend this map whenever new error patterns need custom handling. */ - private static createDeferred() { - let resolve!: (value: T | PromiseLike) => void - let reject!: (reason?: unknown) => void - const promise: Promise = new Promise((res, rej) => { - resolve = res - reject = rej - }) - return { promise, resolve, reject } + private static readonly errorMap: Record = { + 'signal timed out': 'The operation timed out. Please try again later.', + 'user aborted a request': 'The request was cancelled.', } private loadTimers: Map> = new Map() @@ -238,8 +233,10 @@ export class SandboxManager { state.updateNode(normalizedPath, { isLoaded: true }) } catch (error) { - const errorMessage = - error instanceof Error ? error.message : 'Failed to load directory' + const errorMessage = SandboxManager.pipeError( + error, + 'Failed to load directory' + ) state.setError(normalizedPath, errorMessage) console.error(`Failed to load directory ${normalizedPath}:`, error) } finally { @@ -324,9 +321,11 @@ export class SandboxManager { state.setFileContent(normalizedPath, contentState) } catch (err) { + const errorMessage = SandboxManager.pipeError(err, 'Failed to read file') + console.error(`Failed to read file ${normalizedPath}:`, err) - state.setError(normalizedPath, 'Failed to read file') + state.setError(normalizedPath, errorMessage) state.setFileContent(normalizedPath, { type: 'unreadable' }) } finally { state.setLoading(normalizedPath, false) @@ -353,4 +352,42 @@ export class SandboxManager { return downloadUrl } + + /** + * Small utility to create a deferred promise (aka Promise with exposed + * resolve/reject). + */ + private static createDeferred() { + let resolve!: (value: T | PromiseLike) => void + let reject!: (reason?: unknown) => void + const promise: Promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } + } + + /** + * Returns a user-friendly message for a given error. It checks the error's + * message against known substrings in `errorMap` and falls back to the + * supplied default message if no match is found. + */ + private static pipeError(error: unknown, defaultMessage: string): string { + const originalMessage = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : '' + + const lowerOriginal = originalMessage.toLowerCase() + + for (const [search, msg] of Object.entries(SandboxManager.errorMap)) { + if (lowerOriginal.includes(search.toLowerCase())) { + return msg + } + } + + return originalMessage || defaultMessage + } } From 225add9965b3078287a1b52ef9f734a5cf669c13 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 16:05:25 +0200 Subject: [PATCH 54/75] feat: implement loaded state management for filesystem nodes in SandboxInspectProvider and SandboxManager --- .../dashboard/sandbox/inspect/context.tsx | 7 ++++--- .../sandbox/inspect/filesystem/store.ts | 20 +++++++++++++++++++ .../sandbox/inspect/filesystem/types.ts | 1 - .../sandbox/inspect/hooks/use-directory.ts | 5 +---- .../sandbox/inspect/sandbox-manager.ts | 10 +++------- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 4500c8ce2..4ff8ef8b5 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -84,11 +84,12 @@ export function SandboxInspectProvider({ path: normalizedRoot, type: FileType.DIR, isExpanded: true, - isLoaded: true, children: [], }, ]) + state.setLoaded(normalizedRoot, true) + if (seedEntries && seedEntries.length) { const seedNodes: FilesystemNode[] = seedEntries.map((entry) => { const base = { @@ -125,7 +126,7 @@ export function SandboxInspectProvider({ if (!node) return - if (node.type === FileType.FILE) { + if (node.type === FileType.FILE && !store.getState().isLoaded(path)) { await sandboxManagerRef.current?.readFile(path) } @@ -146,7 +147,7 @@ export function SandboxInspectProvider({ const newExpandedState = !node.isExpanded state.setExpanded(normalizedPath, newExpandedState) - if (newExpandedState && !node.isLoaded) { + if (newExpandedState && !state.isLoaded(normalizedPath)) { await sandboxManagerRef.current?.loadDirectory(normalizedPath) } }, diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index cde99d37e..2ca434d69 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -44,6 +44,7 @@ export interface FilesystemState { nodes: Map selectedPath?: string loadingPaths: Set + loadedPaths: Set errorPaths: Map sortingDirection: 'asc' | 'desc' fileContents: Map @@ -57,6 +58,7 @@ export interface FilesystemMutations { setExpanded: (path: string, expanded: boolean) => void setSelected: (path: string) => void setLoading: (path: string, loading: boolean) => void + setLoaded: (path: string, loaded: boolean) => void setError: (path: string, error?: string) => void setFileContent: (path: string, updates: FileContentState) => void resetFileContent: (path: string) => void @@ -69,6 +71,7 @@ export interface FilesystemComputed { getNode: (path: string) => FilesystemNode | undefined isExpanded: (path: string) => boolean isSelected: (path: string) => boolean + isLoaded: (path: string) => boolean hasChildren: (path: string) => boolean getFileContent: (path: string) => FileContentState | undefined } @@ -111,6 +114,7 @@ export const createFilesystemStore = (rootPath: string) => nodes: new Map(), loadingPaths: new Set(), + loadedPaths: new Set(), errorPaths: new Map(), sortingDirection: 'asc' as 'asc' | 'desc', fileContents: new Map(), @@ -250,6 +254,17 @@ export const createFilesystemStore = (rootPath: string) => }) }, + setLoaded: (path: string, loaded: boolean) => { + const normalizedPath = normalizePath(path) + set((state: FilesystemState) => { + if (loaded) { + state.loadedPaths.add(normalizedPath) + } else { + state.loadedPaths.delete(normalizedPath) + } + }) + }, + setError: (path: string, error?: string) => { const normalizedPath = normalizePath(path) @@ -329,6 +344,11 @@ export const createFilesystemStore = (rootPath: string) => return get().selectedPath === normalizedPath }, + isLoaded: (path: string) => { + const normalizedPath = normalizePath(path) + return get().loadedPaths.has(normalizedPath) + }, + hasChildren: (path: string) => { const normalizedPath = normalizePath(path) const node = get().nodes.get(normalizedPath) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/types.ts b/src/features/dashboard/sandbox/inspect/filesystem/types.ts index 2e5b757e5..c55b20c6e 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/types.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/types.ts @@ -6,7 +6,6 @@ interface FilesystemDir { path: string children: string[] // paths of children isExpanded?: boolean - isLoaded?: boolean } interface FilesystemFile { diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts index 90f25fdca..2d71462cc 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-directory.ts @@ -24,10 +24,7 @@ export function useDirectoryState(path: string) { const isLoading = useStore(store, (state) => state.loadingPaths.has(path)) const hasError = useStore(store, (state) => state.errorPaths.has(path)) const error = useStore(store, (state) => state.errorPaths.get(path)) - const isLoaded = useStore(store, (state) => { - const node = state.getNode(path) - return node?.type === 'dir' ? !!node?.isLoaded : undefined - }) + const isLoaded = useStore(store, (state) => state.isLoaded(path)) const hasChildren = useStore(store, (state) => state.hasChildren(path)) return useMemo( diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 27e1ec7a9..c4740beeb 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -189,7 +189,7 @@ export class SandboxManager { if ( !node || node.type !== FileType.DIR || - node.isLoaded || + state.isLoaded(normalizedPath) || state.loadingPaths.has(normalizedPath) ) return @@ -208,7 +208,6 @@ export class SandboxManager { type: FileType.DIR, isExpanded: false, isSelected: false, - isLoaded: false, children: [], } } else { @@ -230,8 +229,6 @@ export class SandboxManager { state.removeNode(childPath) } } - - state.updateNode(normalizedPath, { isLoaded: true }) } catch (error) { const errorMessage = SandboxManager.pipeError( error, @@ -241,6 +238,7 @@ export class SandboxManager { console.error(`Failed to load directory ${normalizedPath}:`, error) } finally { state.setLoading(normalizedPath, false) + state.setLoaded(normalizedPath, true) } } @@ -251,9 +249,6 @@ export class SandboxManager { const node = state.getNode(normalizedPath) if (!node || node.type !== FileType.DIR) return - // mark directory as stale but keep existing children until fresh data arrives - state.updateNode(normalizedPath, { isLoaded: false }) - await this.loadDirectory(normalizedPath) } @@ -329,6 +324,7 @@ export class SandboxManager { state.setFileContent(normalizedPath, { type: 'unreadable' }) } finally { state.setLoading(normalizedPath, false) + state.setLoaded(normalizedPath, true) } } From a868d92dda17efa8f41598125491dba8518c0cdf Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 16:07:27 +0200 Subject: [PATCH 55/75] fix: update useFilesystemNode hook to return undefined instead of throwing an error when node is not found --- src/features/dashboard/sandbox/inspect/hooks/use-node.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts index cf38ae9bd..95879835b 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-node.ts +++ b/src/features/dashboard/sandbox/inspect/hooks/use-node.ts @@ -8,15 +8,11 @@ import { useStore } from 'zustand' /** * Hook for accessing a specific filesystem node */ -export function useFilesystemNode(path: string): FilesystemNode { +export function useFilesystemNode(path: string): FilesystemNode | undefined { const { store } = useSandboxInspectContext() const node = useStore(store, (state) => state.getNode(path)) - if (!node) { - throw new Error(`Node at path ${path} not found`) - } - return node } From 22b98774f5ee1ddc9c89f9db47584bce10b419d3 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 16:26:48 +0200 Subject: [PATCH 56/75] refactor: improve directory loading state management in SandboxInspectProvider and SandboxManager --- src/features/dashboard/sandbox/inspect/context.tsx | 3 ++- src/features/dashboard/sandbox/inspect/hooks/use-file.tsx | 6 +++++- src/features/dashboard/sandbox/inspect/sandbox-manager.ts | 7 +------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 4ff8ef8b5..138fda39b 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -98,11 +98,12 @@ export function SandboxInspectProvider({ } if (entry.type === FileType.DIR) { + state.setLoaded(base.path, false) + return { ...base, type: FileType.DIR, isExpanded: false, - isLoaded: false, children: [], } } diff --git a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx index 460148066..8d82bdcc9 100644 --- a/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx +++ b/src/features/dashboard/sandbox/inspect/hooks/use-file.tsx @@ -60,7 +60,11 @@ export function useFile(path: string) { const ops = useFileOperations(path) return { - ...node, + ...(node && { + name: node.name, + type: node.type, + path: node.path, + }), ...state, ...ops, } diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index c4740beeb..b3dfb8b3d 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -95,13 +95,9 @@ export class SandboxManager { switch (type) { case FilesystemEventType.CREATE: case FilesystemEventType.RENAME: - if (parentNode?.type === FileType.DIR) { + if (parentNode && state.isLoaded(parentDir)) { void this.refreshDirectory(parentDir) - break } - - void this.loadDirectory(normalizedPath) - break case FilesystemEventType.REMOVE: @@ -189,7 +185,6 @@ export class SandboxManager { if ( !node || node.type !== FileType.DIR || - state.isLoaded(normalizedPath) || state.loadingPaths.has(normalizedPath) ) return From 776a5b7ef56e02ed1b484c5e0d8201b6703bd097 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 16:53:18 +0200 Subject: [PATCH 57/75] fix: enhance user feedback for aborted requests in SandboxManager error handling --- src/features/dashboard/sandbox/inspect/sandbox-manager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index b3dfb8b3d..dcecb5437 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -26,7 +26,8 @@ export class SandboxManager { */ private static readonly errorMap: Record = { 'signal timed out': 'The operation timed out. Please try again later.', - 'user aborted a request': 'The request was cancelled.', + 'user aborted a request': + 'The request was cancelled. Try downloading the file.', } private loadTimers: Map> = new Map() From d7c039b65036671a478bce8d6b4ca8f3317d968b Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 17:04:47 +0200 Subject: [PATCH 58/75] fix: update file download logic to use node name and open in new tab --- src/features/dashboard/sandbox/inspect/context.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 138fda39b..95eaa6853 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -164,9 +164,12 @@ export function SandboxInspectProvider({ if (!downloadUrl) return + const node = store.getState().getNode(path) + const a = document.createElement('a') a.href = downloadUrl - a.download = path.split('/').pop() || '' + a.download = node?.name || '' + a.target = '_blank' a.click() }, } From ada637d20b5ea09fe9b7adf3dd076cb81d76ff0e Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 17:51:06 +0200 Subject: [PATCH 59/75] fix: update file reading logic in SandboxManager to use blob format and improve error handling in determineFileContentState --- .../sandbox/inspect/sandbox-manager.ts | 6 ++--- src/lib/utils/filesystem.ts | 26 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index dcecb5437..43f3e140f 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -301,13 +301,11 @@ export class SandboxManager { try { state.setLoading(normalizedPath, true) - const bytes = await this.sandbox.files.read(normalizedPath, { - format: 'bytes', + const blob = await this.sandbox.files.read(normalizedPath, { + format: 'blob', requestTimeoutMs: 30_000, }) - const blob = new Blob([bytes]) - const contentState = await determineFileContentState(blob) state.setFileContent(normalizedPath, contentState) diff --git a/src/lib/utils/filesystem.ts b/src/lib/utils/filesystem.ts index 7dc3e63f1..b8f64dab9 100644 --- a/src/lib/utils/filesystem.ts +++ b/src/lib/utils/filesystem.ts @@ -111,21 +111,21 @@ export async function determineFileContentState( ): Promise { const mimeType = blob.type ?? '' - if (mimeType.startsWith('image/')) { - const dataUri = await new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onloadend = () => resolve(reader.result as string) - reader.onerror = () => reject(reader.error) - reader.readAsDataURL(blob) - }) - - return { type: 'image', dataUri } - } + try { + if (mimeType.startsWith('image/')) { + const dataUri = await new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onloadend = () => resolve(reader.result as string) + reader.onerror = () => reject(reader.error) + reader.readAsDataURL(blob) + }) + + return { type: 'image', dataUri } + } - const buffer = await blob.arrayBuffer() - const data = new Uint8Array(buffer) + const buffer = await blob.arrayBuffer() + const data = new Uint8Array(buffer) - try { const content = new TextDecoder('utf-8', { fatal: true }).decode(data) return { type: 'text', text: content } } catch { From 7536f91886594bd1971a0e43d83d8b1ce02a2481 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Thu, 3 Jul 2025 18:27:11 +0200 Subject: [PATCH 60/75] chore: remove outdated overview diagram from dashboard sandbox --- .../dashboard/sandbox/overview.mermaid | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 src/features/dashboard/sandbox/overview.mermaid diff --git a/src/features/dashboard/sandbox/overview.mermaid b/src/features/dashboard/sandbox/overview.mermaid deleted file mode 100644 index f38f160ad..000000000 --- a/src/features/dashboard/sandbox/overview.mermaid +++ /dev/null @@ -1,122 +0,0 @@ -flowchart TD - -%% -------------------------- -%% Server-side Components -%% -------------------------- -subgraph SERVER_FETCH["Server-side Fetch"] - direction TB - LAYOUT["layout.tsx (server)"] - PAGE["page.tsx (server)"] - DETAILS_ACTION["getSandboxDetails (action)"] - ROOT_ACTION["getSandboxRoot (action)"] - - LAYOUT -- "calls" --> DETAILS_ACTION - PAGE -- "calls" --> ROOT_ACTION - DETAILS_ACTION -- "returns sandboxInfo" --> SANDBOX_PROVIDER - ROOT_ACTION -- "returns root entries" --> INSPECT_PROVIDER -end - -%% -------------------------- -%% Client Contexts -%% -------------------------- -subgraph SANDBOX_CONTEXT["Sandbox Context"] - direction TB - SANDBOX_PROVIDER["SandboxProvider"] - SANDBOX_STATE["Runtime State"] - - SANDBOX_PROVIDER -- "tracks lifecycle" --> SANDBOX_STATE -end - -subgraph INSPECT_CONTEXT["Inspect Context"] - direction TB - INSPECT_PROVIDER["SandboxInspectProvider"] - SANDBOX_INSTANCE["Sandbox Instance (connected)"] - FILESYSTEM_STORE["FilesystemStore"] - EVENT_MANAGER["FilesystemEventManager (root recursive watcher)"] - OPERATIONS["Operations Object"] - - INSPECT_PROVIDER -- "connects" --> SANDBOX_INSTANCE - INSPECT_PROVIDER -- "creates singleton" --> FILESYSTEM_STORE - INSPECT_PROVIDER -- "creates with store + sandbox" --> EVENT_MANAGER - INSPECT_PROVIDER -- "exposes interface" --> OPERATIONS - - SANDBOX_INSTANCE -- "used by" --> EVENT_MANAGER - EVENT_MANAGER -- "writes FS data" --> FILESYSTEM_STORE - OPERATIONS -- "delegates to" --> EVENT_MANAGER - OPERATIONS -- "writes UI flags" --> FILESYSTEM_STORE -end - -%% -------------------------- -%% Hook Layer -%% -------------------------- -subgraph HOOKS["Hook Layer"] - direction TB - FILESYSTEM_HOOKS["Filesystem Hooks"] - DIRECTORY_HOOKS["Directory Hooks"] - NODE_HOOKS["Node Hooks"] - - FILESYSTEM_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE - DIRECTORY_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE - NODE_HOOKS -- "subscribe to store slices" --> FILESYSTEM_STORE - - FILESYSTEM_HOOKS -- "return operations" --> OPERATIONS - DIRECTORY_HOOKS -- "return operations" --> OPERATIONS - NODE_HOOKS -- "return operations" --> OPERATIONS -end - -%% -------------------------- -%% UI Components -%% -------------------------- -subgraph UI_COMPONENTS["UI Components"] - direction LR - FILE_TREE["FileTree"] - CODE_EDITOR["Code Editor"] - OTHER_UI["Other Components"] - - FILE_TREE -- "trigger user actions" --> USER_ACTIONS["User Actions"] - CODE_EDITOR -- "trigger user actions" --> USER_ACTIONS - OTHER_UI -- "trigger user actions" --> USER_ACTIONS -end - -%% -------------------------- -%% Remote (E2B) -%% -------------------------- -subgraph E2B_REMOTE["E2B Remote"] - REMOTE_SANDBOX["Remote Sandbox"] - FS_EVENTS["Filesystem Events"] - - REMOTE_SANDBOX -- "emits real-time" --> FS_EVENTS -end - -%% -------------------------- -%% Data Flow: User Actions -%% -------------------------- -USER_ACTIONS -- "call hooks that return" --> OPERATIONS -OPERATIONS -- "async calls to" --> EVENT_MANAGER -EVENT_MANAGER -- "API calls to" --> REMOTE_SANDBOX - -%% -------------------------- -%% Data Flow: Remote Events -%% -------------------------- -FS_EVENTS -- "handled by" --> EVENT_MANAGER -FILESYSTEM_STORE -- "triggers re-renders via" --> HOOKS -HOOKS -- "provide updated state to" --> UI_COMPONENTS - -%% -------------------------- -%% Styling -%% -------------------------- -classDef contextClass fill:#E3F2FD,stroke:#1976D2,stroke-width:2px -classDef storeClass fill:#E8F5E8,stroke:#388E3C,stroke-width:2px -classDef managerClass fill:#FFF3E0,stroke:#F57C00,stroke-width:2px -classDef hooksClass fill:#FCE4EC,stroke:#C2185B,stroke-width:2px -classDef uiClass fill:#F1F8E9,stroke:#689F38,stroke-width:2px -classDef remoteClass fill:#FFEBEE,stroke:#D32F2F,stroke-width:2px -classDef serverClass fill:#ECEFF1,stroke:#455A64,stroke-width:2px - -class SANDBOX_PROVIDER,INSPECT_PROVIDER contextClass -class FILESYSTEM_STORE storeClass -class EVENT_MANAGER,OPERATIONS managerClass -class FILESYSTEM_HOOKS,DIRECTORY_HOOKS,NODE_HOOKS hooksClass -class FILE_TREE,CODE_EDITOR,OTHER_UI,USER_ACTIONS uiClass -class REMOTE_SANDBOX,FS_EVENTS remoteClass -class LAYOUT,PAGE,DETAILS_ACTION,ROOT_ACTION serverClass \ No newline at end of file From 97cc291323027f72283fb52d57514a36fc738861 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Mon, 7 Jul 2025 18:04:09 +0200 Subject: [PATCH 61/75] fix: enhance error handling in SandboxManager to detect directory-related errors and load directory contents appropriately --- .../sandbox/inspect/sandbox-manager.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 43f3e140f..c1ca21528 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -30,6 +30,13 @@ export class SandboxManager { 'The request was cancelled. Try downloading the file.', } + // Detect error substrings that imply the requested path is actually a directory + private static readonly dirErrorHints: string[] = [ + 'eisdir', + 'is a directory', + 'illegal operation on a directory', + ] + private loadTimers: Map> = new Map() private pendingLoads: Map< string, @@ -310,6 +317,59 @@ export class SandboxManager { state.setFileContent(normalizedPath, contentState) } catch (err) { + // ──────────────────────────────────────────────────────────────── + // Handle the special case where the SDK mis-classifies a symlink + // to a directory as FileType.FILE. The read() call then throws + // an EISDIR / "is a directory" error. We intercept that, convert + // the node to a directory, load its children and *skip* setting an + // error state so the UI does not flicker. + // ──────────────────────────────────────────────────────────────── + + const rawMessage = + err instanceof Error + ? err.message.toLowerCase() + : String(err).toLowerCase() + + const looksLikeDirectory = SandboxManager.dirErrorHints.some((hint) => + rawMessage.includes(hint) + ) + + if (looksLikeDirectory) { + try { + const entries = await this.sandbox.files.list(normalizedPath) + + state.updateNode(normalizedPath, { + type: FileType.DIR, + isExpanded: true, + children: [], + }) + + state.setError(normalizedPath) + + const nodes: FilesystemNode[] = entries.map((entry: EntryInfo) => + entry.type === FileType.DIR + ? { + name: entry.name, + path: entry.path, + type: FileType.DIR, + isExpanded: false, + isSelected: false, + children: [], + } + : { + name: entry.name, + path: entry.path, + type: FileType.FILE, + isSelected: false, + } + ) + + state.addNodes(normalizedPath, nodes) + + return + } catch {} + } + const errorMessage = SandboxManager.pipeError(err, 'Failed to read file') console.error(`Failed to read file ${normalizedPath}:`, err) From 1142756c4940ce3ff133ea582100269930837a4f Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Tue, 15 Jul 2025 15:46:14 +0200 Subject: [PATCH 62/75] feat: add Content Security Policy header to enhance security in Next.js configuration --- next.config.mjs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/next.config.mjs b/next.config.mjs index 26623c764..96fa3793b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,6 +3,19 @@ import { createMDX } from 'fumadocs-mdx/next' const withMDX = createMDX() +const cspHeader = ` + default-src 'self'; + script-src 'self' 'unsafe-eval' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + img-src 'self' blob: data:; + font-src 'self'; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; + upgrade-insecure-requests; +` + /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, @@ -26,8 +39,12 @@ const config = { trailingSlash: false, headers: async () => [ { - source: '/:path*', + source: '/(.*)', headers: [ + { + key: 'Content-Security-Policy', + value: cspHeader.replace(/\n/g, ''), + }, { // config to prevent the browser from rendering the page inside a frame or iframe and avoid clickjacking http://en.wikipedia.org/wiki/Clickjacking key: 'X-Frame-Options', From a028a859b7863e0ceb0843d1ad340d65eb40c626 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Sat, 19 Jul 2025 14:15:01 +0200 Subject: [PATCH 63/75] refactor: move fumadocs roots to archive + add env configs for CSP headers --- .env.example | 8 +++++ .../_docs/[[...slug]]/page.tsx | 28 +++++---------- {src/app => archive}/_docs/layout.tsx | 0 {src/app => archive}/_docs/not-found.tsx | 0 .../analyze-data-with-ai/index.mdx | 0 .../analyze-data-with-ai/meta.json | 0 .../pre-installed-libraries.mdx | 0 .../create-charts-visualizations/index.mdx | 0 .../interactive-charts.mdx | 0 .../create-charts-visualizations/meta.json | 0 .../static-charts.mdx | 0 .../supported-languages/meta.json | 0 .../content/docs/(documentation)/index.mdx | 0 .../content/docs/(documentation)/meta.json | 0 .../quickstart/connect-llms.mdx | 0 .../docs/(documentation)/quickstart/index.mdx | 0 .../quickstart/install-custom-packages.mdx | 0 .../docs/(documentation)/quickstart/meta.json | 0 .../quickstart/migrating-from-v0.mdx | 0 .../quickstart/upload-download-files.mdx | 0 archive/metadata.ts | 35 +++++++++++++++++++ source.config.ts => archive/source.config.ts | 0 {src/lib => archive}/source.ts | 0 next.config.mjs | 24 +++++++------ package.json | 1 - src/configs/metadata.ts | 8 +++++ src/features/client-providers.tsx | 16 ++++----- src/features/dashboard/sandbox/context.tsx | 6 ++-- .../dashboard/sandboxes/table-cells.tsx | 2 +- src/lib/clients/api.ts | 2 +- src/lib/env.ts | 21 +++++++++++ tsconfig.json | 18 ++++------ 32 files changed, 113 insertions(+), 56 deletions(-) rename {src/app => archive}/_docs/[[...slug]]/page.tsx (72%) rename {src/app => archive}/_docs/layout.tsx (100%) rename {src/app => archive}/_docs/not-found.tsx (100%) rename {src => archive}/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx (100%) rename {src => archive}/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json (100%) rename {src => archive}/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx (100%) rename {src => archive}/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx (100%) rename {src => archive}/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx (100%) rename {src => archive}/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json (100%) rename {src => archive}/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx (100%) rename {src => archive}/content/docs/(documentation)/code-interpreting/supported-languages/meta.json (100%) rename {src => archive}/content/docs/(documentation)/index.mdx (100%) rename {src => archive}/content/docs/(documentation)/meta.json (100%) rename {src => archive}/content/docs/(documentation)/quickstart/connect-llms.mdx (100%) rename {src => archive}/content/docs/(documentation)/quickstart/index.mdx (100%) rename {src => archive}/content/docs/(documentation)/quickstart/install-custom-packages.mdx (100%) rename {src => archive}/content/docs/(documentation)/quickstart/meta.json (100%) rename {src => archive}/content/docs/(documentation)/quickstart/migrating-from-v0.mdx (100%) rename {src => archive}/content/docs/(documentation)/quickstart/upload-download-files.mdx (100%) create mode 100644 archive/metadata.ts rename source.config.ts => archive/source.config.ts (100%) rename {src/lib => archive}/source.ts (100%) diff --git a/.env.example b/.env.example index 6078881cf..a062c589b 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,14 @@ KV_REST_API_URL= NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +### ================================= +### OPTIONAL CONTENT SECURITY POLICY +### ================================= +### Example: https://e2b.dev *.e2b.dev +CSP_SCRIPT_SRC= +CSP_STYLE_SRC= +CSP_IMG_SRC= + ### ================================= ### OPTIONAL SERVER ENVIRONMENT VARIABLES ### ================================= diff --git a/src/app/_docs/[[...slug]]/page.tsx b/archive/_docs/[[...slug]]/page.tsx similarity index 72% rename from src/app/_docs/[[...slug]]/page.tsx rename to archive/_docs/[[...slug]]/page.tsx index 4c00c93e3..ce972c8f8 100644 --- a/src/app/_docs/[[...slug]]/page.tsx +++ b/archive/_docs/[[...slug]]/page.tsx @@ -1,20 +1,9 @@ -import { - DocsBody, - DocsDescription, - DocsPage, - DocsTitle, -} from 'fumadocs-ui/page' import type { Metadata } from 'next' import { notFound } from 'next/navigation' /* import { Popup, PopupContent, PopupTrigger } from "fumadocs-twoslash/ui"; */ /* import * as Preview from "@/components/preview"; */ -import { createMetadata, metadataImage } from '@/configs/fumadocs' -import { METADATA } from '@/configs/metadata' -import components from '@/features/docs/components' -import Footer from '@/features/docs/footer/footer' -import { source } from '@/lib/source' -import { cn } from '@/lib/utils' -import { buttonVariants } from '@/ui/primitives/button' +import { createMetadata, METADATA, metadataImage } from '../../metadata' +import { source } from '../../source' /* function PreviewRenderer({ preview }: { preview: string }): ReactNode { if (preview && preview in Preview) { @@ -27,15 +16,17 @@ import { buttonVariants } from '@/ui/primitives/button' export default async function Page(props: { params: Promise<{ slug?: string[] }> -}): Promise> { +}) { const params = await props.params const page = source.getPage(params.slug) if (!page) notFound() - const path = `src/content/docs/${page.file.path}` - const { body: Mdx, toc, lastModified } = page.data + const path = `src/content/docs/${page.path}` + + return null + /* const { body: Mdx, toc, lastModified } = page.data return ( {page.data.title} {page.data.description} - {/* {preview ? : null} */} + {preview ? : null} } - {/* {page.data.index ? : null} */} - ) + ) */ } export async function generateMetadata(props: { diff --git a/src/app/_docs/layout.tsx b/archive/_docs/layout.tsx similarity index 100% rename from src/app/_docs/layout.tsx rename to archive/_docs/layout.tsx diff --git a/src/app/_docs/not-found.tsx b/archive/_docs/not-found.tsx similarity index 100% rename from src/app/_docs/not-found.tsx rename to archive/_docs/not-found.tsx diff --git a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx b/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx similarity index 100% rename from src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx rename to archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx diff --git a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json b/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json similarity index 100% rename from src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json rename to archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json diff --git a/src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx b/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx similarity index 100% rename from src/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx rename to archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx diff --git a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx b/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx similarity index 100% rename from src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx rename to archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx diff --git a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx b/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx similarity index 100% rename from src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx rename to archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx diff --git a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json b/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json similarity index 100% rename from src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json rename to archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json diff --git a/src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx b/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx similarity index 100% rename from src/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx rename to archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx diff --git a/src/content/docs/(documentation)/code-interpreting/supported-languages/meta.json b/archive/content/docs/(documentation)/code-interpreting/supported-languages/meta.json similarity index 100% rename from src/content/docs/(documentation)/code-interpreting/supported-languages/meta.json rename to archive/content/docs/(documentation)/code-interpreting/supported-languages/meta.json diff --git a/src/content/docs/(documentation)/index.mdx b/archive/content/docs/(documentation)/index.mdx similarity index 100% rename from src/content/docs/(documentation)/index.mdx rename to archive/content/docs/(documentation)/index.mdx diff --git a/src/content/docs/(documentation)/meta.json b/archive/content/docs/(documentation)/meta.json similarity index 100% rename from src/content/docs/(documentation)/meta.json rename to archive/content/docs/(documentation)/meta.json diff --git a/src/content/docs/(documentation)/quickstart/connect-llms.mdx b/archive/content/docs/(documentation)/quickstart/connect-llms.mdx similarity index 100% rename from src/content/docs/(documentation)/quickstart/connect-llms.mdx rename to archive/content/docs/(documentation)/quickstart/connect-llms.mdx diff --git a/src/content/docs/(documentation)/quickstart/index.mdx b/archive/content/docs/(documentation)/quickstart/index.mdx similarity index 100% rename from src/content/docs/(documentation)/quickstart/index.mdx rename to archive/content/docs/(documentation)/quickstart/index.mdx diff --git a/src/content/docs/(documentation)/quickstart/install-custom-packages.mdx b/archive/content/docs/(documentation)/quickstart/install-custom-packages.mdx similarity index 100% rename from src/content/docs/(documentation)/quickstart/install-custom-packages.mdx rename to archive/content/docs/(documentation)/quickstart/install-custom-packages.mdx diff --git a/src/content/docs/(documentation)/quickstart/meta.json b/archive/content/docs/(documentation)/quickstart/meta.json similarity index 100% rename from src/content/docs/(documentation)/quickstart/meta.json rename to archive/content/docs/(documentation)/quickstart/meta.json diff --git a/src/content/docs/(documentation)/quickstart/migrating-from-v0.mdx b/archive/content/docs/(documentation)/quickstart/migrating-from-v0.mdx similarity index 100% rename from src/content/docs/(documentation)/quickstart/migrating-from-v0.mdx rename to archive/content/docs/(documentation)/quickstart/migrating-from-v0.mdx diff --git a/src/content/docs/(documentation)/quickstart/upload-download-files.mdx b/archive/content/docs/(documentation)/quickstart/upload-download-files.mdx similarity index 100% rename from src/content/docs/(documentation)/quickstart/upload-download-files.mdx rename to archive/content/docs/(documentation)/quickstart/upload-download-files.mdx diff --git a/archive/metadata.ts b/archive/metadata.ts new file mode 100644 index 000000000..7944b1cf5 --- /dev/null +++ b/archive/metadata.ts @@ -0,0 +1,35 @@ +import type { Metadata } from 'next/types' +import { createMetadataImage } from 'fumadocs-core/server' +import { source } from './source' + +export const METADATA = { + title: 'E2B - Code Interpreting for AI apps', + description: 'Open-source secure sandboxes for AI code execution', +} + +export const metadataImage = createMetadataImage({ + source, + imageRoute: 'og', +}) + +export function createMetadata(override: Metadata): Metadata { + return { + ...override, + openGraph: { + title: override.title ?? undefined, + description: override.description ?? undefined, + url: 'https://fumadocs.vercel.app', + images: '/banner.png', + siteName: 'Fumadocs', + ...override.openGraph, + }, + twitter: { + card: 'summary_large_image', + creator: '@money_is_shark', + title: override.title ?? undefined, + description: override.description ?? undefined, + images: '/banner.png', + ...override.twitter, + }, + } +} diff --git a/source.config.ts b/archive/source.config.ts similarity index 100% rename from source.config.ts rename to archive/source.config.ts diff --git a/src/lib/source.ts b/archive/source.ts similarity index 100% rename from src/lib/source.ts rename to archive/source.ts diff --git a/next.config.mjs b/next.config.mjs index 96fa3793b..b4d6bef2a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,18 +1,17 @@ import { withSentryConfig } from '@sentry/nextjs' -import { createMDX } from 'fumadocs-mdx/next' -const withMDX = createMDX() const cspHeader = ` default-src 'self'; - script-src 'self' 'unsafe-eval' 'unsafe-inline'; - style-src 'self' 'unsafe-inline'; - img-src 'self' blob: data:; + script-src 'self' 'unsafe-eval' 'unsafe-inline' ${process.env.CSP_SCRIPT_SRC}; + style-src 'self' 'unsafe-inline' ${process.env.CSP_STYLE_SRC}; + img-src 'self' data: ${process.env.CSP_IMG_SRC}; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; + worker-src 'self' blob: ${process.env.CSP_SCRIPT_SRC}; upgrade-insecure-requests; ` @@ -41,10 +40,6 @@ const config = { { source: '/(.*)', headers: [ - { - key: 'Content-Security-Policy', - value: cspHeader.replace(/\n/g, ''), - }, { // config to prevent the browser from rendering the page inside a frame or iframe and avoid clickjacking http://en.wikipedia.org/wiki/Clickjacking key: 'X-Frame-Options', @@ -52,6 +47,15 @@ const config = { }, ], }, + { + source: '/dashboard/(.*)', + headers: [ + { + key: 'Content-Security-Policy', + value: cspHeader.replace(/\n/g, ''), + }, + ], + }, ], rewrites: async () => [ { @@ -112,7 +116,7 @@ const config = { skipTrailingSlashRedirect: true, } -export default withSentryConfig(withMDX(config), { +export default withSentryConfig(config, { // For all available options, see: // https://www.npmjs.com/package/@sentry/webpack-plugin#options diff --git a/package.json b/package.json index 432282016..ada930ab6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "<<<<<< Development": "", "shad": "bunx shadcn@canary", "prebuild": "bun scripts:check-app-env", - "postinstall": "fumadocs-mdx", "<<<<<< Testing": "", "test:run": "bun scripts:check-all-env && vitest run", "test:integration": "bun scripts:check-app-env && vitest run src/__test__/integration/", diff --git a/src/configs/metadata.ts b/src/configs/metadata.ts index 1d42e34c2..606a3fc97 100644 --- a/src/configs/metadata.ts +++ b/src/configs/metadata.ts @@ -1,4 +1,12 @@ +import type { Metadata } from 'next/types' + export const METADATA = { title: 'E2B - Code Interpreting for AI apps', description: 'Open-source secure sandboxes for AI code execution', } + +export function createMetadata(override: Metadata): Metadata { + return { + ...override, + } +} diff --git a/src/features/client-providers.tsx b/src/features/client-providers.tsx index 8b56776de..8c9499e11 100644 --- a/src/features/client-providers.tsx +++ b/src/features/client-providers.tsx @@ -2,7 +2,7 @@ import { ToastProvider } from '@/ui/primitives/toast' import { TooltipProvider } from '@/ui/primitives/tooltip' -import { RootProvider } from 'fumadocs-ui/provider' +import { ThemeProvider } from 'next-themes' import posthog from 'posthog-js' import { PostHogProvider as PHProvider } from 'posthog-js/react' import { useEffect } from 'react' @@ -14,18 +14,16 @@ interface ClientProvidersProps { export default function ClientProviders({ children }: ClientProvidersProps) { return ( - {children} - + ) } diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index 5fac4f238..13a13d0b6 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -1,10 +1,10 @@ 'use client' import React, { createContext, useContext, ReactNode } from 'react' -import { SandboxInfo } from '@/types/api' +import { Sandbox } from '@/types/api' interface SandboxContextValue { - sandboxInfo: SandboxInfo + sandboxInfo: Sandbox } const SandboxContext = createContext(null) @@ -19,7 +19,7 @@ export function useSandboxContext() { interface SandboxProviderProps { children: ReactNode - sandboxInfo: SandboxInfo + sandboxInfo: Sandbox } export function SandboxProvider({ diff --git a/src/features/dashboard/sandboxes/table-cells.tsx b/src/features/dashboard/sandboxes/table-cells.tsx index 5b0b28dd2..b08894f68 100644 --- a/src/features/dashboard/sandboxes/table-cells.tsx +++ b/src/features/dashboard/sandboxes/table-cells.tsx @@ -1,7 +1,7 @@ 'use client' import { PROTECTED_URLS } from '@/configs/urls' -import { useServerContext } from '@/lib/hooks/use-server-context' +import { useServerContext } from '@/features/dashboard/server-context' import { cn } from '@/lib/utils' import { Template } from '@/types/api' import { JsonPopover } from '@/ui/json-popover' diff --git a/src/lib/clients/api.ts b/src/lib/clients/api.ts index 320c371b0..4079ad160 100644 --- a/src/lib/clients/api.ts +++ b/src/lib/clients/api.ts @@ -11,7 +11,7 @@ export const infra = createClient({ // @ts-expect-error -- duplex not on type, keep it for now duplex: !!body ? 'half' : undefined, ...options, - }) + } as RequestInit) }, querySerializer: { array: { style: 'form', explode: false }, diff --git a/src/lib/env.ts b/src/lib/env.ts index 05c49c88f..f2c3a729a 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -1,5 +1,23 @@ import { z } from 'zod' +const CSPSrcSchema = z.string().refine((domains) => + domains.split(' ').every((domain) => { + // CSP allows either: + // 1. Full URLs with scheme: https://example.com + // 2. Wildcard subdomains without scheme: *.example.com + // 3. Plain domains without scheme: example.com + const fullUrlPattern = /^https?:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i + const wildcardPattern = /^\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i + const plainDomainPattern = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i + + return ( + fullUrlPattern.test(domain) || + wildcardPattern.test(domain) || + plainDomainPattern.test(domain) + ) + }) +) + export const serverSchema = z.object({ SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), INFRA_API_URL: z.string().url(), @@ -10,6 +28,9 @@ export const serverSchema = z.object({ BILLING_API_URL: z.string().url().optional(), OTEL_SERVICE_NAME: z.string().optional(), ZEROBOUNCE_API_KEY: z.string().optional(), + CSP_SCRIPT_SRC: CSPSrcSchema.optional(), + CSP_STYLE_SRC: CSPSrcSchema.optional(), + CSP_IMG_SRC: CSPSrcSchema.optional(), VERCEL_ENV: z.enum(['production', 'preview', 'development']).optional(), VERCEL_URL: z.string().optional(), VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(), diff --git a/tsconfig.json b/tsconfig.json index 1f5c78aa7..b549ff4f0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -24,18 +20,16 @@ } ], "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] }, "isolatedModules": true }, "include": [ "next-env.d.ts", "src", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + "archive/_docs", + "archive/source.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From c8a24832ef7064ffb63ba843e85568090862cc70 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Sat, 19 Jul 2025 14:19:41 +0200 Subject: [PATCH 64/75] fix: add Supabase URL to CSP img-src directive --- next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.mjs b/next.config.mjs index b4d6bef2a..5a27a2bf2 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -5,7 +5,7 @@ const cspHeader = ` default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' ${process.env.CSP_SCRIPT_SRC}; style-src 'self' 'unsafe-inline' ${process.env.CSP_STYLE_SRC}; - img-src 'self' data: ${process.env.CSP_IMG_SRC}; + img-src 'self' data: ${process.env.NEXT_PUBLIC_SUPABASE_URL} ${process.env.CSP_IMG_SRC}; font-src 'self'; object-src 'none'; base-uri 'self'; From 3947550c1259841e6e95fc1f4a06e50c777a236a Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Sat, 19 Jul 2025 14:22:10 +0200 Subject: [PATCH 65/75] chore: add GitHub and Google avatar URLs to CSP image sources --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index a062c589b..82dc32a05 100644 --- a/.env.example +++ b/.env.example @@ -31,7 +31,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key ### Example: https://e2b.dev *.e2b.dev CSP_SCRIPT_SRC= CSP_STYLE_SRC= -CSP_IMG_SRC= +CSP_IMG_SRC=https://avatars.githubusercontent.com https://lh3.googleusercontent.com ### ================================= ### OPTIONAL SERVER ENVIRONMENT VARIABLES From 3cbcfb28cc017e4d0e5e83cb781e97a56692144b Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Sat, 19 Jul 2025 14:42:55 +0200 Subject: [PATCH 66/75] chore: configure ESLint directories and clean up TypeScript paths --- .eslintignore | 1 + next.config.mjs | 3 +++ tsconfig.json | 10 ++-------- 3 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..8b77f12ba --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +archive/ \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 5a27a2bf2..3c9d0266b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -17,6 +17,9 @@ const cspHeader = ` /** @type {import('next').NextConfig} */ const config = { + eslint: { + dirs: ['src', 'scripts'], // Only run ESLint on these directories during production builds + }, reactStrictMode: true, experimental: { reactCompiler: true, diff --git a/tsconfig.json b/tsconfig.json index b549ff4f0..61e986b17 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,12 +24,6 @@ }, "isolatedModules": true }, - "include": [ - "next-env.d.ts", - "src", - ".next/types/**/*.ts", - "archive/_docs", - "archive/source.ts" - ], - "exclude": ["node_modules"] + "include": ["next-env.d.ts", "src", ".next/types/**/*.ts"], + "exclude": ["node_modules", "archive"] } From d1608e0469e70690cda37d231c8db94fa17e8109 Mon Sep 17 00:00:00 2001 From: Ben Fornefeld Date: Sat, 19 Jul 2025 14:52:20 +0200 Subject: [PATCH 67/75] feat: add frame-src CSP header support with configurable sources --- .env.example | 1 + next.config.mjs | 1 + src/lib/env.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/.env.example b/.env.example index 82dc32a05..4318f8060 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key CSP_SCRIPT_SRC= CSP_STYLE_SRC= CSP_IMG_SRC=https://avatars.githubusercontent.com https://lh3.googleusercontent.com +CSP_FRAME_SRC=https://vercel.live ### ================================= ### OPTIONAL SERVER ENVIRONMENT VARIABLES diff --git a/next.config.mjs b/next.config.mjs index 3c9d0266b..b7f3a4844 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -6,6 +6,7 @@ const cspHeader = ` script-src 'self' 'unsafe-eval' 'unsafe-inline' ${process.env.CSP_SCRIPT_SRC}; style-src 'self' 'unsafe-inline' ${process.env.CSP_STYLE_SRC}; img-src 'self' data: ${process.env.NEXT_PUBLIC_SUPABASE_URL} ${process.env.CSP_IMG_SRC}; + frame-src 'self' ${process.env.CSP_FRAME_SRC}; font-src 'self'; object-src 'none'; base-uri 'self'; diff --git a/src/lib/env.ts b/src/lib/env.ts index f2c3a729a..5143bf74c 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -31,6 +31,7 @@ export const serverSchema = z.object({ CSP_SCRIPT_SRC: CSPSrcSchema.optional(), CSP_STYLE_SRC: CSPSrcSchema.optional(), CSP_IMG_SRC: CSPSrcSchema.optional(), + CSP_FRAME_SRC: CSPSrcSchema.optional(), VERCEL_ENV: z.enum(['production', 'preview', 'development']).optional(), VERCEL_URL: z.string().optional(), VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(), From 76e67858bc028c481fcfcea03d9e5ec31ffb4201 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Mon, 28 Jul 2025 11:35:45 +0200 Subject: [PATCH 68/75] chore: remove deprecated files and clean up archive structure --- archive/_docs/[[...slug]]/page.tsx | 90 -- archive/_docs/layout.tsx | 35 - archive/_docs/not-found.tsx | 9 - .../analyze-data-with-ai/index.mdx | 532 ------------ .../analyze-data-with-ai/meta.json | 4 - .../pre-installed-libraries.mdx | 36 - .../create-charts-visualizations/index.mdx | 11 - .../interactive-charts.mdx | 177 ---- .../create-charts-visualizations/meta.json | 4 - .../static-charts.mdx | 69 -- .../supported-languages/meta.json | 4 - .../content/docs/(documentation)/index.mdx | 34 - .../content/docs/(documentation)/meta.json | 15 - .../quickstart/connect-llms.mdx | 772 ------------------ .../docs/(documentation)/quickstart/index.mdx | 79 -- .../quickstart/install-custom-packages.mdx | 198 ----- .../docs/(documentation)/quickstart/meta.json | 5 - .../quickstart/migrating-from-v0.mdx | 74 -- .../quickstart/upload-download-files.mdx | 149 ---- archive/metadata.ts | 35 - archive/source.config.ts | 61 -- archive/source.ts | 20 - src/configs/fumadocs.ts | 30 - src/lib/clients/api.ts | 1 - src/server/sandboxes/get-sandbox-root.ts | 5 +- 25 files changed, 2 insertions(+), 2447 deletions(-) delete mode 100644 archive/_docs/[[...slug]]/page.tsx delete mode 100644 archive/_docs/layout.tsx delete mode 100644 archive/_docs/not-found.tsx delete mode 100644 archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx delete mode 100644 archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json delete mode 100644 archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx delete mode 100644 archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx delete mode 100644 archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx delete mode 100644 archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json delete mode 100644 archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx delete mode 100644 archive/content/docs/(documentation)/code-interpreting/supported-languages/meta.json delete mode 100644 archive/content/docs/(documentation)/index.mdx delete mode 100644 archive/content/docs/(documentation)/meta.json delete mode 100644 archive/content/docs/(documentation)/quickstart/connect-llms.mdx delete mode 100644 archive/content/docs/(documentation)/quickstart/index.mdx delete mode 100644 archive/content/docs/(documentation)/quickstart/install-custom-packages.mdx delete mode 100644 archive/content/docs/(documentation)/quickstart/meta.json delete mode 100644 archive/content/docs/(documentation)/quickstart/migrating-from-v0.mdx delete mode 100644 archive/content/docs/(documentation)/quickstart/upload-download-files.mdx delete mode 100644 archive/metadata.ts delete mode 100644 archive/source.config.ts delete mode 100644 archive/source.ts delete mode 100644 src/configs/fumadocs.ts diff --git a/archive/_docs/[[...slug]]/page.tsx b/archive/_docs/[[...slug]]/page.tsx deleted file mode 100644 index ce972c8f8..000000000 --- a/archive/_docs/[[...slug]]/page.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import type { Metadata } from 'next' -import { notFound } from 'next/navigation' -/* import { Popup, PopupContent, PopupTrigger } from "fumadocs-twoslash/ui"; */ -/* import * as Preview from "@/components/preview"; */ -import { createMetadata, METADATA, metadataImage } from '../../metadata' -import { source } from '../../source' - -/* function PreviewRenderer({ preview }: { preview: string }): ReactNode { - if (preview && preview in Preview) { - const Comp = Preview[preview as keyof typeof Preview]; - return ; - } - - return null; -} */ - -export default async function Page(props: { - params: Promise<{ slug?: string[] }> -}) { - const params = await props.params - - const page = source.getPage(params.slug) - - if (!page) notFound() - - const path = `src/content/docs/${page.path}` - - return null - /* const { body: Mdx, toc, lastModified } = page.data - - return ( - , - }} - article={{ - className: 'pb-16 xl:pt-10 max-w-3xl xl:ml-0', - }} - > - {page.data.title} - {page.data.description} - - {preview ? : null} } - - - - ) */ -} - -export async function generateMetadata(props: { - params: Promise<{ slug?: string[] }> -}): Promise { - const params = await props.params - const page = source.getPage(params.slug) - - if (!page) notFound() - - const description = page.data.description ?? METADATA.description - - return createMetadata( - metadataImage.withImage(page.slugs, { - title: page.data.title, - description, - openGraph: { - url: `/docs/${page.slugs.join('/')}`, - }, - }) - ) -} - -export function generateStaticParams(): { slug: string[] }[] { - return source.generateParams() -} diff --git a/archive/_docs/layout.tsx b/archive/_docs/layout.tsx deleted file mode 100644 index 212391e46..000000000 --- a/archive/_docs/layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client' - -import '@/styles/docs.css' - -import { baseOptions } from '@/app/layout.config' -import { DocsLayout, type DocsLayoutProps } from 'fumadocs-ui/layouts/docs' -import type { ReactNode } from 'react' -/* import "fumadocs-twoslash/twoslash.css"; */ -import { Nav } from '@/features/docs/navbar/navbar' -import Sidebar from '@/features/docs/sidebar/sidebar' -import { source } from '@/lib/source' -import { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area' -/* import { Trigger } from "@/components/ai/search-ai"; */ - -const docsOptions: DocsLayoutProps = { - ...baseOptions, - tree: source.pageTree, - sidebar: { - component: , - }, -} - -export default function Layout({ children }: { children: ReactNode }) { - return ( -
-
- ) -} diff --git a/archive/_docs/not-found.tsx b/archive/_docs/not-found.tsx deleted file mode 100644 index fb3a8e9a9..000000000 --- a/archive/_docs/not-found.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import NotFound from '@/ui/not-found' - -export default function NotFoundPage() { - return ( -
- -
- ) -} diff --git a/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx b/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx deleted file mode 100644 index 49a552fc6..000000000 --- a/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/index.mdx +++ /dev/null @@ -1,532 +0,0 @@ ---- -title: Overview -description: This guide will show you how to analyze data with AI using E2B Sandbox. ---- - -You can use E2B Sandbox to run AI-generated code to analyze data. Here's how the AI data analysis workflow usually looks like: -1. Your user has a dataset in CSV format or other formats. -2. You prompt the LLM to generate code (usually Python) based on the user's data. -3. The sandbox runs the AI-generated code and returns the results. -4. You display the results to the user. - ---- - -## Example: Analyze CSV file with E2B and Claude 3.5 Sonnet -This short example will show you how to use E2B Sandbox to run AI-generated code to analyze CSV data. - -### Table of Contents -1. [Install dependencies](#1-install-dependencies) -2. [Set your API keys](#2-set-your-api-keys) -3. [Download example CSV file](#3-download-example-csv-file) -4. [Initialize the sandbox and upload the dataset to the sandbox](#4-initialize-the-sandbox-and-upload-the-dataset-to-the-sandbox) -5. [Prepare the method for running AI-generated code](#5-prepare-the-method-for-running-ai-generated-code) -6. [Prepare the prompt and initialize Anthropic client](#6-prepare-the-prompt-and-initialize-anthropic-client) -7. [Connect the sandbox to the LLM with tool calling](#7-connect-the-sandbox-to-the-llm-with-tool-calling) -8. [Parse the LLM response and run the AI-generated code in the sandbox](#8-parse-the-llm-response-and-run-the-ai-generated-code-in-the-sandbox) -9. [Save the generated chart](#9-save-the-generated-chart) -10. [Run the code](#10-run-the-code) -11. [Full final code](#11-full-final-code) - -### 1. Install dependencies -Install the E2B SDK and Claude SDK to your project by running the following command in your terminal. - - -```bash tab -npm i @e2b/code-interpreter @anthropic-ai/sdk dotenv -``` - -```bash tab -pip install e2b-code-interpreter anthropic python-dotenv -``` - - -### 2. Set your API keys -1. Get your E2B API key from [E2B Dashboard](/dashboard?tab=keys). -2. Get your Claude API key from [Claude API Dashboard](https://console.anthropic.com/settings/keys). -3. Paste the keys into your `.env` file. - - -```bash tab -E2B_API_KEY=e2b_*** -ANTHROPIC_API_KEY=sk-ant-*** -``` - - -### 3. Download example CSV file -{/* We'll be using the publicly available [AirBnB NYC dataset](https://www.kaggle.com/datasets/dgomonov/new-york-city-airbnb-open-data). */} - -We'll be using the publicly available [dataset of about 10,000 movies](https://www.kaggle.com/datasets/muqarrishzaib/tmdb-10000-movies-dataset). -1. Click the "Download" button at the top of the page. -2. Select "Download as zip (2 MB)". -3. Unzip the file and you should see `dataset.csv` file. Move it to the root of your project. - - -### 4. Initialize the sandbox and upload the dataset to the sandbox -We'll upload the dataset from the third step to the sandbox and save it as `dataset.csv` file. - - -```ts tab title="index.ts" -import 'dotenv/config' -import fs from 'fs' -import { Sandbox } from '@e2b/code-interpreter' - -// Create sandbox -const sbx = await Sandbox.create() - -// Upload the dataset to the sandbox -const content = fs.readFileSync('dataset.csv') -const datasetPathInSandbox = await sbx.files.write('dataset.csv', content) -``` - -```python tab title="main.py" -from dotenv import load_dotenv -load_dotenv() -from e2b_code_interpreter import Sandbox - -# Create sandbox -sbx = Sandbox() - -# Upload the dataset to the sandbox -dataset_path_in_sandbox = "" -with open("dataset.csv", "rb") as f: - dataset_path_in_sandbox = sbx.files.write("dataset.csv", f) -``` - - -### 5. Prepare the method for running AI-generated code -Add the following code to the file. Here we're adding the method for code execution. - - -```js tab title="index.ts" -// ... code from the previous step - -async function runAIGeneratedCode(aiGeneratedCode: string) { - console.log('Running the code in the sandbox....') - const execution = await sbx.runCode(aiGeneratedCode) - console.log('Code execution finished!') - console.log(execution) -} -``` -```python tab title="main.py" -# ... code from the previous step - -def run_ai_generated_code(ai_generated_code: str): - print('Running the code in the sandbox....') - execution = sbx.run_code(ai_generated_code) - print('Code execution finished!') - print(execution) -``` - - -### 6. Prepare the prompt and initialize Anthropic client -The prompt we'll be using describes the dataset and the analysis we want to perform like this: - 1. Describe the columns in the CSV dataset. - 2. Ask the LLM what we want to analyze - here we want to analyze the vote average over time. We're asking for a chart as the output. - 3. Instruct the LLM to generate Python code for the data analysis. - - -```js tab title="index.ts" -import Anthropic from '@anthropic-ai/sdk' - -const prompt = ` -I have a CSV file about movies. It has about 10k rows. It's saved in the sandbox at ${dataset_path_in_sandbox.path}. -These are the columns: -- 'id': number, id of the movie -- 'original_language': string like "eng", "es", "ko", etc -- 'original_title': string that's name of the movie in the original language -- 'overview': string about the movie -- 'popularity': float, from 0 to 9137.939. It's not normalized at all and there are outliers -- 'release_date': date in the format yyyy-mm-dd -- 'title': string that's the name of the movie in english -- 'vote_average': float number between 0 and 10 that's representing viewers voting average -- 'vote_count': int for how many viewers voted - -I want to better understand how the vote average has changed over the years. Write Python code that analyzes the dataset based on my request and produces right chart accordingly` - -const anthropic = new Anthropic() -console.log('Waiting for the model response...') -const msg = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-20240620', - max_tokens: 1024, - messages: [{ role: 'user', content: prompt }], -}) -``` - -```python tab title="main.py" -from anthropic import Anthropic - -prompt = ''' -I have a CSV file about movies. It has about 10k rows. It's saved in the sandbox at {datasetPathInSandbox.path}. -These are the columns: -- 'id': number, id of the movie -- 'original_language': string like "eng", "es", "ko", etc -- 'original_title': string that's name of the movie in the original language -- 'overview': string about the movie -- 'popularity': float, from 0 to 9137.939. It's not normalized at all and there are outliers -- 'release_date': date in the format yyyy-mm-dd -- 'title': string that's the name of the movie in english -- 'vote_average': float number between 0 and 10 that's representing viewers voting average -- 'vote_count': int for how many viewers voted - -I want to better understand how the vote average has changed over the years. Write Python code that analyzes the dataset based on my request and produces right chart accordingly''' - -anthropic = Anthropic() -msg = anthropic.messages.create( - model='claude-3-5-sonnet-20240620', - max_tokens=1024, - messages=[ - {"role": "user", "content": prompt} - ] -) -``` - - -### 7. Connect the sandbox to the LLM with tool calling -We'll use Claude's ability to [use tools (function calling)](https://docs.anthropic.com/en/docs/build-with-claude/tool-use) to run the code in the sandbox. - -The way we'll do it is by connecting the method for running AI-generated code we created in the previous step to the Claude model. - -Update the initialization of the Anthropic client to include the tool use like this: - -```js tab title="index.ts" -const msg = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-20240620', - max_tokens: 1024, - messages: [{ role: 'user', content: prompt }], - tools: [ // $HighlightLine - { // $HighlightLine - name: 'run_python_code', // $HighlightLine - description: 'Run Python code', // $HighlightLine - input_schema: { // $HighlightLine - type: 'object', // $HighlightLine - properties: { // $HighlightLine - code: { // $HighlightLine - type: 'string', // $HighlightLine - description: 'The Python code to run', // $HighlightLine - }, // $HighlightLine - }, // $HighlightLine - required: ['code'], // $HighlightLine - }, // $HighlightLine - }, // $HighlightLine - ], // $HighlightLine -}) -``` -```python tab title="main.py" -msg = anthropic.messages.create( - model='claude-3-5-sonnet-20240620', - max_tokens=1024, - messages=[ - {"role": "user", "content": prompt} - ], - tools=[ # $HighlightLine - { # $HighlightLine - "name": "run_python_code", # $HighlightLine - "description": "Run Python code", # $HighlightLine - "input_schema": { # $HighlightLine - "type": "object", # $HighlightLine - "properties": { # $HighlightLine - "code": { "type": "string", "description": "The Python code to run" }, # $HighlightLine - }, # $HighlightLine - "required": ["code"], # $HighlightLine - }, # $HighlightLine - }, # $HighlightLine - ], # $HighlightLine -) -``` - - -### 8. Parse the LLM response and run the AI-generated code in the sandbox -Now we'll parse the `msg` object to get the code from the LLM's response based on the tool we created in the previous step. -Once we have the code, we'll pass it to the `runAIGeneratedCode` method in JavaScript or `run_ai_generated_code` method in Python we created in the previous step to run the code in the sandbox. - - -```js tab title="index.ts" -// ... code from the previous steps - -interface CodeRunToolInput { - code: string -} - -for (const contentBlock of msg.content) { - if (contentBlock.type === 'tool_use') { - if (contentBlock.name === 'run_python_code') { - const code = (contentBlock.input as CodeRunToolInput).code - console.log('Will run following code in the sandbox', code) - // Execute the code in the sandbox - await runAIGeneratedCode(code) - } - } -} -``` -```python tab title="main.py" -for content_block in msg.content: - if content_block.type == 'tool_use': - if content_block.name == 'run_python_code': - code = content_block.input['code'] - print('Will run following code in the sandbox', code) - # Execute the code in the sandbox - run_ai_generated_code(code) -``` - - -### 9. Save the generated chart -When running code in the sandbox for data analysis, you can get different types of results. -Including stdout, stderr, charts, tables, text, runtime errors, and more. - -In this example we're specifically asking for a chart so we'll be looking for the chart in the results. - -Let's update the `runAIGeneratedCode` method in JavaScript and `run_ai_generated_code` method in Python to check for the chart in the results and save it to the file. - -```js tab title="index.ts" -async function runAIGeneratedCode(aiGeneratedCode: string) { - console.log('Running the code in the sandbox....') - const execution = await sbx.runCode(aiGeneratedCode) - console.log('Code execution finished!') - - // First let's check if the code ran successfully. - if (execution.error) { // $HighlightLine - console.error('AI-generated code had an error.') // $HighlightLine - console.log(execution.error.name) // $HighlightLine - console.log(execution.error.value) // $HighlightLine - console.log(execution.error.traceback) // $HighlightLine - process.exit(1) // $HighlightLine - } // $HighlightLine - - // Iterate over all the results and specifically check for png files that will represent the chart. - let resultIdx = 0 // $HighlightLine - for (const result of execution.results) { // $HighlightLine - if (result.png) { // $HighlightLine - // Save the png to a file - // The png is in base64 format. - fs.writeFileSync(`chart-${resultIdx}.png`, result.png, { encoding: 'base64' }) // $HighlightLine - console.log(`Chart saved to chart-${resultIdx}.png`) // $HighlightLine - resultIdx++ // $HighlightLine - } // $HighlightLine - } // $HighlightLine -} -``` -```python tab title="main.py" -def run_ai_generated_code(ai_generated_code: str): - print('Running the code in the sandbox....') - execution = sbx.run_code(ai_generated_code) - print('Code execution finished!') - - # First let's check if the code ran successfully. - if execution.error: # $HighlightLine - print('AI-generated code had an error.') # $HighlightLine - print(execution.error.name) # $HighlightLine - print(execution.error.value) # $HighlightLine - print(execution.error.traceback) # $HighlightLine - sys.exit(1) # $HighlightLine - - # Iterate over all the results and specifically check for png files that will represent the chart. - result_idx = 0 # $HighlightLine - for result in execution.results: # $HighlightLine - if result.png: # $HighlightLine - # Save the png to a file - # The png is in base64 format. - with open(f'chart-{result_idx}.png', 'wb') as f: # $HighlightLine - f.write(base64.b64decode(result.png)) # $HighlightLine - print(f'Chart saved to chart-{result_idx}.png') # $HighlightLine - result_idx += 1 # $HighlightLine -``` - - -### 10. Run the code -Now you can run the whole code to see the results. - - -```bash tab -npx tsx index.ts -``` - -```bash tab -python main.py -``` - - -You should see the chart in the root of your project that will look similar to this: - -![Chart visualizing voting average of our dataset over time](/graphics/docs/analyze-data-chart.png) - -### Full final code -Check the full code in JavaScript and Python below: - -```js tab title="index.ts" -import 'dotenv/config' -import fs from 'fs' -import Anthropic from '@anthropic-ai/sdk' -import { Sandbox } from '@e2b/code-interpreter' - -// Create sandbox -const sbx = await Sandbox.create() - -// Upload the dataset to the sandbox -const content = fs.readFileSync('dataset.csv') -const datasetPathInSandbox = await sbx.files.write('/home/user/dataset.csv', content) - -async function runAIGeneratedCode(aiGeneratedCode: string) { - const execution = await sbx.runCode(aiGeneratedCode) - if (execution.error) { - console.error('AI-generated code had an error.') - console.log(execution.error.name) - console.log(execution.error.value) - console.log(execution.error.traceback) - process.exit(1) - } - // Iterate over all the results and specifically check for png files that will represent the chart. - let resultIdx = 0 - for (const result of execution.results) { - if (result.png) { - // Save the png to a file - // The png is in base64 format. - fs.writeFileSync(`chart-${resultIdx}.png`, result.png, { encoding: 'base64' }) - console.log('Chart saved to chart-${resultIdx}.png') - resultIdx++ - } - } -} - -const prompt = ` -I have a CSV file about movies. It has about 10k rows. It's saved in the sandbox at ${datasetPathInSandbox.path}. -These are the columns: -- 'id': number, id of the movie -- 'original_language': string like "eng", "es", "ko", etc -- 'original_title': string that's name of the movie in the original language -- 'overview': string about the movie -- 'popularity': float, from 0 to 9137.939. It's not normalized at all and there are outliers -- 'release_date': date in the format yyyy-mm-dd -- 'title': string that's the name of the movie in english -- 'vote_average': float number between 0 and 10 that's representing viewers voting average -- 'vote_count': int for how many viewers voted - -I want to better understand how the vote average has changed over the years. Write Python code that analyzes the dataset based on my request and produces right chart accordingly` - -const anthropic = new Anthropic() -console.log('Waiting for the model response...') -const msg = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-20240620', - max_tokens: 1024, - messages: [{ role: 'user', content: prompt }], - tools: [ - { - name: 'run_python_code', - description: 'Run Python code', - input_schema: { - type: 'object', - properties: { - code: { - type: 'string', - description: 'The Python code to run', - }, - }, - required: ['code'], - }, - }, - ], -}) - -interface CodeRunToolInput { - code: string -} - -for (const contentBlock of msg.content) { - if (contentBlock.type === 'tool_use') { - if (contentBlock.name === 'run_python_code') { - const code = (contentBlock.input as CodeRunToolInput).code - console.log('Will run following code in the sandbox', code) - // Execute the code in the sandbox - await runAIGeneratedCode(code) - } - } -} -``` -```python tab title="main.py" -import sys -import base64 -from dotenv import load_dotenv -load_dotenv() -from e2b_code_interpreter import Sandbox -from anthropic import Anthropic - -# Create sandbox -sbx = Sandbox() - -# Upload the dataset to the sandbox -with open("../dataset.csv", "rb") as f: - dataset_path_in_sandbox = sbx.files.write("dataset.csv", f) - - -def run_ai_generated_code(ai_generated_code: str): - print('Running the code in the sandbox....') - execution = sbx.notebook.exec_cell(ai_generated_code) - print('Code execution finished!') - - # First let's check if the code ran successfully. - if execution.error: - print('AI-generated code had an error.') - print(execution.error.name) - print(execution.error.value) - print(execution.error.traceback) - sys.exit(1) - - # Iterate over all the results and specifically check for png files that will represent the chart. - result_idx = 0 - for result in execution.results: - if result.png: - # Save the png to a file - # The png is in base64 format. - with open(f'chart-{result_idx}.png', 'wb') as f: - f.write(base64.b64decode(result.png)) - print(f'Chart saved to chart-{result_idx}.png') - result_idx += 1 - -prompt = f""" -I have a CSV file about movies. It has about 10k rows. It's saved in the sandbox at {dataset_path_in_sandbox.path}. -These are the columns: -- 'id': number, id of the movie -- 'original_language': string like "eng", "es", "ko", etc -- 'original_title': string that's name of the movie in the original language -- 'overview': string about the movie -- 'popularity': float, from 0 to 9137.939. It's not normalized at all and there are outliers -- 'release_date': date in the format yyyy-mm-dd -- 'title': string that's the name of the movie in english -- 'vote_average': float number between 0 and 10 that's representing viewers voting average -- 'vote_count': int for how many viewers voted - -I want to better understand how the vote average has changed over the years. -Write Python code that analyzes the dataset based on my request and produces right chart accordingly""" - -anthropic = Anthropic() -print("Waiting for model response...") -msg = anthropic.messages.create( - model='claude-3-5-sonnet-20240620', - max_tokens=1024, - messages=[ - {"role": "user", "content": prompt} - ], - tools=[ - { - "name": "run_python_code", - "description": "Run Python code", - "input_schema": { - "type": "object", - "properties": { - "code": { "type": "string", "description": "The Python code to run" }, - }, - "required": ["code"] - } - } - ] -) - -for content_block in msg.content: - if content_block.type == "tool_use": - if content_block.name == "run_python_code": - code = content_block.input["code"] - print("Will run following code in the sandbox", code) - # Execute the code in the sandbox - run_ai_generated_code(code) - -``` - diff --git a/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json b/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json deleted file mode 100644 index a97598499..000000000 --- a/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Analyze data with AI", - "pages": ["index", "pre-installed-libraries"] -} diff --git a/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx b/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx deleted file mode 100644 index 73418feda..000000000 --- a/archive/content/docs/(documentation)/code-interpreting/analyze-data-with-ai/pre-installed-libraries.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Pre-installed libraries ---- - -The sandbox comes with a [set of pre-installed Python libraries](https://github.com/e2b-dev/code-interpreter/blob/main/template/requirements.txt) for data analysis -but you can [install additional packages](/docs/quickstart/install-custom-packages): -- `aiohttp` (v3.9.3) -- `beautifulsoup4` (v4.12.3) -- `bokeh` (v3.3.4) -- `gensim` (v4.3.2) -- `imageio` (v2.34.0) -- `joblib` (v1.3.2) -- `librosa` (v0.10.1) -- `matplotlib` (v3.8.3) -- `nltk` (v3.8.1) -- `numpy` (v1.26.4) -- `opencv-python` (v4.9.0.80) -- `openpyxl` (v3.1.2) -- `pandas` (v1.5.3) -- `plotly` (v5.19.0) -- `pytest` (v8.1.0) -- `python`-docx (v1.1.0) -- `pytz` (v2024.1) -- `requests` (v2.26.0) -- `scikit-image` (v0.22.0) -- `scikit-learn` (v1.4.1.post1) -- `scipy` (v1.12.0) -- `seaborn` (v0.13.2) -- `soundfile` (v0.12.1) -- `spacy` (v3.7.4) -- `textblob` (v0.18.0) -- `tornado` (v6.4) -- `urllib3` (v1.26.7) -- `xarray` (v2024.2.0) -- `xlrd` (v2.0.1) -- `sympy` (v1.12) diff --git a/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx b/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx deleted file mode 100644 index 78de55f57..000000000 --- a/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Overview -description: E2B Sandbox allows you to create charts and visualizations by executing Python code inside the sandbox with `runCode()` method in JavaScript and `run_code()` method in Python. ---- - -These charts and visualizations can be [static](/docs/code-interpreting/create-charts-visualizations/static-charts) or [interactive](/docs/code-interpreting/create-charts-visualizations/interactive-charts) plots. - -{/* -Learn more about different types of results that E2B Sandbox can return [(TODO: link)here](/docs/code-interpreting/results). - */} - diff --git a/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx b/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx deleted file mode 100644 index c436fa005..000000000 --- a/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/interactive-charts.mdx +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: Interactive charts -description: E2B allows you to create interactive charts with custom styling. ---- - -E2B automatically detects charts when executing Python code with `runCode()` in JavaScript or `run_code()` in Python. The Python code must include Matplotlib charts. - -When a chart is detected, E2B sends the data of the chart back to the client. You can access the chart in the `execution.results` array where each item is a `Result` object with the `chart` property. - - -Try out [AI Data Analyst](https://github.com/e2b-dev/ai-analyst/) - a Next.js app that uses E2B to create interactive charts. - - -Here's a simple example of bar chart: - -```js tab -import { Sandbox, BarChart } from '@e2b/code-interpreter' - -const code = ` -import matplotlib.pyplot as plt - -# Prepare data -authors = ['Author A', 'Author B', 'Author C', 'Author D'] -sales = [100, 200, 300, 400] - -# Create and customize the bar chart -plt.figure(figsize=(10, 6)) -plt.bar(authors, sales, label='Books Sold', color='blue') -plt.xlabel('Authors') -plt.ylabel('Number of Books Sold') -plt.title('Book Sales by Authors') - -# Display the chart -plt.tight_layout() -plt.show() -` - -const sandbox = await Sandbox.create() -const result = await sandbox.runCode(code) -const chart = result.results[0].chart as BarChart - -console.log('Type:', chart.type) -console.log('Title:', chart.title) -console.log('X Label:', chart.x_label) -console.log('Y Label:', chart.y_label) -console.log('X Unit:', chart.x_unit) -console.log('Y Unit:', chart.y_unit) -console.log('Elements:', chart.elements) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -code = """ -import matplotlib.pyplot as plt - -# Prepare data -authors = ['Author A', 'Author B', 'Author C', 'Author D'] -sales = [100, 200, 300, 400] - -# Create and customize the bar chart -plt.figure(figsize=(10, 6)) -plt.bar(authors, sales, label='Books Sold', color='blue') -plt.xlabel('Authors') -plt.ylabel('Number of Books Sold') -plt.title('Book Sales by Authors') - -# Display the chart -plt.tight_layout() -plt.show() -""" - -sandbox = Sandbox() -execution = sandbox.run_code(code) -chart = execution.results[0].chart - -print('Type:', chart.type) -print('Title:', chart.title) -print('X Label:', chart.x_label) -print('Y Label:', chart.y_label) -print('X Unit:', chart.x_unit) -print('Y Unit:', chart.y_unit) -print('Elements:') -for element in chart.elements: - print('\n Label:', element.label) - print(' Value:', element.value) - print(' Group:', element.group) -``` - - -The code above will output the following: - -```bash tab -Type: bar -Title: Book Sales by Authors -X Label: Authors -Y Label: Number of Books Sold -X Unit: null -Y Unit: null -Elements: [ - { - label: "Author A", - group: "Books Sold", - value: 100, - }, { - label: "Author B", - group: "Books Sold", - value: 200, - }, { - label: "Author C", - group: "Books Sold", - value: 300, - }, { - label: "Author D", - group: "Books Sold", - value: 400, - } -] -``` - -```bash tab -Type: ChartType.BAR -Title: Book Sales by Authors -X Label: Authors -Y Label: Number of Books Sold -X Unit: None -Y Unit: None -Elements: - - Label: Author A - Value: 100.0 - Group: Books Sold - - Label: Author B - Value: 200.0 - Group: Books Sold - - Label: Author C - Value: 300.0 - Group: Books Sold - - Label: Author D - Value: 400.0 - Group: Books Sold -``` - - -You can send this data to your frontend to create an interactive chart with your favorite charting library. - ---- - -## Supported intertactive charts -The following charts are currently supported: -- Line chart -- Bar chart -- Scatter plot -- Pie chart -- Box and whisker plot - - -{/* The following charts are currently supported: -- [Line chart](#line-chart) -- [Bar chart](#bar-chart) -- [Scatter plot](#scatter-plot) -- [Pie chart](#pie-chart) -- [Box and whisker plot](#box-and-whisker-plot) - - -## Line chart - -## Bar chart - -## Scatter plot - -## Pie chart - -## Box and whisker plot */} \ No newline at end of file diff --git a/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json b/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json deleted file mode 100644 index da28260ea..000000000 --- a/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Create charts & visualizations", - "pages": ["index", "static-charts", "interactive-charts"] -} diff --git a/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx b/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx deleted file mode 100644 index adb55485c..000000000 --- a/archive/content/docs/(documentation)/code-interpreting/create-charts-visualizations/static-charts.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Static charts ---- - -Every time you run Python code with `runCode()` in JavaScript or `run_code()` method in Python, the code is executed in a headless Jupyter server inside the sandbox. - -E2B automatically detects any plots created with Matplotlib and sends them back to the client as images encoded in the base64 format. -These images are directly accesible on the `result` items in the `execution.results` array. - -Here's how to retrieve a static chart from the executed Python code that contains a Matplotlib plot. - -```js tab -import { Sandbox } from '@e2b/code-interpreter' -import fs from 'fs' - -const codeToRun = ` -import matplotlib.pyplot as plt - -plt.plot([1, 2, 3, 4]) -plt.ylabel('some numbers') -plt.show() -` -const sandbox = await Sandbox.create() - -// Run the code inside the sandbox -const execution = await sandbox.runCode(codeToRun) - -// There's only one result in this case - the plot displayed with `plt.show()` -const firstResult = execution.results[0] - -if (firstResult.png) { - // Save the png to a file. The png is in base64 format. - fs.writeFileSync('chart.png', firstResult.png, { encoding: 'base64' }) - console.log('Chart saved as chart.png') -} -``` - -```python tab -import base64 -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() - -code_to_run = """ -import matplotlib.pyplot as plt - -plt.plot([1, 2, 3, 4]) -plt.ylabel('some numbers') -plt.show() -""" - -sandbox = Sandbox() - -# Run the code inside the sandbox -execution = sandbox.run_code(code_to_run) - -# There's only one result in this case - the plot displayed with `plt.show()` -first_result = execution.results[0] - -if first_result.png: - // Save the png to a file. The png is in base64 format. - with open('chart.png', 'wb') as f: - f.write(base64.b64decode(first_result.png)) - print('Chart saved as chart.png') -``` - - -The code in the variable `codeToRun`/`code_to_run` will produce this following plot that we're saving as `chart.png` file. -![Static chart produced by the code](/graphics/docs/static-chart.png) \ No newline at end of file diff --git a/archive/content/docs/(documentation)/code-interpreting/supported-languages/meta.json b/archive/content/docs/(documentation)/code-interpreting/supported-languages/meta.json deleted file mode 100644 index 3a2900ea8..000000000 --- a/archive/content/docs/(documentation)/code-interpreting/supported-languages/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Supported languages", - "pages": ["index"] -} diff --git a/archive/content/docs/(documentation)/index.mdx b/archive/content/docs/(documentation)/index.mdx deleted file mode 100644 index 7c864d781..000000000 --- a/archive/content/docs/(documentation)/index.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: E2B Documentation -description: Here you'll find all the guides, concepts, and SDK references for developing with E2B. ---- - - - -```bash tab -npm i @e2b/code-interpreter -``` - -```bash tab -pip install e2b-code-interpreter -``` - - - -## What is E2B? - -E2B is an [open-source](https://github.com/e2b-dev) infrastructure that allows you run to AI-generated code in secure isolated sandboxes in the cloud. -To start and control sandboxes, use our [Python SDK](https://pypi.org/project/e2b/) or [JavaScript SDK](https://www.npmjs.com/package/e2b). - -Some of the typical use cases for E2B are AI data analysis or visualization, running AI-generated code of various languages, playground for coding agents, environment for codegen evals, or running full AI-generated apps like in [Fragments](https://github.com/e2b-dev/fragments). - -### Under the hood - -The E2B Sandbox is a small isolated VM the can be started very quickly (~150ms). You can think of it as a small computer for the AI model. You can run many sandboxes at once. Typically, you run separate sandbox for each LLM, user, or AI agent session in your app. -For example, if you were building an AI data analysis chatbot, you would start the sandbox for every user session. - -## Quickstart - -## Code interpreting with AI - -## Learn the core concepts diff --git a/archive/content/docs/(documentation)/meta.json b/archive/content/docs/(documentation)/meta.json deleted file mode 100644 index 534af30f4..000000000 --- a/archive/content/docs/(documentation)/meta.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "title": "Documentation", - "description": "The E2B documentation", - "root": true, - "defaultOpen": true, - "pages": [ - "---Home---", - "quickstart", - "quickstart/migrating-from-v0", - "--- ---", - "---Code Interpreting---", - "code-interpreting/analyze-data-with-ai", - "code-interpreting/create-charts-visualizations" - ] -} diff --git a/archive/content/docs/(documentation)/quickstart/connect-llms.mdx b/archive/content/docs/(documentation)/quickstart/connect-llms.mdx deleted file mode 100644 index f5b7dbac8..000000000 --- a/archive/content/docs/(documentation)/quickstart/connect-llms.mdx +++ /dev/null @@ -1,772 +0,0 @@ ---- -title: Connect LLMs to E2B -description: E2B can work with any LLM and AI framework. The easiest way to connect an LLM to E2B is to use the tool use capabilities of the LLM (sometimes known as function calling). ---- - -If the LLM doesn't support tool use, you can, for example, prompt the LLM to output code snippets and then manually extract the code snippets with [RegEx](https://en.wikipedia.org/wiki/Regular_expression). - -## Contents -- [OpenAI](#openai) -- [Anthropic](#anthropic) -- [Mistral](#mistral) -- [Groq](#groq) -- [Vercel AI SDK](#vercel-ai-sdk) -- [CrewAI](#crewai) -- [LangChain](#langchain) -- [LlamaIndex](#llamaindex) -- [Ollama](#ollama) - ---- - -## OpenAI - -### Simple - - - -```python tab -# pip install openai e2b-code-interpreter -from openai import OpenAI -from e2b_code_interpreter import Sandbox - -# Create OpenAI client -client = OpenAI() -system = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Send messages to OpenAI API -response = client.chat.completions.create( - model="gpt-4o", - messages=[ - {"role": "system", "content": system}, - {"role": "user", "content": prompt} - ] -) - -# Extract the code from the response -code = response.choices[0].message.content - -# Execute code in E2B Sandbox -if code: - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.text - - print(result) -``` - - - -### Function calling - - - -```python tab -# pip install openai e2b-code-interpreter -import json -from openai import OpenAI -from e2b_code_interpreter import Sandbox - -# Create OpenAI client -client = OpenAI() -model = "gpt-4o" - -# Define the messages -messages = [ - { - "role": "user", - "content": "Calculate how many r's are in the word 'strawberry'" - } -] - -# Define the tools -tools = [{ - "type": "function", - "function": { - "name": "execute_python", - "description": "Execute python code in a Jupyter notebook cell and return result", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The python code to execute in a single cell" - } - }, - "required": ["code"] - } - } -}] - -# Generate text with OpenAI -response = client.chat.completions.create( - model=model, - messages=messages, - tools=tools, -) - -# Append the response message to the messages list -response_message = response.choices[0].message -messages.append(response_message) - -# Execute the tool if it's called by the model -if response_message.tool_calls: - for tool_call in response_message.tool_calls: - if tool_call.function.name == "execute_python": - # Create a sandbox and execute the code - with Sandbox() as sandbox: - code = json.loads(tool_call.function.arguments)['code'] - execution = sandbox.run_code(code) - result = execution.text - - # Send the result back to the model - messages.append({ - "role": "tool", - "name": "execute_python", - "content": result, - "tool_call_id": tool_call.id, - }) - -# Generate the final response -final_response = client.chat.completions.create( - model=model, - messages=messages -) - -print(final_response.choices[0].message.content) -``` - - - ---- - -## Anthropic - -### Simple - - - -```python tab -# pip install anthropic e2b-code-interpreter -from anthropic import Anthropic -from e2b_code_interpreter import Sandbox - -# Create Anthropic client -anthropic = Anthropic() -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Send messages to Anthropic API -response = anthropic.messages.create( - model="claude-3-5-sonnet-20240620", - max_tokens=1024, - messages=[ - {"role": "assistant", "content": system_prompt}, - {"role": "user", "content": prompt} - ] -) - -# Extract code from response -code = response.content[0].text - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.logs.stdout - -print(result) -``` - - - -### Function calling - - - -```python tab -# pip install anthropic e2b-code-interpreter -from anthropic import Anthropic -from e2b_code_interpreter import Sandbox - -# Create Anthropic client -client = Anthropic() -model = "claude-3-5-sonnet-20240620" - -# Define the messages -messages = [ - { - "role": "user", - "content": "Calculate how many r's are in the word 'strawberry'" - } -] - -# Define the tools -tools = [{ - "name": "execute_python", - "description": "Execute python code in a Jupyter notebook cell and return (not print) the result", - "input_schema": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The python code to execute in a single cell" - } - }, - "required": ["code"] - } -}] - -# Generate text with Anthropic -message = client.messages.create( - model=model, - max_tokens=1024, - messages=messages, - tools=tools -) - -# Append the response message to the messages list -messages.append({ - "role": "assistant", - "content": message.content -}) - -# Execute the tool if it's called by the model -if message.stop_reason == "tool_use": - tool_use = next(block for block in message.content if block.type == "tool_use") - tool_name = tool_use.name - tool_input = tool_use.input - - if tool_name == "execute_python": - with Sandbox() as sandbox: - code = tool_input['code'] - execution = sandbox.run_code(code) - result = execution.text - - # Append the tool result to the messages list - messages.append({ - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": tool_use.id, - "content": result, - } - ], - }) - -# Generate the final response -final_response = client.messages.create( - model=model, - max_tokens=1024, - messages=messages, - tools=tools -) - -print(final_response.content[0].text) -``` - - - ---- - -## Mistral - -### Simple - - - -```python tab -# pip install mistralai e2b-code-interpreter -import os -from mistralai import Mistral -from e2b_code_interpreter import Sandbox - -# Create Mistral client -client = Mistral(api_key=os.environ["MISTRAL_API_KEY"]) -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Send the prompt to the model -response = client.chat.complete( - model="codestral-latest", - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": prompt} - ] -) - -# Extract the code from the response -code = response.choices[0].message.content - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.text - -print(result) -``` - - - -### Function calling - - - -```python tab -# pip install mistralai e2b-code-interpreter -import os -import json -from mistralai import Mistral -from e2b_code_interpreter import Sandbox - -# Create Mistral client -client = Mistral(api_key=os.environ["MISTRAL_API_KEY"]) -model = "mistral-large-latest" -messages = [ - { - "role": "user", - "content": "Calculate how many r's are in the word 'strawberry'" - } -] - -# Define the tools -tools = [{ - "type": "function", - "function": { - "name": "execute_python", - "description": "Execute python code in a Jupyter notebook cell and return result", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The python code to execute in a single cell" - } - }, - "required": ["code"] - } - } -}] - -# Send the prompt to the model -response = client.chat.complete( - model=model, - messages=messages, - tools=tools -) - -# Append the response message to the messages list -response_message = response.choices[0].message -messages.append(response_message) - -# Execute the tool if it's called by the model -if response_message.tool_calls: - for tool_call in response_message.tool_calls: - if tool_call.function.name == "execute_python": - # Create a sandbox and execute the code - with Sandbox() as sandbox: - code = json.loads(tool_call.function.arguments)['code'] - execution = sandbox.run_code(code) - result = execution.text - - # Send the result back to the model - messages.append({ - "role": "tool", - "name": "execute_python", - "content": result, - "tool_call_id": tool_call.id, - }) - -# Generate the final response -final_response = client.chat.complete( - model=model, - messages=messages, -) - -print(final_response.choices[0].message.content) -``` - - - ---- - -## Groq - - -```python tab -# pip install groq e2b-code-interpreter -import os -from groq import Groq -from e2b_code_interpreter import Sandbox - -api_key = os.environ["GROQ_API_KEY"] - -# Create Groq client -client = Groq(api_key=api_key) -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry.'" - -# Send the prompt to the model -response = client.chat.completions.create( - model="llama3-70b-8192", - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": prompt}, - ] -) - -# Extract the code from the response -code = response.choices[0].message.content - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.text - -print(result) -``` - - - ---- - -## Vercel AI SDK -Vercel's [AI SDK](https://sdk.vercel.ai) offers support for multiple different LLM providers through a unified JavaScript interface that's easy to use. - -### Simple - - - -```js tab -// npm install ai @ai-sdk/openai @e2b/code-interpreter -import { openai } from '@ai-sdk/openai' -import { generateText } from 'ai' -import { Sandbox } from '@e2b/code-interpreter' - -// Create OpenAI client -const model = openai('gpt-4o') -const system = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -const prompt = "Calculate how many r's are in the word 'strawberry'" - -// Generate code with OpenAI -const { text: code } = await generateText({ - model, - system, - prompt -}) - -// Run the code in E2B Sandbox -const sandbox = await Sandbox.create() -const { text, results, logs, error } = await sandbox.runCode(code) - -console.log(text) -``` - - - -### Function calling - - - -```js tab -// npm install ai @ai-sdk/openai zod @e2b/code-interpreter -import { openai } from '@ai-sdk/openai' -import { generateText } from 'ai' -import z from 'zod' -import { Sandbox } from '@e2b/code-interpreter' - -// Create OpenAI client -const model = openai('gpt-4o') - -const prompt = "Calculate how many r's are in the word 'strawberry'" - -// Generate text with OpenAI -const { text } = await generateText({ - model, - prompt, - tools: { - // Define a tool that runs code in a sandbox - execute_python: { - description: 'Execute python code in a Jupyter notebook cell and return result', - parameters: z.object({ - code: z.string().describe('The python code to execute in a single cell'), - }), - execute: async ({ code }) => { - // Create a sandbox, execute LLM-generated code, and return the result - const sandbox = await Sandbox.create() - const { text, results, logs, error } = await sandbox.runCode(code) - return results - }, - }, - }, - // This is required to feed the tool call result back to the LLM - maxSteps: 2 -}) - -console.log(text) -``` - - - ---- - -## CrewAI -[CrewAI](https://crewai.com/) is a platform for building AI agents. - - - -```python tab -# pip install crewai crewai[tools] e2b-code-interpreter -from crewai_tools import tool -from crewai import Agent, Task, Crew, LLM -from e2b_code_interpreter import Sandbox - -@tool("Python interpreter tool") -def execute_python(code: str): - """ - Execute Python code and return the results. - """ - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - return execution.text - -# Define the agent -python_executor = Agent( - role='Python Executor', - goal='Execute Python code and return the results', - backstory='You are an expert Python programmer capable of executing code and returning results.', - tools=[execute_python], - llm=LLM(model="gpt-4o") -) - -# Define the task -execute_task = Task( - description="Calculate how many r's are in the word 'strawberry'", - agent=python_executor, - expected_output="The number of r's in the word 'strawberry'" -) - -# Create the crew -code_execution_crew = Crew( - agents=[python_executor], - tasks=[execute_task], - verbose=True, -) - -# Run the crew -result = code_execution_crew.kickoff() -print(result) -``` - - - ---- - -## LangChain -[LangChain](https://langchain.com/) offers support multiple different LLM providers. - -### Simple - - - -```python tab -# pip install langchain langchain-openai e2b-code-interpreter -from langchain_openai import ChatOpenAI -from langchain_core.prompts import ChatPromptTemplate -from langchain_core.output_parsers import StrOutputParser -from e2b_code_interpreter import Sandbox - -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Create LangChain components -llm = ChatOpenAI(model="gpt-4o") -prompt_template = ChatPromptTemplate.from_messages([ - ("system", system_prompt), - ("human", "{input}") -]) - -output_parser = StrOutputParser() - -# Create the chain -chain = prompt_template | llm | output_parser - -# Run the chain -code = chain.invoke({"input": prompt}) - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.text - -print(result) -``` - - - -### Agent - - - -```python tab -# pip install langchain langchain-openai e2b-code-interpreter -from langchain_core.prompts import ChatPromptTemplate -from langchain_core.tools import tool -from langchain.agents import create_tool_calling_agent, AgentExecutor -from langchain_openai import ChatOpenAI -from e2b_code_interpreter import Sandbox - -system_prompt = "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." -prompt = "Calculate how many r's are in the word 'strawberry'" - -# Define the tool -@tool -def execute_python(code: str): - """ - Execute python code in a Jupyter notebook. - """ - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - return execution.text - -# Define LangChain components -prompt_template = ChatPromptTemplate.from_messages([ - ("system", system_prompt), - ("human", "{input}"), - ("placeholder", "{agent_scratchpad}"), -]) - -tools = [execute_python] -llm = ChatOpenAI(model="gpt-4o", temperature=0) - -agent = create_tool_calling_agent(llm, tools, prompt_template) -agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) - -# Run the agent -agent_executor.invoke({"input": prompt}) -``` - - - -### Function calling - - - -```python tab -# pip install langchain langchain-openai e2b-code-interpreter -from langchain_openai import ChatOpenAI -from langchain.tools import Tool -from langchain.schema import HumanMessage, AIMessage, FunctionMessage -from e2b_code_interpreter import Sandbox - -def execute_python(code: str): - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - return execution.text - -# Define a tool that uses the E2B Sandbox -e2b_sandbox_tool = Tool( - name="execute_python", - func=execute_python, - description="Execute python code in a Jupyter notebook cell and return result" -) - -# Initialize the language model and bind the tool -llm = ChatOpenAI(model="gpt-4o").bind_tools([e2b_sandbox_tool]) - -# Define the messages -messages = [ - HumanMessage(content="Calculate how many 'r's are in the word 'strawberry'.") -] - -# Run the model with a prompt -result = llm.invoke(messages) -messages.append(AIMessage(content=result.content)) - -# Check if the model called the tool -if result.additional_kwargs.get('tool_calls'): - tool_call = result.additional_kwargs['tool_calls'][0] - if tool_call['function']['name'] == "execute_python": - code = tool_call['function']['arguments'] - execution_result = execute_python(code) - - # Send the result back to the model - messages.append( - FunctionMessage(name="execute_python", content=execution_result) - ) - -final_result = llm.invoke(messages) -print(final_result.content) -``` - - - ---- - -## LlamaIndex -[LlamaIndex](https://www.llamaindex.ai/) offers support multiple different LLM providers. - - -```python tab -# pip install llama-index e2b-code-interpreter -from llama_index.core.tools import FunctionTool -from llama_index.llms.openai import OpenAI -from llama_index.core.agent import ReActAgent -from e2b_code_interpreter import Sandbox - -# Define the tool -def execute_python(code: str): - with Sandbox() as sandbox: - execution = sandbox.run_code(code) - return execution.text - -e2b_sandbox_tool = FunctionTool.from_defaults( - name="execute_python", - description="Execute python code in a Jupyter notebook cell and return result", - fn=execute_python -) - -# Initialize LLM -llm = OpenAI(model="gpt-4o") - -# Initialize ReAct agent -agent = ReActAgent.from_tools([e2b_sandbox_tool], llm=llm, verbose=True) -agent.chat("Calculate how many r's are in the word 'strawberry'") -``` - - - -## Ollama - - - -```python tab -# pip install ollama -import ollama -from e2b_code_interpreter import Sandbox - -# Send the prompt to the model -response = ollama.chat( - model="llama3.2", - messages=[{ - "role": "system", - "content": "You are a helpful assistant that can execute python code in a Jupyter notebook. Only respond with the code to be executed and nothing else. Strip backticks in code blocks." - }, - { - "role": "user", - "content": "Calculate how many r's are in the word 'strawberry'" - } -]) - -# Extract the code from the response -code = response['message']['content'] - -# Execute code in E2B Sandbox -with Sandbox() as sandbox: - execution = sandbox.run_code(code) - result = execution.logs.stdout - -print(result) -``` - - diff --git a/archive/content/docs/(documentation)/quickstart/index.mdx b/archive/content/docs/(documentation)/quickstart/index.mdx deleted file mode 100644 index 3b3daed45..000000000 --- a/archive/content/docs/(documentation)/quickstart/index.mdx +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Running your first Sandbox -description: This guide will show you how to start your first E2B Sandbox. ---- - -## 1. Create E2B account - -Every new E2B account get $100 in credits. You can sign up [here](e2b.dev/sign-up). - - -## 2. Set your environment variables -1. Navigate to the [E2B Dashboard](/dashboard?tab=keys). -2. Copy your API key. -3. Paste your E2B API key into your `.env` file. -```bash .env -E2B_API_KEY=e2b_*** -``` - -## 3. Install E2B SDK -Install the E2B SDK to your project by running the following command in your terminal. - - -```bash tab -npm i @e2b/code-interpreter dotenv -``` - -```bash tab -pip install e2b-code-interpreter python-dotenv -``` - - - - -## 4. Write code for starting Sandbox -We'll write the minimal code for starting Sandbox, executing Python inside it and listing all files inside the root directory. - - -```ts tab title="index.js" -import 'dotenv/config' -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = await Sandbox.create() // By default the sandbox is alive for 5 minutes -const execution = await sbx.runCode('print("hello world")') // Execute Python inside the sandbox -console.log(execution.logs) - -const files = await sbx.files.list('/') -console.log(files) -``` - -```python tab title="main.py" -from dotenv import load_dotenv -load_dotenv() -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() # By default the sandbox is alive for 5 minutes -execution = sbx.run_code("print('hello world')") # Execute Python inside the sandbox -print(execution.logs) - -files = sbx.files.list("/") -print(files) -``` - - - - -## 5. Start your first E2B Sandbox -Run the code with the following command: - - -```bash tab -npx tsx ./index.ts -``` - -```bash tab -python main.py -``` - - - diff --git a/archive/content/docs/(documentation)/quickstart/install-custom-packages.mdx b/archive/content/docs/(documentation)/quickstart/install-custom-packages.mdx deleted file mode 100644 index 857307bb1..000000000 --- a/archive/content/docs/(documentation)/quickstart/install-custom-packages.mdx +++ /dev/null @@ -1,198 +0,0 @@ ---- -title: Install custom packages -description: Here you'll find two ways to install custom packages in the E2B Sandbox. ---- - -There are two ways to install custom packages in the E2B Sandbox. - -1. [Create custom sandbox with preinstalled packages](#create-a-custom-sandbox). -2. [Install packages during the sandbox runtime](#install-packages-during-the-sandbox-runtime). - ---- - -## Create a custom sandbox - -Use this option if you know beforehand what packages you need in the sandbox. - -Prerequisites: -- E2B CLI -- Docker running - - -Custom sandbox template is a Docker image that we automatically convert to a sandbox that you can then start with our SDK. - - -### 1. Install E2B CLI -Install the [E2B CLI](https://npmjs.com/package/@e2b/cli) globally on your machine with NPM. - - -```bash tab -npm i -g @e2b/cli -``` - - - -### 2. Login to E2B CLI -Before you can create a custom sandbox, you need to login to E2B CLI. - - -```bash tab -e2b auth login -``` - - - -### 2. Initialize a sandbox template - - -```bash tab -e2b template init -``` - - - -### 3. Specify the packages you need in `e2b.Dockerfile` -Edit the E2B Dockerfile to install the packages you need. - - -You need to use the `e2bdev/code-interpreter:latest` base image. - - - - -```dockerfile tab -FROM e2bdev/code-interpreter:latest - -RUN pip install cowsay -RUN npm install cowsay -``` - - - -### 4. Build the sandbox template -Run the following command to build the sandbox template. - - -```bash tab -e2b template build -c "/root/.jupyter/start-up.sh" -``` - - - -This will take a while, as it convert the Docker image to a sandbox which is a small VM. -At the end of the process you will see the sandbox ID like this: -``` -Running postprocessing. It can take up to few minutes. - -Postprocessing finished. - -✅ Building sandbox template YOUR_TEMPLATE_ID finished. -``` - -### 5. Start your custom sandbox -Now you can pass the template ID to the SDK to start your custom sandbox. - - -```js tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = Sandbox.create({ - template: 'YOUR_TEMPLATE_ID', -}) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox(template='YOUR_TEMPLATE_ID') -``` - - - ---- - -## Install packages during the sandbox runtime -Use this option if don't know beforehand what packages you need in the sandbox. You can install packages with the package manager of your choice. - - -The packages installed during the runtime are available only in the running sandbox instance. -When you start a new sandbox instance, the packages are not be available. - - -### 1. Install Python packages with PIP - - -```js tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = Sandbox.create() -sbx.commands.run('pip install cowsay') // This will install the cowsay package -sbx.runCode(` - import cowsay - cowsay.cow("Hello, world!") -`) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() -sbx.commands.run("pip install cowsay") // This will install the cowsay package -sbx.run_code(""" - import cowsay - cowsay.cow("Hello, world!") -""") -``` - - - -### 2. Install Node.js packages with NPM - - -```js tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = Sandbox.create() -sbx.commands.run('npm install cowsay') // This will install the cowsay package -sbx.runCode(` - const cowsay = require('cowsay') - console.log(cowsay.say({ text: 'Hello, world!' })) -`, { language: 'javascript' }) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() -sbx.commands.run("npm install cowsay") // This will install the cowsay package -sbx.run_code(""" - import { say } from 'cowsay' - console.log(say('Hello, world!')) -""", language="javascript") -``` - - - -### 3. Install packages with package manager of your choice -Since E2B Sandboxes are Debian based machines, you can use any package manager supported by Debian. -You just need to make sure that the package manager is already installed in the sandbox. - -For example, to install `curl` and `git`, you can use the following commands: - - - -```js tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = Sandbox.create() -await sbx.commands.run('apt-get update && apt-get install -y curl git') -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox() -sbx.commands.run("apt-get update && apt-get install -y curl git") -``` - - \ No newline at end of file diff --git a/archive/content/docs/(documentation)/quickstart/meta.json b/archive/content/docs/(documentation)/quickstart/meta.json deleted file mode 100644 index ef625cb13..000000000 --- a/archive/content/docs/(documentation)/quickstart/meta.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Quickstart", - "description": "This guide will show you how to start your first E2B Sandbox.", - "pages": ["index", "...", "!migrating-from-v0"] -} diff --git a/archive/content/docs/(documentation)/quickstart/migrating-from-v0.mdx b/archive/content/docs/(documentation)/quickstart/migrating-from-v0.mdx deleted file mode 100644 index 22ce60eee..000000000 --- a/archive/content/docs/(documentation)/quickstart/migrating-from-v0.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Migrating E2B SDK from v0.* to v1.0 using Grit ---- - -This guide explains how to migrate your project from **E2B SDK `v0.*`** to **E2B SDK `v1.0`** using [Grit](https://www.grit.io/) and our custom migrations. -This mostly automates the process of searching through your codebase to update code to migrate to the new SDK without errors. However, this is not a 100% accurate process, and you should expect human intervention to be required. - -## Step 1: Install the Grit CLI - -You can install **Grit CLI** using one of the following methods: - -### Option 1: Install via NPM - -```package-install -npm install --location=global @getgrit/cli -``` - -### Option 2: Install via bash script - -```bash -curl -fsSL https://docs.grit.io/install | bash -``` - -## Step 2: Prepare for migration - -Before applying the migration, format your code and commit your changes to ensure you have a stable state to revert to if necessary. - -```bash -git add . -git commit -m "Last changes made" -``` - -## Step 3: Run Grit with the Custom Pattern - -To apply the custom migration pattern, run the following command for your project. The `--interactive` flag allows you to review each change as it is made. - -### For JavaScript/TypeScript: - -```bash -grit apply github.com/e2b-dev/e2b-cookbook#e2b_v0_to_v1_js --interactive -``` - -### For Python: - -```bash -grit apply github.com/e2b-dev/e2b-cookbook#e2b_v0_to_v1_py --interactive -``` - -## Step 5: Review changes - -Once the migration is applied, review the changes for possible mistakes, including code that was broken (false positives) or outdated code that was not fixed (false negatives) by the migration. Such issues you may encounter include: - -- ⚠️ If non-E2B objects with `.close()` methods, `.id` attributes, etc. exist in files that import E2B libraries, they will be incorrectly changed. You need to reject these changes manually. -- ⚠️ The output structure of `Sandbox.list()` has changed in the new SDK. You must manually rewrite your code to account for this. -- ⚠️ The global `cwd` option no longer exists when creating a Sandbox. You must manually rewrite your code to include the `cwd` option with each command. - -You should manually review all changes made by Grit to fix these "gotchas" and others which are sure to exist. - -## Step 6: Commit changes - -Before commiting the changes made by Grit, it's recommended to re-format the generated code following the convention of your choice. After verifying the changes and reformatting code, commit them to Git. - -```bash -git add . -git commit -m "Migrate project to E2B SDK v1.0" -``` - -## Step 7: Test your application - -Now, thoroughly test your application to make sure the migration is successful and everything is functioning as expected. - ---- - -By following these steps, you can easily migrate your TypeScript or Python project from **E2B SDK `v0.*`** to **E2B SDK `v1.0`** using Grit. Don’t forget to use version control to track all changes throughout the process and to thoroughly test your application after migrating. diff --git a/archive/content/docs/(documentation)/quickstart/upload-download-files.mdx b/archive/content/docs/(documentation)/quickstart/upload-download-files.mdx deleted file mode 100644 index 953493c46..000000000 --- a/archive/content/docs/(documentation)/quickstart/upload-download-files.mdx +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: Upload & downloads files -description: E2B Sandbox allows you to upload and downloads file to and from the Sandbox. ---- - -An alternative way to get your data to the sandbox is to create a [custom sandbox template](/docs/sandbox-template). - -## Upload file - - -```ts tab -import { Sandbox } from '@e2b/code-interpreter' - -// Read local file -const content = fs.readFileSync('/local/file') - -const sbx = await Sandbox.create() -// Upload file to the sandbox to path '/home/user/my-file' -await sbx.files.write('/home/user/my-file', content) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox.create() - -# Read local file -with open("/local/file", "rb") as file: - # Upload file to the sandbox to path '/home/user/my-file' - sbx.files.write("/home/user/my-file", file) -``` - - - -## Upload multiple files -Currently, if you want to upload multiple files, you need to upload each one of the separately. -We're working on a better solution. - - - -```ts tab -import { Sandbox } from '@e2b/code-interpreter' - -// Read local files -const fileA = fs.readFileSync('/local/file/a') -const fileB = fs.readFileSync('/local/file/b') - -const sbx = await Sandbox.create() -// Upload file A to the sandbox to path '/home/user/my-file-a' -await sbx.files.write('/home/user/my-file-a', content) -// Upload file B to the sandbox to path '/home/user/my-file-b' -await sbx.files.write('/home/user/my-file-b', content) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox.create() - -# Read local file -with open("/local/file", "rb") as file: - # Upload file to the sandbox to path '/home/user/my-file' - sbx.files.write("/home/user/my-file", file) -``` - - - -## Upload directory -We currently don't support an easy way to upload a whole directory. -You need to upload each file separately. - -We're working on a better solution. - ---- - -## Download file -To download a file, you need to first get the file's content and then write it to a local file. - - - -```ts tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = await Sandbox.create() -// Download file from the sandbox to path '/home/user/my-file' -const content = await sbx.files.read('/home/user/my-file') -// Write file to local path -fs.writeFileSync('/local/file', content) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox.create() -# Download file from the sandbox to path '/home/user/my-file' -content = sbx.files.read('/home/user/my-file') -# Write file to local path -with open('/local/file', 'w') as file: - file.write(content) -``` - - - -## Download multiple files -To download multiple files, you need to download each one of them separately from the sandbox. - -We're working on a better solution. - - - -```ts tab -import { Sandbox } from '@e2b/code-interpreter' - -const sbx = await Sandbox.create() -// Download file A from the sandbox to path '/home/user/my-file' -const contentA = await sbx.files.read('/home/user/my-file-a') -// Write file A to local path -fs.writeFileSync('/local/file/a', contentA) - -// Download file B from the sandbox to path '/home/user/my-file' -const contentB = await sbx.files.read('/home/user/my-file-b') -// Write file B to local path -fs.writeFileSync('/local/file/b', contentB) -``` - -```python tab -from e2b_code_interpreter import Sandbox - -sbx = Sandbox.create() -# Download file A from the sandbox to path '/home/user/my-file-a' -contentA = sbx.files.read('/home/user/my-file-a') -# Write file A to local path -with open('/local/file/a', 'w') as file: - file.write(contentA) - -# Download file B from the sandbox to path '/home/user/my-file-b' -contentB = sbx.files.read('/home/user/my-file-b') -# Write file B to local path -with open('/local/file/b', 'w') as file: - file.write(contentB) -``` - - - -## Download directory -We currently don't support an easy way to download a whole directory. -You need to download each file separately. - -We're working on a better solution. diff --git a/archive/metadata.ts b/archive/metadata.ts deleted file mode 100644 index 7944b1cf5..000000000 --- a/archive/metadata.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Metadata } from 'next/types' -import { createMetadataImage } from 'fumadocs-core/server' -import { source } from './source' - -export const METADATA = { - title: 'E2B - Code Interpreting for AI apps', - description: 'Open-source secure sandboxes for AI code execution', -} - -export const metadataImage = createMetadataImage({ - source, - imageRoute: 'og', -}) - -export function createMetadata(override: Metadata): Metadata { - return { - ...override, - openGraph: { - title: override.title ?? undefined, - description: override.description ?? undefined, - url: 'https://fumadocs.vercel.app', - images: '/banner.png', - siteName: 'Fumadocs', - ...override.openGraph, - }, - twitter: { - card: 'summary_large_image', - creator: '@money_is_shark', - title: override.title ?? undefined, - description: override.description ?? undefined, - images: '/banner.png', - ...override.twitter, - }, - } -} diff --git a/archive/source.config.ts b/archive/source.config.ts deleted file mode 100644 index 12285dc34..000000000 --- a/archive/source.config.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - defineConfig, - defineDocs, - frontmatterSchema, - metaSchema, -} from 'fumadocs-mdx/config' - -export const docs = defineDocs({ - dir: 'src/content/docs', - docs: { schema: frontmatterSchema }, - meta: { schema: metaSchema }, -}) - -export default defineConfig({ - lastModifiedTime: 'git', -}) - -/* export default defineConfig({ - lastModifiedTime: 'git', - mdxOptions: { - rehypeCodeOptions: { - lazy: true, - experimentalJSEngine: true, - langs: ['ts', 'js', 'html', 'tsx', 'mdx'], - inline: 'tailing-curly-colon', - themes: { - light: 'github-light', - dark: 'github-dark', - }, - transformers: [ - ...(rehypeCodeDefaultOptions.transformers ?? []), - transformerTwoslash(), - { - name: 'transformers:remove-notation-escape', - code(hast) { - for (const line of hast.children) { - if (line.type !== 'element') continue - - const lastSpan = line.children.findLast( - (v) => v.type === 'element' - ) - - const head = lastSpan?.children[0] - if (head?.type !== 'text') return - - head.value = head.value.replace(/\[\\!code/g, '[!code') - } - }, - }, - ], - }, - remarkPlugins: [ - remarkMermaid, - remarkMath, - [remarkInstall, { persist: { id: 'package-manager' } }], - [remarkDocGen, { generators: [fileGenerator()] }], - remarkTypeScriptToJavaScript, - ], - rehypePlugins: (v) => [rehypeKatex, ...v], - }, -}) */ diff --git a/archive/source.ts b/archive/source.ts deleted file mode 100644 index 0afbb0515..000000000 --- a/archive/source.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { docs } from '@/../.source' -import { IconContainer } from '@/ui/icons' -import type { InferMetaType, InferPageType } from 'fumadocs-core/source' -import { loader } from 'fumadocs-core/source' -import { icons } from 'lucide-react' -import { createElement } from 'react' - -export const source = loader({ - baseUrl: '/docs', - icon(icon) { - if (icon && icon in icons) - return createElement(IconContainer, { - icon: icons[icon as keyof typeof icons], - }) - }, - source: docs.toFumadocsSource(), -}) - -export type Page = InferPageType -export type Meta = InferMetaType diff --git a/src/configs/fumadocs.ts b/src/configs/fumadocs.ts deleted file mode 100644 index 5d0dd8d66..000000000 --- a/src/configs/fumadocs.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { source } from '@/lib/source' -import { createMetadataImage } from 'fumadocs-core/server' -import type { Metadata } from 'next/types' - -export const metadataImage = createMetadataImage({ - source, - imageRoute: 'og', -}) - -export function createMetadata(override: Metadata): Metadata { - return { - ...override, - openGraph: { - title: override.title ?? undefined, - description: override.description ?? undefined, - url: 'https://fumadocs.vercel.app', - images: '/banner.png', - siteName: 'Fumadocs', - ...override.openGraph, - }, - twitter: { - card: 'summary_large_image', - creator: '@money_is_shark', - title: override.title ?? undefined, - description: override.description ?? undefined, - images: '/banner.png', - ...override.twitter, - }, - } -} diff --git a/src/lib/clients/api.ts b/src/lib/clients/api.ts index 4079ad160..18fff4992 100644 --- a/src/lib/clients/api.ts +++ b/src/lib/clients/api.ts @@ -8,7 +8,6 @@ export const infra = createClient({ headers, body, method, - // @ts-expect-error -- duplex not on type, keep it for now duplex: !!body ? 'half' : undefined, ...options, } as RequestInit) diff --git a/src/server/sandboxes/get-sandbox-root.ts b/src/server/sandboxes/get-sandbox-root.ts index 8cc730df0..deaf52ede 100644 --- a/src/server/sandboxes/get-sandbox-root.ts +++ b/src/server/sandboxes/get-sandbox-root.ts @@ -1,10 +1,9 @@ import { z } from 'zod' import { authActionClient } from '@/lib/clients/action' import { SUPABASE_AUTH_HEADERS } from '@/configs/api' -import { ERROR_CODES } from '@/configs/logs' -import { logError } from '@/lib/clients/logger' import { returnServerError } from '@/lib/utils/action' import Sandbox from 'e2b' +import { l } from '@/lib/clients/logger' export const GetSandboxRootSchema = z.object({ teamId: z.string().uuid(), @@ -31,7 +30,7 @@ export const getSandboxRoot = authActionClient entries: await sandbox.files.list(rootPath), } } catch (err) { - logError(ERROR_CODES.E2B_SDK, 'files.list', err) + l.error('get_sandbox_root:unexpected_error', err) return returnServerError('Failed to list root directory.') } }) From c5cc1a2b3b55fc539bcfc14d54f59a95cb50382b Mon Sep 17 00:00:00 2001 From: Ben Fornefeld <50748440+ben-fornefeld@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:22:56 +0200 Subject: [PATCH 69/75] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/utils/filesystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/filesystem.ts b/src/lib/utils/filesystem.ts index b8f64dab9..0ad7e5e88 100644 --- a/src/lib/utils/filesystem.ts +++ b/src/lib/utils/filesystem.ts @@ -67,7 +67,7 @@ export function joinPath(...segments: string[]): string { if (segments.length === 0) return '/' const joined = segments - .filter((segment) => segment !== '' && segment != null) + .filter((segment) => segment !== '' && segment !== null && segment !== undefined) .join('/') return normalizePath(joined) From 960e960b9ab882998167ac5f7aec6324586e82b8 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 30 Jul 2025 11:20:11 +0200 Subject: [PATCH 70/75] chore: remove optional Content Security Policy configurations from environment and Next.js setup --- .env.example | 9 --------- next.config.mjs | 24 ------------------------ src/lib/env.ts | 22 ---------------------- 3 files changed, 55 deletions(-) diff --git a/.env.example b/.env.example index 4318f8060..6078881cf 100644 --- a/.env.example +++ b/.env.example @@ -25,15 +25,6 @@ KV_REST_API_URL= NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key -### ================================= -### OPTIONAL CONTENT SECURITY POLICY -### ================================= -### Example: https://e2b.dev *.e2b.dev -CSP_SCRIPT_SRC= -CSP_STYLE_SRC= -CSP_IMG_SRC=https://avatars.githubusercontent.com https://lh3.googleusercontent.com -CSP_FRAME_SRC=https://vercel.live - ### ================================= ### OPTIONAL SERVER ENVIRONMENT VARIABLES ### ================================= diff --git a/next.config.mjs b/next.config.mjs index b7f3a4844..d243ba06c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,21 +1,6 @@ import { withSentryConfig } from '@sentry/nextjs' -const cspHeader = ` - default-src 'self'; - script-src 'self' 'unsafe-eval' 'unsafe-inline' ${process.env.CSP_SCRIPT_SRC}; - style-src 'self' 'unsafe-inline' ${process.env.CSP_STYLE_SRC}; - img-src 'self' data: ${process.env.NEXT_PUBLIC_SUPABASE_URL} ${process.env.CSP_IMG_SRC}; - frame-src 'self' ${process.env.CSP_FRAME_SRC}; - font-src 'self'; - object-src 'none'; - base-uri 'self'; - form-action 'self'; - frame-ancestors 'none'; - worker-src 'self' blob: ${process.env.CSP_SCRIPT_SRC}; - upgrade-insecure-requests; -` - /** @type {import('next').NextConfig} */ const config = { eslint: { @@ -51,15 +36,6 @@ const config = { }, ], }, - { - source: '/dashboard/(.*)', - headers: [ - { - key: 'Content-Security-Policy', - value: cspHeader.replace(/\n/g, ''), - }, - ], - }, ], rewrites: async () => [ { diff --git a/src/lib/env.ts b/src/lib/env.ts index 5143bf74c..05c49c88f 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -1,23 +1,5 @@ import { z } from 'zod' -const CSPSrcSchema = z.string().refine((domains) => - domains.split(' ').every((domain) => { - // CSP allows either: - // 1. Full URLs with scheme: https://example.com - // 2. Wildcard subdomains without scheme: *.example.com - // 3. Plain domains without scheme: example.com - const fullUrlPattern = /^https?:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i - const wildcardPattern = /^\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i - const plainDomainPattern = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i - - return ( - fullUrlPattern.test(domain) || - wildcardPattern.test(domain) || - plainDomainPattern.test(domain) - ) - }) -) - export const serverSchema = z.object({ SUPABASE_SERVICE_ROLE_KEY: z.string().min(1), INFRA_API_URL: z.string().url(), @@ -28,10 +10,6 @@ export const serverSchema = z.object({ BILLING_API_URL: z.string().url().optional(), OTEL_SERVICE_NAME: z.string().optional(), ZEROBOUNCE_API_KEY: z.string().optional(), - CSP_SCRIPT_SRC: CSPSrcSchema.optional(), - CSP_STYLE_SRC: CSPSrcSchema.optional(), - CSP_IMG_SRC: CSPSrcSchema.optional(), - CSP_FRAME_SRC: CSPSrcSchema.optional(), VERCEL_ENV: z.enum(['production', 'preview', 'development']).optional(), VERCEL_URL: z.string().optional(), VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(), From 392c71d5849aaba205cd487c553314940583e734 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 30 Jul 2025 12:37:15 +0200 Subject: [PATCH 71/75] chore: add 'pathe' dependency and refactor path utility functions for improved normalization and handling --- bun.lock | 1 + package.json | 1 + .../dashboard/sandbox/inspect/context.tsx | 8 +- .../sandbox/inspect/sandbox-manager.ts | 78 ++---------- src/lib/utils/filesystem.ts | 117 ++++++------------ 5 files changed, 47 insertions(+), 158 deletions(-) diff --git a/bun.lock b/bun.lock index e43d1c5d9..dcc21399f 100644 --- a/bun.lock +++ b/bun.lock @@ -61,6 +61,7 @@ "next-safe-action": "^7.10.4", "next-themes": "^0.4.4", "openapi-fetch": "^0.14.0", + "pathe": "^2.0.3", "pino": "^9.7.0", "postgres": "^3.4.5", "posthog-js": "^1.214.0", diff --git a/package.json b/package.json index ada930ab6..08c6c4b5e 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "next-safe-action": "^7.10.4", "next-themes": "^0.4.4", "openapi-fetch": "^0.14.0", + "pathe": "^2.0.3", "pino": "^9.7.0", "postgres": "^3.4.5", "posthog-js": "^1.214.0", diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 95eaa6853..6b1ffcb22 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -48,10 +48,6 @@ export function SandboxInspectProvider({ const router = useRouter() - const sandboxId = useMemo(() => { - return sandboxInfo.sandboxID + '-' + sandboxInfo.clientID - }, [sandboxInfo.sandboxID, sandboxInfo.clientID]) - /* * ---------- synchronous store initialisation ---------- * We want the tree to render immediately using the "seedEntries" streamed from the @@ -195,7 +191,7 @@ export function SandboxInspectProvider({ return } - const sandbox = await Sandbox.connect(sandboxId, { + const sandbox = await Sandbox.connect(sandboxInfo.sandboxID, { domain: process.env.NEXT_PUBLIC_E2B_DOMAIN, headers: { ...SUPABASE_AUTH_HEADERS(data.session?.access_token, teamId), @@ -214,7 +210,7 @@ export function SandboxInspectProvider({ return () => { sandboxManagerRef.current?.stopWatching() } - }, [sandboxId, teamId, rootPath, router]) + }, [sandboxInfo.sandboxID, teamId, rootPath, router]) if (!storeRef.current || !operationsRef.current) { return null // should never happen, but satisfies type-checker diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index c1ca21528..0de2b6f2c 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -11,6 +11,11 @@ import { FilesystemNode } from './filesystem/types' import { normalizePath, joinPath, getParentPath } from '@/lib/utils/filesystem' import { determineFileContentState } from '@/lib/utils/filesystem' +export const HANDLED_ERRORS = { + 'signal timed out': 'The operation timed out. Please try again later.', + 'user aborted a request': 'The request was cancelled. Try downloading the file.', +} as const + export class SandboxManager { private watchHandle?: WatchHandle private readonly rootPath: string @@ -20,22 +25,6 @@ export class SandboxManager { private static readonly LOAD_DEBOUNCE_MS = 250 private static readonly READ_DEBOUNCE_MS = 250 - /** - * Mapping from substrings found in error messages to user-friendly messages. - * Extend this map whenever new error patterns need custom handling. - */ - private static readonly errorMap: Record = { - 'signal timed out': 'The operation timed out. Please try again later.', - 'user aborted a request': - 'The request was cancelled. Try downloading the file.', - } - - // Detect error substrings that imply the requested path is actually a directory - private static readonly dirErrorHints: string[] = [ - 'eisdir', - 'is a directory', - 'illegal operation on a directory', - ] private loadTimers: Map> = new Map() private pendingLoads: Map< @@ -316,60 +305,7 @@ export class SandboxManager { const contentState = await determineFileContentState(blob) state.setFileContent(normalizedPath, contentState) - } catch (err) { - // ──────────────────────────────────────────────────────────────── - // Handle the special case where the SDK mis-classifies a symlink - // to a directory as FileType.FILE. The read() call then throws - // an EISDIR / "is a directory" error. We intercept that, convert - // the node to a directory, load its children and *skip* setting an - // error state so the UI does not flicker. - // ──────────────────────────────────────────────────────────────── - - const rawMessage = - err instanceof Error - ? err.message.toLowerCase() - : String(err).toLowerCase() - - const looksLikeDirectory = SandboxManager.dirErrorHints.some((hint) => - rawMessage.includes(hint) - ) - - if (looksLikeDirectory) { - try { - const entries = await this.sandbox.files.list(normalizedPath) - - state.updateNode(normalizedPath, { - type: FileType.DIR, - isExpanded: true, - children: [], - }) - - state.setError(normalizedPath) - - const nodes: FilesystemNode[] = entries.map((entry: EntryInfo) => - entry.type === FileType.DIR - ? { - name: entry.name, - path: entry.path, - type: FileType.DIR, - isExpanded: false, - isSelected: false, - children: [], - } - : { - name: entry.name, - path: entry.path, - type: FileType.FILE, - isSelected: false, - } - ) - - state.addNodes(normalizedPath, nodes) - - return - } catch {} - } - + } catch (err) { const errorMessage = SandboxManager.pipeError(err, 'Failed to read file') console.error(`Failed to read file ${normalizedPath}:`, err) @@ -432,7 +368,7 @@ export class SandboxManager { const lowerOriginal = originalMessage.toLowerCase() - for (const [search, msg] of Object.entries(SandboxManager.errorMap)) { + for (const [search, msg] of Object.entries(HANDLED_ERRORS)) { if (lowerOriginal.includes(search.toLowerCase())) { return msg } diff --git a/src/lib/utils/filesystem.ts b/src/lib/utils/filesystem.ts index 0ad7e5e88..39b63e03a 100644 --- a/src/lib/utils/filesystem.ts +++ b/src/lib/utils/filesystem.ts @@ -1,111 +1,66 @@ import { FileContentState } from '@/features/dashboard/sandbox/inspect/filesystem/store' +// Leverage pathe (a tiny, browser-friendly path replacement) +import { normalize, dirname, basename, join } from 'pathe' + /** - * Normalize a path by removing duplicate slashes and resolving . and .. segments + * Normalize a path so that it: + * • always starts with "/" (root-relative) + * • has duplicate slashes removed + * • resolves . and .. segments */ export function normalizePath(path: string): string { - // Handle empty path - if (!path || path === '') return '/' - - // Ensure path starts with / - if (!path.startsWith('/')) { - path = '/' + path - } - - // Split path into segments - const segments = path - .split('/') - .filter((segment) => segment !== '' && segment !== '.') - const normalized: string[] = [] - - for (const segment of segments) { - if (segment === '..') { - // Pop the last segment if we have one (don't go above root) - if (normalized.length > 0) { - normalized.pop() - } - } else { - normalized.push(segment) - } - } - - // Join segments back together - const result = '/' + normalized.join('/') - - // Ensure we don't return empty string, always at least '/' - return result === '' ? '/' : result + if (!path) return '/' + const normalized = normalize(path) + return normalized.startsWith('/') ? normalized : `/${normalized}` } -/** - * Get the parent directory of a path - */ +/** Get the parent directory of a path */ export function getParentPath(path: string): string { - const normalized = normalizePath(path) - if (normalized === '/') return '/' - - const lastSlashIndex = normalized.lastIndexOf('/') - if (lastSlashIndex === 0) return '/' - - return normalized.substring(0, lastSlashIndex) + const norm = normalizePath(path) + return norm === '/' ? '/' : dirname(norm) || '/' } -/** - * Get the basename (filename) of a path - */ +/** Get the basename (filename) of a path */ export function getBasename(path: string): string { - const normalized = normalizePath(path) - if (normalized === '/') return '/' - - const lastSlashIndex = normalized.lastIndexOf('/') - return normalized.substring(lastSlashIndex + 1) + const norm = normalizePath(path) + return norm === '/' ? '/' : basename(norm) } -/** - * Join path segments together - */ -export function joinPath(...segments: string[]): string { +/** Join path segments together */ +export function joinPath(...segments: (string | null | undefined)[]): string { if (segments.length === 0) return '/' - - const joined = segments - .filter((segment) => segment !== '' && segment !== null && segment !== undefined) - .join('/') - - return normalizePath(joined) + const filtered = segments.filter( + (s): s is string => s !== '' && s !== null && s !== undefined + ) + return normalizePath(join(...filtered)) } -/** - * Check if a path is a child of another path - */ +/** Check if a path is a strict child of another path */ export function isChildPath(parentPath: string, childPath: string): boolean { - const normalizedParent = normalizePath(parentPath) - const normalizedChild = normalizePath(childPath) - - if (normalizedParent === normalizedChild) return false + const parent = normalizePath(parentPath) + const child = normalizePath(childPath) + if (parent === child) return false - // Ensure parent ends with / for proper comparison - const parentWithSlash = - normalizedParent === '/' ? '/' : normalizedParent + '/' - - return normalizedChild.startsWith(parentWithSlash) + const parentWithSlash = parent === '/' ? '/' : `${parent}/` + return child.startsWith(parentWithSlash) } -/** - * Get the depth of a path (number of directory levels) - */ +/** Get the depth of a path (number of directory levels) */ export function getPathDepth(path: string): number { - const normalized = normalizePath(path) - if (normalized === '/') return 0 - - return normalized.split('/').length - 1 + const norm = normalizePath(path) + return norm === '/' ? 0 : norm.split('/').length - 1 } -/** - * Check if a path is the root path - */ +/** Check if a path is the root path */ export function isRootPath(path: string): boolean { return normalizePath(path) === '/' } +// --------------------------------------------------------------------------- +// Binary/text blob helpers (unchanged) +// --------------------------------------------------------------------------- + export async function determineFileContentState( blob: Blob ): Promise { From b0732b9f5dfa2d2a0c369859dd8403bd93ae72d8 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 30 Jul 2025 13:19:14 +0200 Subject: [PATCH 72/75] feat: enhance SandboxManager to support secure sandbox environments with new constructor parameter --- src/features/dashboard/sandbox/inspect/context.tsx | 3 ++- .../dashboard/sandbox/inspect/sandbox-manager.ts | 9 +++++++-- src/lib/utils/filesystem.ts | 6 ++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index 6b1ffcb22..d899d9fcf 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -201,7 +201,8 @@ export function SandboxInspectProvider({ sandboxManagerRef.current = new SandboxManager( storeRef.current, sandbox, - rootPath + rootPath, + sandboxInfo.envdAccessToken !== undefined ) } diff --git a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts index 0de2b6f2c..f26697289 100644 --- a/src/features/dashboard/sandbox/inspect/sandbox-manager.ts +++ b/src/features/dashboard/sandbox/inspect/sandbox-manager.ts @@ -21,6 +21,7 @@ export class SandboxManager { private readonly rootPath: string private store: FilesystemStore private sandbox: Sandbox + private readonly isSandboxSecure: boolean = false private static readonly LOAD_DEBOUNCE_MS = 250 private static readonly READ_DEBOUNCE_MS = 250 @@ -46,10 +47,11 @@ export class SandboxManager { } > = new Map() - constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string) { + constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string, isSandboxSecure: boolean) { this.store = store this.sandbox = sandbox this.rootPath = normalizePath(rootPath) + this.isSandboxSecure = isSandboxSecure // immediately start a single recursive watcher at the root void this.startRootWatcher() @@ -332,7 +334,10 @@ export class SandboxManager { return '' } - const downloadUrl = await this.sandbox.downloadUrl(normalizedPath) + const downloadUrl = await this.sandbox.downloadUrl(normalizedPath, { + user: 'root', + useSignature: this.isSandboxSecure || undefined, + }) console.log('downloadUrl', downloadUrl) diff --git a/src/lib/utils/filesystem.ts b/src/lib/utils/filesystem.ts index 39b63e03a..af7717d8d 100644 --- a/src/lib/utils/filesystem.ts +++ b/src/lib/utils/filesystem.ts @@ -11,19 +11,21 @@ import { normalize, dirname, basename, join } from 'pathe' */ export function normalizePath(path: string): string { if (!path) return '/' - const normalized = normalize(path) - return normalized.startsWith('/') ? normalized : `/${normalized}` + + return normalize(path) } /** Get the parent directory of a path */ export function getParentPath(path: string): string { const norm = normalizePath(path) + return norm === '/' ? '/' : dirname(norm) || '/' } /** Get the basename (filename) of a path */ export function getBasename(path: string): string { const norm = normalizePath(path) + return norm === '/' ? '/' : basename(norm) } From cde833bd716454182b3e0076af4006c418ab7e0c Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 30 Jul 2025 13:19:29 +0200 Subject: [PATCH 73/75] feat: enhance SandboxManager to support secure sandbox environments with new constructor parameter --- bun.lock | 4 ++-- package.json | 2 +- src/features/dashboard/sandbox/context.tsx | 6 +++--- src/types/api.d.ts | 3 +++ 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bun.lock b/bun.lock index dcc21399f..0f6d2906a 100644 --- a/bun.lock +++ b/bun.lock @@ -46,7 +46,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", - "e2b": "^1.7.1", + "e2b": "^1.10.0", "fast-xml-parser": "^4.5.1", "fumadocs-core": "^15.0.6", "fumadocs-mdx": "^11.5.3", @@ -1499,7 +1499,7 @@ "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], - "e2b": ["e2b@1.9.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "compare-versions": "^6.1.0", "openapi-fetch": "^0.9.7", "platform": "^1.3.6" } }, "sha512-MM3RhWW7YENYocTy20BvKVcn8li/FxkDrHINS7tmz00ffl1ZavQTRxCI9Sl8ofeRg+HVMlqO4W8LJ+ij9VTZPg=="], + "e2b": ["e2b@1.10.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.6.2", "@connectrpc/connect": "2.0.0-rc.3", "@connectrpc/connect-web": "2.0.0-rc.3", "compare-versions": "^6.1.0", "openapi-fetch": "^0.9.7", "platform": "^1.3.6" } }, "sha512-m0lt8hTQ84M7tUjF2Dw7oNwfMcc8EyCHJtA1vX6Sv3OO2OtjPdCky854XWY+UejDK+q3m5vuSpSgLgeE0rJ7LA=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], diff --git a/package.json b/package.json index 08c6c4b5e..411769730 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", - "e2b": "^1.7.1", + "e2b": "^1.10.0", "fast-xml-parser": "^4.5.1", "fumadocs-core": "^15.0.6", "fumadocs-mdx": "^11.5.3", diff --git a/src/features/dashboard/sandbox/context.tsx b/src/features/dashboard/sandbox/context.tsx index 13a13d0b6..9cf82e17e 100644 --- a/src/features/dashboard/sandbox/context.tsx +++ b/src/features/dashboard/sandbox/context.tsx @@ -1,10 +1,10 @@ 'use client' import React, { createContext, useContext, ReactNode } from 'react' -import { Sandbox } from '@/types/api' +import { SandboxInfo } from '@/types/api' interface SandboxContextValue { - sandboxInfo: Sandbox + sandboxInfo: SandboxInfo } const SandboxContext = createContext(null) @@ -19,7 +19,7 @@ export function useSandboxContext() { interface SandboxProviderProps { children: ReactNode - sandboxInfo: Sandbox + sandboxInfo: SandboxInfo } export function SandboxProvider({ diff --git a/src/types/api.d.ts b/src/types/api.d.ts index 9319a504a..a2cbb8fd5 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -2,6 +2,8 @@ import { components as InfraComponents } from '@/types/infra-api' type Sandbox = InfraComponents['schemas']['ListedSandbox'] +type SandboxInfo = InfraComponents['schemas']['SandboxDetail'] + type Sandboxes = InfraComponents['schemas']['ListedSandbox'][] type SandboxMetric = InfraComponents['schemas']['SandboxMetric'] @@ -33,6 +35,7 @@ export type { DefaultTemplate, IdentifierMaskingDetails, Sandbox, + SandboxInfo, Sandboxes, SandboxesMetricsRecord, SandboxMetric, From b73c04d19e1abac7a838f374a9e0d83c9611bfe2 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 30 Jul 2025 13:49:53 +0200 Subject: [PATCH 74/75] refactor: simplify setSelected method in SandboxInspectProvider and update FilesystemMutations interface to accept optional path --- src/features/dashboard/sandbox/inspect/context.tsx | 4 +--- src/features/dashboard/sandbox/inspect/filesystem/store.ts | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/context.tsx b/src/features/dashboard/sandbox/inspect/context.tsx index d899d9fcf..ff61e6024 100644 --- a/src/features/dashboard/sandbox/inspect/context.tsx +++ b/src/features/dashboard/sandbox/inspect/context.tsx @@ -130,9 +130,7 @@ export function SandboxInspectProvider({ store.getState().setSelected(path) }, resetSelected: () => { - store.setState((state) => { - state.selectedPath = undefined - }) + store.getState().setSelected(undefined) }, toggleDirectory: async (path: string) => { const normalizedPath = normalizePath(path) diff --git a/src/features/dashboard/sandbox/inspect/filesystem/store.ts b/src/features/dashboard/sandbox/inspect/filesystem/store.ts index 2ca434d69..2f3719578 100644 --- a/src/features/dashboard/sandbox/inspect/filesystem/store.ts +++ b/src/features/dashboard/sandbox/inspect/filesystem/store.ts @@ -56,7 +56,7 @@ export interface FilesystemMutations { removeNode: (path: string) => void updateNode: (path: string, updates: Partial) => void setExpanded: (path: string, expanded: boolean) => void - setSelected: (path: string) => void + setSelected: (path?: string) => void setLoading: (path: string, loading: boolean) => void setLoaded: (path: string, loaded: boolean) => void setError: (path: string, error?: string) => void @@ -234,8 +234,8 @@ export const createFilesystemStore = (rootPath: string) => }) }, - setSelected: (path: string) => { - const normalizedPath = normalizePath(path) + setSelected: (path) => { + const normalizedPath = path ? normalizePath(path) : undefined set((state: FilesystemState) => { state.selectedPath = normalizedPath From c6f88eab05f1900d51fa03480cb5c6a07be5f396 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 30 Jul 2025 16:15:50 +0200 Subject: [PATCH 75/75] chore: update tsconfig.json to remove 'archive' from excluded directories --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 61e986b17..525414d80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ "isolatedModules": true }, "include": ["next-env.d.ts", "src", ".next/types/**/*.ts"], - "exclude": ["node_modules", "archive"] + "exclude": ["node_modules"] }