diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f8d9cf..8cc742b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -529,6 +529,12 @@ importers: react-dom: specifier: 19.2.8 version: 19.2.8(react@19.2.8) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + shiki: + specifier: ^4.4.3 + version: 4.4.3 dependenciesMeta: '@primer/agent-eval': injected: true @@ -1328,6 +1334,37 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@shikijs/core@4.4.3': + resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.4.3': + resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.4.3': + resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} + engines: {node: '>=20'} + + '@shikijs/langs@4.4.3': + resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.4.3': + resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} + engines: {node: '>=20'} + + '@shikijs/themes@4.4.3': + resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} + engines: {node: '>=20'} + + '@shikijs/types@4.4.3': + resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1483,12 +1520,18 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} @@ -1512,6 +1555,9 @@ packages: '@types/tar-stream@3.1.4': resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.65.0': resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1630,6 +1676,9 @@ packages: resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} + '@vitest/expect@4.1.11': resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} @@ -1995,10 +2044,19 @@ packages: caniuse-lite@1.0.30001809: resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -2026,6 +2084,9 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2093,10 +2154,17 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + docker-modem@5.0.7: resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} engines: {node: '>= 8.0'} @@ -2507,6 +2575,12 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -2516,6 +2590,9 @@ packages: hsluv@1.0.1: resolution: {integrity: sha512-zCaFTiDqBLQjCCFBu0qg7z9ASYPd+Bxx2GDCVZJsnehjK80S+jByqhuFz0pCd2Aw3FSKr18AWbRlwnKR0YdizQ==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + human-id@4.2.0: resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true @@ -2899,9 +2976,27 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + memfs@4.68.1: resolution: {integrity: sha512-OD+IDRUvIxu3QHL+nFm9gdyugInD27FDJ+sl4B5QgomPHXMlbw+GP918P8VNKu2FkNlVeqBkpzkwROpamVifRw==} + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -3010,6 +3105,12 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3126,6 +3227,9 @@ packages: process-warning@5.1.0: resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + protobufjs@7.6.5: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} @@ -3186,6 +3290,15 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -3276,6 +3389,9 @@ packages: engines: {node: '>=10'} hasBin: true + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3309,6 +3425,10 @@ packages: resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} + shiki@4.4.3: + resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} + engines: {node: '>=20'} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -3338,6 +3458,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split-ca@1.0.1: resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} @@ -3385,6 +3508,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3482,6 +3608,9 @@ packages: peerDependencies: tslib: '2' + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -3543,6 +3672,21 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -3555,6 +3699,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.2.2: resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3709,6 +3859,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -4379,6 +4532,46 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@shikijs/core@4.4.3': + dependencies: + '@shikijs/primitive': 4.4.3 + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + + '@shikijs/primitive@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + + '@shikijs/types@4.4.3': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.23': @@ -4502,10 +4695,18 @@ snapshots: '@types/estree@1.0.9': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/node@18.19.130': dependencies: undici-types: 5.26.5 @@ -4535,6 +4736,8 @@ snapshots: dependencies: '@types/node': 26.4.0 + '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4717,6 +4920,8 @@ snapshots: '@typescript-eslint/types': 8.67.0 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.4.0': {} + '@vitest/expect@4.1.11': dependencies: '@standard-schema/spec': 1.1.0 @@ -5030,8 +5235,14 @@ snapshots: caniuse-lite@1.0.30001809: {} + ccount@2.0.1: {} + chai@6.2.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + chownr@1.1.4: {} client-only@0.0.1: {} @@ -5054,6 +5265,8 @@ snapshots: colorette@2.0.20: {} + comma-separated-tokens@2.0.3: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -5118,8 +5331,14 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + docker-modem@5.0.7: dependencies: debug: 4.4.3 @@ -5646,6 +5865,24 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + help-me@5.0.0: {} history@5.3.0: @@ -5654,6 +5891,8 @@ snapshots: hsluv@1.0.1: {} + html-void-elements@3.0.0: {} + human-id@4.2.0: {} hyperdyperid@1.2.0: {} @@ -5970,6 +6209,18 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.4.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + memfs@4.68.1: dependencies: '@jsonjoy.com/fs-core': 4.68.1(tslib@2.8.1) @@ -5987,6 +6238,23 @@ snapshots: tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + minimatch@10.2.6: dependencies: brace-expansion: 5.0.9 @@ -6097,6 +6365,14 @@ snapshots: dependencies: wrappy: 1.0.2 + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6223,6 +6499,8 @@ snapshots: process-warning@5.1.0: {} + property-information@7.2.0: {} + protobufjs@7.6.5: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -6293,6 +6571,16 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.9 @@ -6394,6 +6682,8 @@ snapshots: semver@7.8.5: {} + server-only@0.0.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -6458,6 +6748,17 @@ snapshots: shell-quote@1.10.0: {} + shiki@4.4.3: + dependencies: + '@shikijs/core': 4.4.3 + '@shikijs/engine-javascript': 4.4.3 + '@shikijs/engine-oniguruma': 4.4.3 + '@shikijs/langs': 4.4.3 + '@shikijs/themes': 4.4.3 + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -6496,6 +6797,8 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + split-ca@1.0.1: {} split2@4.2.0: {} @@ -6566,6 +6869,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -6667,6 +6975,8 @@ snapshots: dependencies: tslib: 2.8.1 + trim-lines@3.0.1: {} + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 @@ -6752,6 +7062,29 @@ snapshots: undici-types@8.3.0: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: browserslist: 4.28.7 @@ -6764,6 +7097,16 @@ snapshots: util-deprecate@1.0.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@8.2.2(@types/node@26.4.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 @@ -6922,3 +7265,5 @@ snapshots: '@yuku-parser/binding-win32-x64': 0.8.7 zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/website/README.md b/website/README.md index e8a13ae..907d896 100644 --- a/website/README.md +++ b/website/README.md @@ -36,3 +36,24 @@ model and treatment. Each judge shows its score, scoring criteria, rationale, and file-backed findings with code snippets. Scores use the judge's configured scale, not a shared pass/fail threshold. Judge errors and missing results are shown separately from scored results. + +Each trial's **Code** tab shows the saved `artifacts.workspaceDirectory` as an +expandable file tree with read-only UTF-8 text previews. This is the final saved +workspace, including starter files, rather than a diff of the agent's changes. +Workspaces are read at build time, so the explorer also works in the static export. +Missing workspaces have an unavailable message. + +Recognized file types use Shiki syntax highlighting with GitHub light and dark +themes that follow the website's color mode. Unknown file types remain plain text. +Previews are highlighted on the server and exported as individual JSON assets. +The explorer fetches a preview only when its file is selected, keeping file contents, +tokens, Shiki, and language grammars out of the initial results page. Highlighting +runs at build time for the static export, or on request during local development. +The browser renders the returned tokens as escaped text, not generated HTML. + +Dependency, build, and Git directories (`node_modules`, `.next`, `.turbo`, `dist`, +and `.git`) are omitted. Symbolic links and binary files cannot be previewed. +Previews are limited to 256 KiB per file and 2 MiB per workspace; the tree is limited +to 2,000 entries and 50 directory levels. Limits are indicated in the explorer. +Review workspace contents before publishing a result bundle, as previewable files +are included in the website. diff --git a/website/package.json b/website/package.json index 64c44ac..7298098 100644 --- a/website/package.json +++ b/website/package.json @@ -16,7 +16,9 @@ "@primer/tailwind-config": "^1.0.0", "next": "^16.3.1", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "server-only": "^0.0.1", + "shiki": "^4.4.3" }, "dependenciesMeta": { "@primer/agent-eval": { diff --git a/website/src/app/components/FileExplorer.module.css b/website/src/app/components/FileExplorer.module.css new file mode 100644 index 0000000..3ec3c77 --- /dev/null +++ b/website/src/app/components/FileExplorer.module.css @@ -0,0 +1,72 @@ +.explorer { + display: grid; + grid-template-columns: minmax(180px, 280px) minmax(0, 1fr); + border: 1px solid var(--borderColor-default); + border-radius: var(--borderRadius-medium); + overflow: hidden; +} + +.tree { + max-height: 600px; + overflow: auto; + padding: var(--base-size-8); + border-right: 1px solid var(--borderColor-default); +} + +.preview { + min-width: 0; +} + +.header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--base-size-16); + padding: var(--base-size-12); + background: var(--bgColor-muted); + border-bottom: 1px solid var(--borderColor-default); +} + +.code { + margin: 0; + padding: var(--base-size-16); + max-height: 550px; + overflow: auto; + font-size: var(--text-body-size-medium); + tab-size: 2; +} + +.code span { + color: var(--shiki-light); + font-style: var(--shiki-light-font-style); + font-weight: var(--shiki-light-font-weight); + text-decoration: var(--shiki-light-text-decoration); +} + +:global([data-color-mode='dark']) .code span { + color: var(--shiki-dark); + font-style: var(--shiki-dark-font-style); + font-weight: var(--shiki-dark-font-weight); + text-decoration: var(--shiki-dark-text-decoration); +} + +@media (prefers-color-scheme: dark) { + :global([data-color-mode='auto']) .code span { + color: var(--shiki-dark); + font-style: var(--shiki-dark-font-style); + font-weight: var(--shiki-dark-font-weight); + text-decoration: var(--shiki-dark-text-decoration); + } +} + +@media (max-width: 767px) { + .explorer { + grid-template-columns: minmax(0, 1fr); + } + + .tree { + max-height: 240px; + border-right: 0; + border-bottom: 1px solid var(--borderColor-default); + } +} diff --git a/website/src/app/components/FileExplorer.tsx b/website/src/app/components/FileExplorer.tsx new file mode 100644 index 0000000..102a973 --- /dev/null +++ b/website/src/app/components/FileExplorer.tsx @@ -0,0 +1,82 @@ +'use client' + +import {FileIcon} from '@primer/octicons-react' +import {TreeView} from '@primer/react' +import {useId, useState} from 'react' +import type {FilePreviewReference} from '../../file-preview' +import type {WorkspaceEntry, WorkspaceFile, WorkspaceFiles} from '../../workspace-files' +import {FilePreview} from './FilePreview' +import styles from './FileExplorer.module.css' + +function FileExplorer({workspace}: {workspace: WorkspaceFiles}) { + const [selectedFile, setSelectedFile] = useState | null>(null) + const id = useId() + + if (workspace.type === 'unavailable') { + return

{workspace.reason}

+ } + + function renderEntry(entry: WorkspaceEntry) { + if (entry.type === 'directory') { + return ( + + + + + {entry.name} + {entry.children.map(renderEntry)} + + ) + } + + return ( + { + setSelectedFile(entry) + }} + > + + + + {entry.name} + + ) + } + + if (workspace.entries.length === 0) { + return

No files are available in the generated workspace.

+ } + + return ( +
+ {workspace.truncated ? ( +

The file tree is limited to 2,000 entries and 50 directory levels.

+ ) : null} +
+
+ {workspace.entries.map(renderEntry)} +
+
+ {selectedFile ? ( + <> +
+

{selectedFile.path}

+ + {selectedFile.size.toLocaleString('en-US')} bytes + +
+ + + ) : ( +

Select a file to view its contents.

+ )} +
+
+
+ ) +} + +export {FileExplorer} diff --git a/website/src/app/components/FilePreview.test.tsx b/website/src/app/components/FilePreview.test.tsx new file mode 100644 index 0000000..e0e3d06 --- /dev/null +++ b/website/src/app/components/FilePreview.test.tsx @@ -0,0 +1,127 @@ +import {renderToStaticMarkup} from 'react-dom/server' +import {expect, test, vi} from 'vitest' +import {getFileLanguage, highlightFile} from '../../file-highlighting' +import {isFilePreviewData} from '../../file-preview' +import type {WorkspaceFile} from '../../workspace-files' +import {FilePreview, FilePreviewContent} from './FilePreview' + +vi.mock('server-only', () => { + return {} +}) +vi.mock('@primer/react', () => { + return {Button: 'button'} +}) + +function file(filepath: string, content: string): WorkspaceFile { + return { + type: 'file', + path: filepath, + name: filepath.split('/').at(-1) ?? filepath, + size: Buffer.byteLength(content), + preview: {type: 'text', content}, + } +} + +async function renderFile(workspaceFile: WorkspaceFile) { + const preview = await highlightFile(workspaceFile) + expect(isFilePreviewData(JSON.parse(JSON.stringify(preview)))).toBe(true) + return renderToStaticMarkup() +} + +test.each([ + ['src/App.tsx', 'tsx'], + ['src/index.ts', 'typescript'], + ['src/types.d.ts', 'typescript'], + ['src/index.mts', 'typescript'], + ['src/App.jsx', 'jsx'], + ['next.config.mjs', 'javascript'], + ['index.cjs', 'javascript'], + ['package.json', 'json'], + ['styles.CSS', 'css'], + ['README.md', 'markdown'], + ['workflow.yml', 'yaml'], + ['script.py', 'python'], + ['script.sh', 'shellscript'], + ['Dockerfile', 'docker'], + ['Dockerfile.dev', 'docker'], + ['Makefile', 'make'], + ['.env.local', 'dotenv'], + ['LICENSE', 'text'], + ['file.unknown', 'text'], +])('detects the language for %s', (filepath, language) => { + expect(getFileLanguage(filepath)).toBe(language) +}) + +test.each([ + ['src/App.tsx', 'export default function App() {\n return \n}\n'], + ['package.json', '{\n "name": "generated-app"\n}\n'], + ['styles.css', 'button {\n color: red;\n}\n'], +])('renders server-highlighted %s tokens with both themes', async (filepath, content) => { + const html = await renderFile(file(filepath, content)) + expect(html).toContain('--shiki-light:') + expect(html).toContain('--shiki-dark:') + expect(html).toContain(`aria-label="${filepath}"`) + expect(html).toContain('tabindex="0"') + expect(html.replace(/<[^>]*>/g, '')).toBe(renderToStaticMarkup(<>{content})) +}) + +test.each([ + '\n\n\tconst greeting = "hello"\n\n', + '\r\n\tconst greeting = "hello"\r\n\r\n', + '\nconst first = 1\r\nconst second = 2\n', + 'const greeting = "hello"', + 'const greeting = "こんにちは 👋"\n', +])('preserves source text, whitespace, and line endings: %j', async content => { + const html = await renderFile(file('index.ts', content)) + expect(html.replace(/<[^>]*>/g, '')).toBe(renderToStaticMarkup(<>{content})) +}) + +test('escapes file contents rather than rendering generated HTML', async () => { + const content = '' + const html = await renderFile(file('index.html', content)) + expect(html).not.toContain('\nplain text\n' + const html = await renderFile(file('notes.unknown', content)) + expect(html).not.toContain(']*>/g, '')).toBe(renderToStaticMarkup(<>{content})) +}) + +test('preserves empty and unavailable preview messages', async () => { + const empty = await renderFile(file('empty.ts', '')) + expect(empty).toContain('This file is empty.') + const unavailable = await renderFile({ + ...file('binary.png', ''), + preview: {type: 'unavailable', reason: 'Binary files cannot be previewed.'}, + }) + expect(unavailable).toContain('Binary files cannot be previewed.') + expect(unavailable).not.toContain(' { + const html = renderToStaticMarkup( + , + ) + expect(html).toContain('Loading file preview...') + expect(html).not.toContain(' { + expect(isFilePreviewData(value)).toBe(false) +}) diff --git a/website/src/app/components/FilePreview.tsx b/website/src/app/components/FilePreview.tsx new file mode 100644 index 0000000..015d346 --- /dev/null +++ b/website/src/app/components/FilePreview.tsx @@ -0,0 +1,117 @@ +'use client' + +import {Button} from '@primer/react' +import {Fragment, useEffect, useState} from 'react' +import {isFilePreviewData, type FilePreviewData, type FilePreviewReference} from '../../file-preview' +import type {WorkspaceFile} from '../../workspace-files' +import styles from './FileExplorer.module.css' + +type PreviewState = {type: 'loading'} | {type: 'error'; message: string} | {type: 'loaded'; preview: FilePreviewData} + +function FilePreviewContent({filepath, preview}: {filepath: string; preview: FilePreviewData}) { + if (preview.type === 'unavailable') { + return

{preview.reason}

+ } + if (preview.content.length === 0) { + return

This file is empty.

+ } + if (preview.type === 'text') { + return ( +
+        {preview.content}
+      
+ ) + } + + let previousEnd = 0 + const highlighted = preview.tokens.map((token, index) => { + // Token offsets let us retain the original line endings and blank lines. + const gap = preview.content.slice(previousEnd, token.offset) + previousEnd = token.offset + token.content.length + return ( + + {gap} + {token.content} + + ) + }) + + return ( +
+      
+        {highlighted}
+        {preview.content.slice(previousEnd)}
+      
+    
+ ) +} + +function RemoteFilePreview({filepath, url}: {filepath: string; url: string}) { + const [state, setState] = useState({type: 'loading'}) + const [attempt, setAttempt] = useState(0) + + useEffect(() => { + const controller = new AbortController() + + async function loadPreview() { + try { + const response = await fetch(url, {signal: controller.signal}) + if (!response.ok) { + throw new Error(`The server returned HTTP ${response.status}.`) + } + const preview: unknown = await response.json() + if (!isFilePreviewData(preview)) { + throw new Error('The server returned an invalid file preview.') + } + if (!controller.signal.aborted) { + setState({type: 'loaded', preview}) + } + } catch (error) { + if (!controller.signal.aborted) { + const message = error instanceof Error ? error.message : String(error) + setState({type: 'error', message: `Unable to load this file. ${message}`}) + } + } + } + + void loadPreview() + return () => { + controller.abort() + } + }, [url, attempt]) + + if (state.type === 'loading') { + return ( +

+ Loading file preview... +

+ ) + } + if (state.type === 'error') { + return ( +
+

+ {state.message} +

+ +
+ ) + } + return +} + +function FilePreview({file}: {file: WorkspaceFile}) { + if (file.preview.type === 'remote') { + return + } + return +} + +export {FilePreview, FilePreviewContent} diff --git a/website/src/app/components/RunDetailsPage.test.tsx b/website/src/app/components/RunDetailsPage.test.tsx new file mode 100644 index 0000000..d6e8a72 --- /dev/null +++ b/website/src/app/components/RunDetailsPage.test.tsx @@ -0,0 +1,90 @@ +import {renderToStaticMarkup} from 'react-dom/server' +import {afterEach, expect, test, vi} from 'vitest' +import type {RunDetails} from '../../run-details' +import {RunDetailsPage} from './RunDetailsPage' +import {RunDetailsView} from './RunDetailsView' + +vi.mock('server-only', () => { + return {} +}) +vi.mock('./RunDetailsView', () => { + return { + RunDetailsView: vi.fn(() => { + return null + }), + } +}) + +afterEach(() => { + vi.unstubAllEnvs() + vi.clearAllMocks() +}) + +test.each(['benchmarks', 'experiments'] as const)( + 'passes lightweight %s preview references instead of file contents or tokens', + collection => { + vi.stubEnv('PAGES_BASE_PATH', '/agent-eval') + const content = 'const generatedSource = "not part of the run payload"\n'.repeat(1000) + const run: RunDetails = { + date: '2026-09-03', + results: [ + { + id: 'trial 1', + scenarioId: 'scenario', + treatment: 'Control', + model: 'test-model', + testsPassed: 0, + totalTests: 0, + turns: 0, + outputTokens: 0, + premiumRequests: 0, + totalApiDurationMs: 0, + sessionDurationMs: 0, + tests: [], + transcript: [], + walkthrough: {type: 'Unavailable'}, + judges: [], + workspace: { + type: 'available', + truncated: false, + entries: [ + { + type: 'directory', + name: 'src', + path: 'src', + children: [ + { + type: 'file', + name: 'index.ts', + path: 'src/index.ts', + size: content.length, + preview: {type: 'text', content}, + }, + ], + }, + ], + }, + }, + ], + } + renderToStaticMarkup( + , + ) + const props = vi.mocked(RunDetailsView).mock.calls[0][0] + const payload = JSON.stringify(props.run) + expect(payload).not.toContain('generatedSource') + expect(payload).not.toContain('--shiki-') + expect(payload).toContain(`/agent-eval/file-previews/${collection}/test-id/2026-09-03/trial%201/`) + expect(payload).toContain('/preview.json') + expect(Buffer.byteLength(payload)).toBeLessThan(2000) + }, +) diff --git a/website/src/app/components/RunDetailsPage.tsx b/website/src/app/components/RunDetailsPage.tsx index 4f8c469..9a42aec 100644 --- a/website/src/app/components/RunDetailsPage.tsx +++ b/website/src/app/components/RunDetailsPage.tsx @@ -1,413 +1,50 @@ -'use client' - -import {CheckCircleFillIcon, CopilotIcon, PersonIcon, XCircleFillIcon} from '@primer/octicons-react' -import {Breadcrumbs, FormControl, Select, Stack, UnderlineNav} from '@primer/react' -import type {RunDetails, TranscriptEntry, WalkthroughDataUrl} from '../../run-details' -import type {Route} from 'next' -import Link from 'next/link' -import Image from 'next/image' -import {useState} from 'react' -import {JudgeResults} from './JudgeResults' - -type RunResult = RunDetails['results'][number] - -type ScenarioResultGroup = { - scenarioId: string - results: [RunResult, ...Array] -} - -function formatDuration(milliseconds: number): string { - if (milliseconds < 1000) { - return `${milliseconds} ms` - } - - return `${(milliseconds / 1000).toFixed(1)} s` -} - -function Transcript({entries}: {entries: Array}) { - if (entries.length === 0) { - return

No transcript messages were recorded.

- } - - return ( -
    - {entries.map(entry => { - const isUser = entry.label === 'User' - const isAssistant = entry.label === 'Assistant' - - return ( -
  1. - - {isUser ? : } - -
    -
    - {entry.label} - {entry.timestamp ? ( - - ) : null} -
    -
    -                {entry.content}
    -              
    -
    -
  2. - ) - })} -
- ) -} - -function BrowserScreenshot({alt, source}: {alt: string; source: string}) { - return ( -
- - {alt} -
- ) -} - -function UiWalkthrough({scenarioId, walkthrough}: {scenarioId: string; walkthrough: WalkthroughDataUrl}) { - if (walkthrough.type === 'Video') { - return ( -