diff --git a/bun.lock b/bun.lock index df77071..23adbc4 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "@modelcontextprotocol/sdk": "1.26.0", "agents": "^0.7.6", "get-port": "^7.1.0", - "remix": "3.0.0-alpha.3", + "remix": "3.0.0-alpha.4", "workers-ai-provider": "^3.1.2", "zod": "^4.3.6", }, @@ -306,35 +306,45 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], - "@remix-run/async-context-middleware": ["@remix-run/async-context-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-z2hTnQspCWsKka1QxAu5qf+IRbMYwTBmwL28zCk8nIPcYxG19rDbBeRmfQZ9hxHes72XVDu3Fd3ArLTy8bivdw=="], + "@remix-run/async-context-middleware": ["@remix-run/async-context-middleware@0.2.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-ebnPM2OFM/BO3GSoRKs2i5+lgY/fIunzpL8/RhuEuP9a04cQYXMbEzbdae+qBHTgtsVdPrqeGr/lwlF5FNnQZA=="], - "@remix-run/component": ["@remix-run/component@0.5.0", "", { "dependencies": { "@remix-run/interaction": "^0.5.0" } }, "sha512-xRLOcgwWKZxFdj63bWi3/snC2uxkm978B49EGEv1/G43iBFksYjS4ADanfYxREvQjMlaCqSUH0ZTZsJbGsz3PQ=="], + "@remix-run/auth": ["@remix-run/auth@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-TFsK40jymT7iZyGWOjoqlFFmILdDXy3jYBoI1mohjdqrp8SVWbm7htiSbdd+j8DGBUEbxMTvKcaVG0bPA1M8VA=="], - "@remix-run/compression-middleware": ["@remix-run/compression-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-pKsKtIzW/yjCbftSQRYfvnKKdq65Nu+YEhbQooiXtmd1Ub4inHSrzeA/vca3sQkBnBr/eRA9uujxIszRe+6LAg=="], + "@remix-run/auth-middleware": ["@remix-run/auth-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-QZlI4MyU8VTVI6WB4T4MWWqtbYrIUiiBG3mgdJUXjEYMBBoxVAkT/6rT3YErmhX+SVARP2zC46qzujrbYcQLCQ=="], + + "@remix-run/component": ["@remix-run/component@0.6.0", "", { "dependencies": { "@types/dom-navigation": "^1.0.7" } }, "sha512-hmIUdPnBtONeOH8bbLuOVGSgFD/MAUNvpH0Xwgo27GByBIjQhOm25Tt9R04HQslDhHAzwNVnWeQkr3j9eyXYhQ=="], + + "@remix-run/compression-middleware": ["@remix-run/compression-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-vE+d4a04Tq9fO5KBmNtMD5GKJEt6U++B1yufEdBMNe5ROBQkpPWSuKlhkRcZyoV8SPEI2HbkKMtQ8QXfGJR11g=="], "@remix-run/cookie": ["@remix-run/cookie@0.5.1", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-gbeZfVd1AKRlFj3IJWcIcR6zqVGz2XGJhR+mcqYiWnYt6KM8oUGtc82dsc4qZnWWA1f0nM4/He9wrU4GjB0pag=="], - "@remix-run/data-schema": ["@remix-run/data-schema@0.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0" } }, "sha512-PzpYP9P19cb8bS7Y9+MSxyAWGy0n13sx1lYOMoKI+iEx7pdB2ZLaiidwcn1l6AeR6gjVIpuzyM6/UONG3bzboA=="], + "@remix-run/cop-middleware": ["@remix-run/cop-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-5rMNrBswTJJLPsu1nUfXubgVf7XBTRfPNfd/4QQHaXRdhXVWVxU9HbNJrOjd7iNbBWuZeaJxSdDepbQLnuBkTg=="], + + "@remix-run/cors-middleware": ["@remix-run/cors-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/headers": "^0.19.0" } }, "sha512-x5nyxDhWEOnznqetDhtj5WYoHrKpbiZ8K6BCGQ2+DFjLYHoyWCayciUNrqOVSIWc5XdV6KlKHcg1TyQvW/cS4w=="], + + "@remix-run/csrf-middleware": ["@remix-run/csrf-middleware@0.1.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-EZAV9UZrx3qdYu3ov9NgFVJchw6kOLYI2uRB+e3WBVW1phruHKc42EpdJIZ6fU0J9Of0Lfo4X4VTM7O1/u1cWw=="], - "@remix-run/data-table": ["@remix-run/data-table@0.1.0", "", { "dependencies": { "@remix-run/data-schema": "^0.1.0" } }, "sha512-u70INxiF9pThV2LcP7y37G7eq16OXWrA+HXS8qNplQgJJd1zhwZrODOtfF9U+PKLiWQkBj+bEeh7QhOlvGsVDg=="], + "@remix-run/data-schema": ["@remix-run/data-schema@0.2.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0" } }, "sha512-bXgdWcWZS6TkbVO9Piwn035iw0LczfSTFrCW4Vd1k57x/4j3eWZwfHBr3lXdw5whefacs3m3pVZ8RUiu8NtBeA=="], - "@remix-run/data-table-mysql": ["@remix-run/data-table-mysql@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "mysql2": "^3.15.3" }, "optionalPeers": ["mysql2"] }, "sha512-lzghxTYZDHODNisIJWkq4IkcGsb1pUrp9WGtlNEVWrXWC6aArOiojR3PW4arFwNG5ddKJPRfwot+ySc2YZy5RQ=="], + "@remix-run/data-table": ["@remix-run/data-table@0.2.0", "", {}, "sha512-6QbdlKER0F0rX3XkTHC7ZPBgu/dyGvvgrKRbraBD9iGHTE7AED1emo3/YWsDJUeFzy7INNJaUIpNm9jNcgL8Kg=="], - "@remix-run/data-table-postgres": ["@remix-run/data-table-postgres@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "pg": "^8.16.3" }, "optionalPeers": ["pg"] }, "sha512-mqzARY5tOFVLjFAArryuLQ93M8IdjVnLKnD1VQyJHFbHQF7Zbr8+exv7Hp2hZ1TIGAhZiepGjjC8Re631yJ1Jw=="], + "@remix-run/data-table-mysql": ["@remix-run/data-table-mysql@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "mysql2": "^3.15.3" }, "optionalPeers": ["mysql2"] }, "sha512-voTcYUjG12PWlrWIYSqoa+C3WK9Q6TVKPrqnvGKY9s6EVBil9e+glaP2JOM8BbzL0hGuKPifdbrDGfGpGitdQw=="], - "@remix-run/data-table-sqlite": ["@remix-run/data-table-sqlite@0.1.0", "", { "dependencies": { "@remix-run/data-table": "^0.1.0" }, "peerDependencies": { "better-sqlite3": "^12.4.1" }, "optionalPeers": ["better-sqlite3"] }, "sha512-hFtmz9haMr3p/aFWL5D1zpJsgAnNdLVCF6HvXBmtK4m3NdLmM1eNhblpB/SfPGNEODsmx4Jvcaof57dfaLuKgA=="], + "@remix-run/data-table-postgres": ["@remix-run/data-table-postgres@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "pg": "^8.16.3" }, "optionalPeers": ["pg"] }, "sha512-03v+zpWL4lMJr34PZ76PWFpjIa4K6Uq3NLBqyvN4ojPpm6J7TGsFwPy6sInRm3HjoS4gPSCEmmQ7dUovWa58EA=="], + + "@remix-run/data-table-sqlite": ["@remix-run/data-table-sqlite@0.2.0", "", { "dependencies": { "@remix-run/data-table": "^0.2.0" }, "peerDependencies": { "better-sqlite3": "^12.4.1" }, "optionalPeers": ["better-sqlite3"] }, "sha512-TAQ0u8YnmO3WdnuZULPhc+fgZbKZZPutXiPLf8c9NDhXSC41FlcvfQHTKUxBPVhqdi9mhGiblHHNp6BsVu11dg=="], "@remix-run/fetch-proxy": ["@remix-run/fetch-proxy@0.7.1", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-rPLfOpAaCXtm1dLI45uIPKERNbXbrh0P9AJc1sliz8pWd/McaFYjdr5KzB4QrFSfPvEt/Wmy6F2521qB1kK0ug=="], - "@remix-run/fetch-router": ["@remix-run/fetch-router@0.17.0", "", { "dependencies": { "@remix-run/route-pattern": "^0.19.0", "@remix-run/session": "^0.4.1" } }, "sha512-3FeJGrTqrKKCvZdQWijbCXTEHKcdttkLFbI2ogfpZ+iDYSNZ9036wgDXuuoZqg6d+D0E8Unhk5ZwrLKDCd/hOw=="], + "@remix-run/fetch-router": ["@remix-run/fetch-router@0.18.0", "", { "dependencies": { "@remix-run/route-pattern": "^0.20.0" } }, "sha512-9Z4JgLH9/jD8jiVvAY9LZR+VoZxPJOQ7pENTBJoSo91PZOkPXfCxQWhPhAwYCP8z+/0FV4ZWSg1DmPPoU2f0UQ=="], "@remix-run/file-storage": ["@remix-run/file-storage@0.13.3", "", { "dependencies": { "@remix-run/fs": "^0.4.2", "@remix-run/lazy-file": "^5.0.2" } }, "sha512-HBDz9RRsFRvI6EoeasklxH/NleGy0QZBXBcA4gQBW8ueucop21TQI4wvGlhZmXcnJ3nP4RkhdF2Gff2/HD5eiA=="], "@remix-run/file-storage-s3": ["@remix-run/file-storage-s3@0.1.0", "", { "dependencies": { "@remix-run/file-storage": "^0.13.3", "aws4fetch": "^1.0.20" } }, "sha512-r80An7nSFidK/0xn9O9/HxfUcgxVpM4kprnTGr6pGhKdgbaTCEtA+U5ETYGfeedFxhDcT+7ue+4Fv/VxeIvFwQ=="], - "@remix-run/form-data-middleware": ["@remix-run/form-data-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/form-data-parser": "^0.15.0" } }, "sha512-WZfP1U6lDoipkfjcd0V39HJeTPMTX2WyaPcOBTbBHS0kapIZiHYm6RpGLhE8U58652i3TBh/zzvAczJIbFV2AA=="], + "@remix-run/form-data-middleware": ["@remix-run/form-data-middleware@0.2.0", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/form-data-parser": "^0.16.0" } }, "sha512-oaBcYvyP/U2GrDdFyQUh2pNCshvKwPFBoUup1Bz3NHnMzym4OkZJgaBi4o7HIJC33QudnqI7Mp7TW1pRg0CvIA=="], - "@remix-run/form-data-parser": ["@remix-run/form-data-parser@0.15.0", "", { "dependencies": { "@remix-run/multipart-parser": "^0.14.2" } }, "sha512-sQP4r9218TWmow6Nt252VjKE674dRi4Z8WTnWUxJJG8I/qNfnGZubZ8LgyE0dR9z1gfaEpkd19MfYMLiOTOkJQ=="], + "@remix-run/form-data-parser": ["@remix-run/form-data-parser@0.16.0", "", { "dependencies": { "@remix-run/multipart-parser": "^0.15.0" } }, "sha512-k4QCgCyPURpqe+9Rual1GBJ8Ab6ri82Clfh5ooxq03jKQ1TyQZT5xh+XP0R05+Bqg5bzAZl7r2BpmDEvfoRTWw=="], "@remix-run/fs": ["@remix-run/fs@0.4.2", "", { "dependencies": { "@remix-run/lazy-file": "^5.0.2", "@remix-run/mime": "^0.4.0" } }, "sha512-z3W2L+iUwgZ7i0S379SYQ8veOe2Weqs+JajmyTCqSVzbmMUniH3qQ6SAYr3FjbrKtLLWHN3SpK4XtFv57VzbLA=="], @@ -342,33 +352,31 @@ "@remix-run/html-template": ["@remix-run/html-template@0.3.0", "", {}, "sha512-aAMx68udtIk0fmCpCXHYscVeCDsRVEmEgh4XvtusPr3vkHu3jn4gx5oAxgsPXPdDmmD/d75SYyI0m/F+aLz5iQ=="], - "@remix-run/interaction": ["@remix-run/interaction@0.5.0", "", {}, "sha512-Z2ja9/7TfMHt/wzWq425GI2xj6QxW4E3OHZ8In81uytZKIuWaI6Pn3v8qyMrInwnBEaLcfcbeQVCiExgHU8D4A=="], - "@remix-run/lazy-file": ["@remix-run/lazy-file@5.0.2", "", { "dependencies": { "@remix-run/mime": "^0.4.0" } }, "sha512-52Bo5dTV+EDwrUMS3mjeR+Sly85aHeN3fnNTeaflqzlCMWJwr2pX+y6/3mTDtRdxmTWF1MGQAoeayzfPb4zZJg=="], - "@remix-run/logger-middleware": ["@remix-run/logger-middleware@0.1.3", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-6gxz1XC2lJYQS3Oz1pZzxpuoLowwd2PSpimMaQnkk0fZ7hHYxx7uV+FSs2Z3fue6kYvZ+IxSUe8Wy52V2r4LxA=="], + "@remix-run/logger-middleware": ["@remix-run/logger-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-gFDyYn8o5ddjQoEbrc8CK6PPq3lzrOX6BCqlvM+QVTMJ5/2aHgfMbQdXnWtbeiWJJrpODpABrSNfjLkHjaK4og=="], - "@remix-run/method-override-middleware": ["@remix-run/method-override-middleware@0.1.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0" } }, "sha512-gYFsdY0eIStTpsqGnF/22YracUmS8cZlef6KsBKOVf1nOI9wwwbRrj/DWLMQsWt22YSBMuPYZW5NLKEmXvJRZw=="], + "@remix-run/method-override-middleware": ["@remix-run/method-override-middleware@0.1.5", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0" } }, "sha512-HYQG6YR4l0rYMJ0MXth9SWfeSCwnqHIrWgMQpZHKS/MT7roX7EAfknrl9vzjQWsVfkwl6RrSD30SrLPONj3tsg=="], "@remix-run/mime": ["@remix-run/mime@0.4.0", "", {}, "sha512-O6TcTL6CtuX82Q8BHqAere5O+0hYcrzSgY9whsDOBuqbW753Rczprs2jYw3qCDSo0kLxykW4ys3qgZcdgZ+chw=="], - "@remix-run/multipart-parser": ["@remix-run/multipart-parser@0.14.2", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-yDq9ql4Xz92bRG/Sgl4cg2dRlxxC6A40XBy/oyDhy76hJtTQvgyzx9sfPXYPxcfL1BtqljC+sYHE0PvjmQhSfw=="], + "@remix-run/multipart-parser": ["@remix-run/multipart-parser@0.15.0", "", { "dependencies": { "@remix-run/headers": "^0.19.0" } }, "sha512-/Ugo6k2bN7gh7Ybyhe7R/NrkD075fHrEfVf17P+NNC9rlWHBQCOSyJ4V8n4wtoG8umeImagU/AuHuUuTwzvHww=="], "@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.13.0", "", {}, "sha512-1EsNo0ZpgXu/90AWoRZf/oE3RVTUS80tiTUpt+hv5pjtAkw7icN4WskDwz/KdAw5ARbJLMhZBrO1NqThmy/McA=="], "@remix-run/response": ["@remix-run/response@0.3.2", "", { "dependencies": { "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0" } }, "sha512-GkFqVq5E7Do6rMKTBjgoNyJlrsLrqYg+TDlCDrXoZ3v8O2RlSI14+bCF8lGQHy15DWX2pizVj6R0e6NmjcuLuA=="], - "@remix-run/route-pattern": ["@remix-run/route-pattern@0.19.0", "", {}, "sha512-RXKaIJ2Lx01uyZc0iw+yLzowFCa1/NuB8jN7QTo4QUe2CaUGtvPGdhgrTUp75lyNNCSJIrM9SaAJ6c1pjZdmoA=="], + "@remix-run/route-pattern": ["@remix-run/route-pattern@0.20.0", "", {}, "sha512-TEdJ5eFn40St26oyaRYGI1FWeXDvlEzkbvollM8Xit0qIuDFyFG03Okvvfc1s8KgR9sYULDPjxJIzk6xvIRR9A=="], "@remix-run/session": ["@remix-run/session@0.4.1", "", {}, "sha512-Bm6aKYgutb/raHZ3laloz8g/Qu7f3CeK3o4gUVDMxtEiAdWCzJamwHoTpGOc5+g1Kuy7z85v4M6nGrF06MFDSg=="], - "@remix-run/session-middleware": ["@remix-run/session-middleware@0.1.4", "", { "dependencies": { "@remix-run/cookie": "^0.5.1", "@remix-run/fetch-router": "^0.17.0", "@remix-run/session": "^0.4.1" } }, "sha512-qqLmf7mG88h+Ge8pWiJMO8+t9nfQMuO/Zx2W68IwB7Cpt+b6PDpB++i3dd/KLlsjJ43XPMoT2ydmo+eQMgBX3g=="], + "@remix-run/session-middleware": ["@remix-run/session-middleware@0.2.0", "", { "dependencies": { "@remix-run/cookie": "^0.5.1", "@remix-run/fetch-router": "^0.18.0", "@remix-run/session": "^0.4.1" } }, "sha512-kRAr0inELyXeOnUhlaM/eKIe9x1RZsOGbglb7Keb1+Lf6KxWxNMwv1ymmHBfae2ossjQTN/QUaABwQgX/uk3DQ=="], "@remix-run/session-storage-memcache": ["@remix-run/session-storage-memcache@0.1.0", "", { "dependencies": { "@remix-run/session": "^0.4.1" } }, "sha512-k853rpHncdTJUwdk0hqd+gZ2OONZLNdOUJBKdJB+MehxrVv1TtacDnA+Xs3kh+IVwUrsTmBhED+GHSUocMATUg=="], "@remix-run/session-storage-redis": ["@remix-run/session-storage-redis@0.1.0", "", { "dependencies": { "@remix-run/session": "^0.4.1" }, "peerDependencies": { "redis": "^5.10.0" }, "optionalPeers": ["redis"] }, "sha512-MovUS1E98wDHP8zsESJGm3ySB7iiOhd+3usxyXXM2sbF9gIe6r1bdAXXirGIoC8AEq1v8IiFE5u5ipo7PX0UHQ=="], - "@remix-run/static-middleware": ["@remix-run/static-middleware@0.4.4", "", { "dependencies": { "@remix-run/fetch-router": "^0.17.0", "@remix-run/fs": "^0.4.2", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-aL5ngFG57uPXTEDaH0uP/cKDpYkLMTtmPjK+SR1ugS654ORk8WTD4Ajf56QekMykCvCnO6PkgFAruUyKkwDNMg=="], + "@remix-run/static-middleware": ["@remix-run/static-middleware@0.4.5", "", { "dependencies": { "@remix-run/fetch-router": "^0.18.0", "@remix-run/fs": "^0.4.2", "@remix-run/html-template": "^0.3.0", "@remix-run/mime": "^0.4.0", "@remix-run/response": "^0.3.2" } }, "sha512-jxEbrQMDWcUgmv/2NSv4pahvaW3I4jxqZUMDQ7/VfkcgeJKLFk15vgp4BJ5vLDKKMvwVcgM0Ccbw4Y3Ev2zemA=="], "@remix-run/tar-parser": ["@remix-run/tar-parser@0.7.0", "", {}, "sha512-PW8JxEUzaGcnqxC5hBI8L9lK/Qz3oad6IGKZ+NExI3L7urVJUux+yCBrsme79DMBgS6hL+lgd/5LPFA5fSwF9A=="], @@ -396,6 +404,8 @@ "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@types/dom-navigation": ["@types/dom-navigation@1.0.7", "", {}, "sha512-Di4W+i2faYquHUnyWUg3bBQp5pTNvjDDA7mIYfD/1WlLgan6sKkeVjGbdL78K0CuNEk5Pfc/c0rfelwkz10mnQ=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -952,7 +962,7 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], - "remix": ["remix@3.0.0-alpha.3", "", { "dependencies": { "@remix-run/async-context-middleware": "^0.1.3", "@remix-run/component": "^0.5.0", "@remix-run/compression-middleware": "^0.1.3", "@remix-run/cookie": "^0.5.1", "@remix-run/data-schema": "^0.1.0", "@remix-run/data-table": "^0.1.0", "@remix-run/data-table-mysql": "^0.1.0", "@remix-run/data-table-postgres": "^0.1.0", "@remix-run/data-table-sqlite": "^0.1.0", "@remix-run/fetch-proxy": "^0.7.1", "@remix-run/fetch-router": "^0.17.0", "@remix-run/file-storage": "^0.13.3", "@remix-run/file-storage-s3": "^0.1.0", "@remix-run/form-data-middleware": "^0.1.4", "@remix-run/form-data-parser": "^0.15.0", "@remix-run/fs": "^0.4.2", "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/interaction": "^0.5.0", "@remix-run/lazy-file": "^5.0.2", "@remix-run/logger-middleware": "^0.1.3", "@remix-run/method-override-middleware": "^0.1.4", "@remix-run/mime": "^0.4.0", "@remix-run/multipart-parser": "^0.14.2", "@remix-run/node-fetch-server": "^0.13.0", "@remix-run/response": "^0.3.2", "@remix-run/route-pattern": "^0.19.0", "@remix-run/session": "^0.4.1", "@remix-run/session-middleware": "^0.1.4", "@remix-run/session-storage-memcache": "^0.1.0", "@remix-run/session-storage-redis": "^0.1.0", "@remix-run/static-middleware": "^0.4.4", "@remix-run/tar-parser": "^0.7.0" } }, "sha512-RIctAYR7OW3oYzAGclLhgltrRtKviIdnCVwoLcPDicOjV4I2mJ9AEi8YXl2+hGPupzNNEUcrDtoICd7xNuMptg=="], + "remix": ["remix@3.0.0-alpha.4", "", { "dependencies": { "@remix-run/async-context-middleware": "^0.2.0", "@remix-run/auth": "^0.1.0", "@remix-run/auth-middleware": "^0.1.0", "@remix-run/component": "^0.6.0", "@remix-run/compression-middleware": "^0.1.4", "@remix-run/cookie": "^0.5.1", "@remix-run/cop-middleware": "^0.1.0", "@remix-run/cors-middleware": "^0.1.0", "@remix-run/csrf-middleware": "^0.1.0", "@remix-run/data-schema": "^0.2.0", "@remix-run/data-table": "^0.2.0", "@remix-run/data-table-mysql": "^0.2.0", "@remix-run/data-table-postgres": "^0.2.0", "@remix-run/data-table-sqlite": "^0.2.0", "@remix-run/fetch-proxy": "^0.7.1", "@remix-run/fetch-router": "^0.18.0", "@remix-run/file-storage": "^0.13.3", "@remix-run/file-storage-s3": "^0.1.0", "@remix-run/form-data-middleware": "^0.2.0", "@remix-run/form-data-parser": "^0.16.0", "@remix-run/fs": "^0.4.2", "@remix-run/headers": "^0.19.0", "@remix-run/html-template": "^0.3.0", "@remix-run/lazy-file": "^5.0.2", "@remix-run/logger-middleware": "^0.1.4", "@remix-run/method-override-middleware": "^0.1.5", "@remix-run/mime": "^0.4.0", "@remix-run/multipart-parser": "^0.15.0", "@remix-run/node-fetch-server": "^0.13.0", "@remix-run/response": "^0.3.2", "@remix-run/route-pattern": "^0.20.0", "@remix-run/session": "^0.4.1", "@remix-run/session-middleware": "^0.2.0", "@remix-run/session-storage-memcache": "^0.1.0", "@remix-run/session-storage-redis": "^0.1.0", "@remix-run/static-middleware": "^0.4.5", "@remix-run/tar-parser": "^0.7.0" } }, "sha512-fvYHPm8QbrbL09wmmddrnknyntB+fl4bLmpECpK32cOoJW3pMoMvESBB1qRAQe+yWgTexVjp5G3BGOmmtYOX0A=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], diff --git a/client/app.tsx b/client/app.tsx index cf4d925..525bcec 100644 --- a/client/app.tsx +++ b/client/app.tsx @@ -1,199 +1,184 @@ -import { type Handle } from 'remix/component' -import { clientRoutes } from './routes/index.tsx' -import { - getPathname, - listenToRouterNavigation, - Router, -} from './client-router.tsx' -import { - fetchSessionInfo, - type SessionInfo, - type SessionStatus, -} from './session.ts' -import { buildAuthLink } from './auth-links.ts' -import { colors, mq, spacing, typography } from './styles/tokens.ts' - +import { css, type Handle } from 'remix/component'; +import { clientRoutes } from './routes/index.tsx'; +import { getPathname, listenToRouterNavigation, Router, } from './client-router.tsx'; +import { fetchSessionInfo, type SessionInfo, type SessionStatus, } from './session.ts'; +import { buildAuthLink } from './auth-links.ts'; +import { colors, mq, spacing, typography } from './styles/tokens.ts'; export function App(handle: Handle) { - let session: SessionInfo | null = null - let sessionStatus: SessionStatus = 'idle' - let sessionRefreshInFlight = false - let sessionRefreshQueued = false - let currentPathname = getPathname() - - function queueSessionRefresh() { - sessionRefreshQueued = true - if (sessionRefreshInFlight) return - - // Preserve current nav state during refreshes after first load. - if (sessionStatus === 'idle') { - sessionStatus = 'loading' - handle.update() - } - - sessionRefreshQueued = false - sessionRefreshInFlight = true - handle.queueTask(async (signal) => { - const nextSession = await fetchSessionInfo(signal) - sessionRefreshInFlight = false - if (signal.aborted) return - session = nextSession - sessionStatus = 'ready' - handle.update() - if (sessionRefreshQueued) { - queueSessionRefresh() - } - }) - if (sessionStatus !== 'loading') { - handle.update() - } - } - - handle.queueTask(() => { - queueSessionRefresh() - }) - listenToRouterNavigation(handle, () => { - currentPathname = getPathname() - queueSessionRefresh() - handle.update() - }) - - const navLinkCss = { - color: colors.primaryText, - fontWeight: typography.fontWeight.medium, - textDecoration: 'none', - '&:hover': { - textDecoration: 'underline', - }, - } - - const navHomeLinkCss = { - ...navLinkCss, - display: 'flex', - alignItems: 'center', - lineHeight: 0, - '&:hover': { - textDecoration: 'none', - opacity: 0.85, - }, - } - - const logOutButtonCss = { - padding: `${spacing.xs} ${spacing.md}`, - borderRadius: '999px', - border: `1px solid ${colors.border}`, - backgroundColor: 'transparent', - color: colors.text, - fontWeight: typography.fontWeight.medium, - cursor: 'pointer', - } - - return () => { - const isChatLayout = currentPathname.startsWith('/chat') - const sessionEmail = session?.email ?? '' - const isSessionReady = sessionStatus === 'ready' - const isLoggedIn = isSessionReady && Boolean(sessionEmail) - const showAuthLinks = isSessionReady && !isLoggedIn - const oauthRedirectTo = - typeof window !== 'undefined' && currentPathname === '/oauth/authorize' - ? `${currentPathname}${window.location.search}` - : null - const loginHref = buildAuthLink('/login', oauthRedirectTo) - const signupHref = buildAuthLink('/signup', oauthRedirectTo) - - return ( -
-
); + }; } diff --git a/client/client-router.tsx b/client/client-router.tsx index 813fdf4..ca013c7 100644 --- a/client/client-router.tsx +++ b/client/client-router.tsx @@ -1,4 +1,4 @@ -import { type Handle } from 'remix/component' +import { addEventListeners, type Handle } from 'remix/component' type RouterSetup = { routes: Record @@ -251,7 +251,7 @@ function ensureRouter() { export function listenToRouterNavigation(handle: Handle, listener: () => void) { ensureRouter() - handle.on(routerEvents, { + addEventListeners(routerEvents, handle.signal, { navigate: () => listener(), }) } diff --git a/client/counter.tsx b/client/counter.tsx index f7fcea9..66e99af 100644 --- a/client/counter.tsx +++ b/client/counter.tsx @@ -1,49 +1,36 @@ -import { type Handle } from 'remix/component' -import { - colors, - radius, - spacing, - transitions, - typography, -} from './styles/tokens.ts' - +import { css, type Handle, on } from 'remix/component'; +import { colors, radius, spacing, transitions, typography, } from './styles/tokens.ts'; type CounterSetup = { - initial?: number -} - + initial?: number; +}; export function Counter(handle: Handle, setup: CounterSetup = {}) { - let count = setup.initial ?? 0 - - function increment() { - count += 1 - handle.update() - } - - return () => ( - - ) + ); } diff --git a/client/double-check.ts b/client/double-check.ts index e4bf7c0..7cc793e 100644 --- a/client/double-check.ts +++ b/client/double-check.ts @@ -1,26 +1,14 @@ import { type Handle } from 'remix/component' -type BlurHandler = (event: FocusEvent) => void -type ClickHandler = (event: MouseEvent) => void - type ButtonLikeProps = { + mix?: Array + onConfirm?: (event: MouseEvent) => void on?: { - blur?: BlurHandler - click?: ClickHandler + click?: (event: MouseEvent) => void } [key: string]: unknown } -function callAll( - ...handlers: Array<((event: Event) => void) | undefined> -) { - return (event: Event) => { - for (const handler of handlers) { - handler?.(event) - } - } -} - export function createDoubleCheck(handle: Handle) { let doubleCheck = false @@ -39,29 +27,36 @@ export function createDoubleCheck(handle: Handle) { }, getButtonProps(props?: Props): Props { const buttonProps = props ?? ({} as Props) - - const onBlur: BlurHandler = () => { - setDoubleCheck(false) - } - - const onClick: ClickHandler = (event) => { - if (!doubleCheck) { - event.preventDefault() - setDoubleCheck(true) - return - } - - buttonProps.on?.click?.(event) - setDoubleCheck(false) - } + const { + mix: inputMix, + onConfirm, + on: onOverrides, + ...rest + } = buttonProps as ButtonLikeProps + const mix = [...(inputMix ?? [])] + const confirmHandler = onConfirm ?? onOverrides?.click + + mix.push({ + handleEvent(handle) { + handle.addEventListener('blur', () => { + setDoubleCheck(false) + }) + + handle.addEventListener('click', (event) => { + if (!doubleCheck) { + event.preventDefault() + setDoubleCheck(true) + return + } + confirmHandler?.(event) + setDoubleCheck(false) + }) + }, + }) return { - ...buttonProps, - on: { - ...buttonProps.on, - blur: callAll(onBlur, buttonProps.on?.blur), - click: onClick, - }, + ...rest, + mix, } }, } diff --git a/client/editable-text.tsx b/client/editable-text.tsx index 468baf3..a85ed28 100644 --- a/client/editable-text.tsx +++ b/client/editable-text.tsx @@ -1,171 +1,148 @@ -import { type Handle } from 'remix/component' - +import { css, type Handle, on } from 'remix/component'; type EditableTextProps = { - id: string - ariaLabel: string - value: string - emptyText?: string - buttonCss?: Record - inputCss?: Record - onSave: (value: string) => Promise | boolean -} - + id: string; + ariaLabel: string; + value: string; + emptyText?: string; + buttonCss?: Record; + inputCss?: Record; + onSave: (value: string) => Promise | boolean; +}; const inheritTextStyles = { - fontSize: 'inherit', - fontStyle: 'inherit', - fontWeight: 'inherit', - fontFamily: 'inherit', - textAlign: 'inherit', - lineHeight: 'inherit', - color: 'inherit', -} as const - + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', + lineHeight: 'inherit', + color: 'inherit', +} as const; export function EditableText(handle: Handle) { - let isEditing = false - let draftValue = '' - let isSaving = false - - function focusInput(inputId: string) { - void handle.queueTask(async () => { - const input = document.getElementById(inputId) - if (!(input instanceof HTMLInputElement)) return - input.focus() - input.select() - }) - } - - function focusButton(buttonId: string) { - void handle.queueTask(async () => { - const button = document.getElementById(buttonId) - if (!(button instanceof HTMLButtonElement)) return - button.focus() - }) - } - - return (props: EditableTextProps) => { - const buttonId = `${props.id}-button` - - function startEditing() { - if (isSaving) return - draftValue = props.value - isEditing = true - handle.update() - focusInput(props.id) - } - - function cancelEditing() { - if (isSaving) return - draftValue = props.value - isEditing = false - handle.update() - focusButton(buttonId) - } - - async function submitEditing(event: SubmitEvent) { - event.preventDefault() - if (isSaving) return - const nextValue = draftValue.trim() - if (!nextValue) return - - isSaving = true - handle.update() - let didSave = false - try { - didSave = await props.onSave(nextValue) - } catch (error) { - isSaving = false - handle.update() - throw error - } - isSaving = false - if (!didSave) { - handle.update() - return - } - - isEditing = false - handle.update() - focusButton(buttonId) - } - - function handleDraftInput(event: Event) { - if (!(event.currentTarget instanceof HTMLInputElement)) return - draftValue = event.currentTarget.value - handle.update() - } - - function handleDraftKeyDown(event: KeyboardEvent) { - if (!(event.currentTarget instanceof HTMLInputElement)) return - if (event.key === 'Escape') { - event.preventDefault() - cancelEditing() - return - } - if (event.key === 'Enter') { - event.preventDefault() - event.currentTarget.form?.requestSubmit() - } - } - - if (isEditing) { - return ( -
- -
- ) - } - - return ( - - ) - } + ); + }; } diff --git a/client/routes/account.tsx b/client/routes/account.tsx index 3c706ff..04faf2c 100644 --- a/client/routes/account.tsx +++ b/client/routes/account.tsx @@ -1,82 +1,81 @@ -import { type Handle } from 'remix/component' -import { colors, spacing, typography } from '#client/styles/tokens.ts' - -type AccountStatus = 'idle' | 'loading' | 'ready' | 'error' - +import { css, type Handle } from 'remix/component'; +import { colors, spacing, typography } from '#client/styles/tokens.ts'; +type AccountStatus = 'idle' | 'loading' | 'ready' | 'error'; export function AccountRoute(handle: Handle) { - let status: AccountStatus = 'loading' - let email = '' - let message: string | null = null - - async function loadAccount(signal: AbortSignal) { - try { - const response = await fetch('/session', { - headers: { Accept: 'application/json' }, - credentials: 'include', - signal, - }) - if (signal.aborted) return - const payload = await response.json().catch(() => null) - const sessionEmail = - response.ok && - payload?.ok && - typeof payload?.session?.email === 'string' - ? payload.session.email.trim() - : '' - if (!sessionEmail) { - window.location.assign('/login') - return - } - email = sessionEmail - status = 'ready' - message = null - handle.update() - } catch { - if (signal.aborted) return - status = 'error' - message = 'Unable to load your account.' - handle.update() - } - } - - return () => { - if (status === 'loading') { - handle.queueTask(loadAccount) - } - - return ( -
-
-

+ let status: AccountStatus = 'loading'; + let email = ''; + let message: string | null = null; + async function loadAccount(signal: AbortSignal) { + try { + const response = await fetch('/session', { + headers: { Accept: 'application/json' }, + credentials: 'include', + signal, + }); + if (signal.aborted) + return; + const payload = await response.json().catch(() => null); + const sessionEmail = response.ok && + payload?.ok && + typeof payload?.session?.email === 'string' + ? payload.session.email.trim() + : ''; + if (!sessionEmail) { + window.location.assign('/login'); + return; + } + email = sessionEmail; + status = 'ready'; + message = null; + handle.update(); + } + catch { + if (signal.aborted) + return; + status = 'error'; + message = 'Unable to load your account.'; + handle.update(); + } + } + return () => { + if (status === 'loading') { + handle.queueTask(loadAccount); + } + return (
+
+

{email ? `Welcome, ${email}` : 'Welcome'}

-

+

You are signed in to epicflare.

- {status === 'loading' ? ( -

Loading your account…

- ) : null} - {message ? ( -

+ {status === 'loading' ? (

Loading your account…

) : null} + {message ? (

{message} -

- ) : null} -
- ) - } +

) : null} +

); + }; } diff --git a/client/routes/chat.tsx b/client/routes/chat.tsx index 9a5ab1a..96f9676 100644 --- a/client/routes/chat.tsx +++ b/client/routes/chat.tsx @@ -1,4 +1,4 @@ -import { type Handle } from 'remix/component' +import { addEventListeners, css, type Handle, on } from 'remix/component' import { ChatClient, type ChatClientSnapshot } from '#client/chat-client.ts' import { navigate, routerEvents } from '#client/client-router.tsx' import { createDoubleCheck } from '#client/double-check.ts' @@ -31,9 +31,7 @@ import { type ChatThreadSummary, type ChatThreadUpdateResponse, } from '#shared/chat.ts' - type ThreadStatus = 'idle' | 'loading' | 'ready' | 'error' - function getSelectedThreadIdFromLocation() { if (typeof window === 'undefined') return null const prefix = '/chat/' @@ -41,31 +39,26 @@ function getSelectedThreadIdFromLocation() { const threadId = window.location.pathname.slice(prefix.length).trim() return threadId || null } - function buildThreadHref(threadId: string) { return `/chat/${threadId}` } - function isMobileViewport() { return ( typeof window !== 'undefined' && window.matchMedia(`(max-width: ${breakpoints.tablet})`).matches ) } - const MESSAGES_SCROLL_CONTAINER_ID = 'chat-messages-scroll-container' const THREAD_LIST_SCROLL_CONTAINER_ID = 'chat-thread-list-scroll-container' const MESSAGES_SCROLL_THRESHOLD_PX = 96 const THREAD_LIST_SCROLL_THRESHOLD_PX = 96 const MESSAGE_SCROLL_FADE_HEIGHT = '2.5rem' const THREADS_PAGE_LIMIT = 40 - function truncatePreview(text: string) { const normalized = text.trim() if (!normalized) return '' return normalized.length > 120 ? `${normalized.slice(0, 117)}...` : normalized } - function createInitialSnapshot(): ChatClientSnapshot { return { messages: [], @@ -79,7 +72,6 @@ function createInitialSnapshot(): ChatClientSnapshot { connected: false, } } - function buildThreadPreviewFromMessages( messages: ChatClientSnapshot['messages'], ) { @@ -91,7 +83,10 @@ function buildThreadPreviewFromMessages( part, ): part is Extract< (typeof lastMessage.parts)[number], - { type: 'text'; text: string } + { + type: 'text' + text: string + } > => part.type === 'text' && typeof part.text === 'string', ) .map((part) => part.text) @@ -99,7 +94,6 @@ function buildThreadPreviewFromMessages( .trim() return text ? truncatePreview(text) : null } - async function fetchThreads(input?: { cursor?: string | null signal?: AbortSignal @@ -123,7 +117,10 @@ async function fetchThreads(input?: { | (ChatThreadListResponse & { error?: string }) - | { ok?: false; error?: string } + | { + ok?: false + error?: string + } | null if ( !response.ok || @@ -142,7 +139,6 @@ async function fetchThreads(input?: { totalCount: payload.totalCount, } } - async function fetchThreadById(threadId: string, signal?: AbortSignal) { const url = new URL('/chat-threads', window.location.href) url.searchParams.set('threadId', threadId) @@ -155,7 +151,10 @@ async function fetchThreadById(threadId: string, signal?: AbortSignal) { | (ChatThreadLookupResponse & { error?: string }) - | { ok?: false; error?: string } + | { + ok?: false + error?: string + } | null if ( !response.ok || @@ -167,7 +166,6 @@ async function fetchThreadById(threadId: string, signal?: AbortSignal) { } return payload.thread } - async function createThread() { const response = await fetch('/chat-threads', { method: 'POST', @@ -183,7 +181,6 @@ async function createThread() { } return payload.thread } - async function deleteThread(threadId: string) { const response = await fetch('/chat-threads/delete', { method: 'POST', @@ -199,7 +196,6 @@ async function deleteThread(threadId: string) { throw new Error(payload?.error || 'Unable to delete thread.') } } - async function updateThreadTitle(threadId: string, title: string) { const response = await fetch('/chat-threads/update', { method: 'POST', @@ -208,8 +204,13 @@ async function updateThreadTitle(threadId: string, title: string) { body: JSON.stringify({ threadId, title }), }) const payload = (await response.json().catch(() => null)) as - | (ChatThreadUpdateResponse & { error?: string }) - | { ok?: false; error?: string } + | (ChatThreadUpdateResponse & { + error?: string + }) + | { + ok?: false + error?: string + } | null if ( !response.ok || @@ -221,7 +222,6 @@ async function updateThreadTitle(threadId: string, title: string) { } return payload.thread } - function renderMessageParts( parts: Array<{ type: string @@ -237,56 +237,57 @@ function renderMessageParts( return (

{part.text}

) } - if (part.type.startsWith('tool-')) { return (
{part.type.replace(/^tool-/, '')} - State: {part.state} + + State: {part.state} + {part.input !== undefined ? ( - + Input: {JSON.stringify(part.input)} ) : null} {part.output !== undefined ? ( - + Output: {JSON.stringify(part.output)} ) : null} {part.errorText ? ( - {part.errorText} + {part.errorText} ) : null}
) } - return null }) } - function renderPaperAirplaneIcon() { return (

{actionError}

+

{actionError}

) : null}
-