diff --git a/apps/docs/package.json b/apps/docs/package.json index 74ce641aa..cc5dab936 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -29,7 +29,7 @@ "@types/node": "^26.2.0", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "oxlint": "^1.78.0", + "oxlint": "1.80.0", "postcss": "^8.5.26", "tailwindcss": "^4.3.3", "typescript": "^6.0.3" diff --git a/package.json b/package.json index 3eb7c94f0..f18c6f96f 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "zod": "4.4.3" }, "devDependencies": { - "@oxlint/plugins": "1.78.0", + "@oxlint/plugins": "1.80.0", "@tailwindcss/vite": "^4.1.11", "@types/node": "^26.2.0", "@types/react": "^19.1.9", @@ -101,7 +101,7 @@ "electron-builder": "^26.15.3", "electron-updater": "^6.8.9", "esbuild": "^0.28.2", - "oxlint": "1.78.0", + "oxlint": "1.80.0", "tailwindcss": "^4.1.11", "typebox": "1.3.7", "typescript": "^5.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21ac05dcb..5c7beffb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ importers: version: 4.4.3 devDependencies: '@oxlint/plugins': - specifier: 1.78.0 - version: 1.78.0 + specifier: 1.80.0 + version: 1.80.0 '@tailwindcss/vite': specifier: ^4.1.11 version: 4.3.3(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.16.9)(yaml@2.9.0)) @@ -79,8 +79,8 @@ importers: specifier: ^0.28.2 version: 0.28.2 oxlint: - specifier: 1.78.0 - version: 1.78.0 + specifier: 1.80.0 + version: 1.80.0 tailwindcss: specifier: ^4.1.11 version: 4.3.3 @@ -143,8 +143,8 @@ importers: specifier: ^19.2.4 version: 19.2.4(@types/react@19.2.18) oxlint: - specifier: ^1.78.0 - version: 1.78.0 + specifier: 1.80.0 + version: 1.80.0 postcss: specifier: ^8.5.26 version: 8.5.26 @@ -1322,130 +1322,130 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} - '@oxlint/binding-android-arm-eabi@1.78.0': - resolution: {integrity: sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==} + '@oxlint/binding-android-arm-eabi@1.80.0': + resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.78.0': - resolution: {integrity: sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==} + '@oxlint/binding-android-arm64@1.80.0': + resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.78.0': - resolution: {integrity: sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==} + '@oxlint/binding-darwin-arm64@1.80.0': + resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.78.0': - resolution: {integrity: sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==} + '@oxlint/binding-darwin-x64@1.80.0': + resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.78.0': - resolution: {integrity: sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==} + '@oxlint/binding-freebsd-x64@1.80.0': + resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.78.0': - resolution: {integrity: sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==} + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.78.0': - resolution: {integrity: sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==} + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.78.0': - resolution: {integrity: sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==} + '@oxlint/binding-linux-arm64-gnu@1.80.0': + resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.78.0': - resolution: {integrity: sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==} + '@oxlint/binding-linux-arm64-musl@1.80.0': + resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.78.0': - resolution: {integrity: sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==} + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.78.0': - resolution: {integrity: sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==} + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.78.0': - resolution: {integrity: sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==} + '@oxlint/binding-linux-riscv64-musl@1.80.0': + resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.78.0': - resolution: {integrity: sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==} + '@oxlint/binding-linux-s390x-gnu@1.80.0': + resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.78.0': - resolution: {integrity: sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==} + '@oxlint/binding-linux-x64-gnu@1.80.0': + resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.78.0': - resolution: {integrity: sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==} + '@oxlint/binding-linux-x64-musl@1.80.0': + resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.78.0': - resolution: {integrity: sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==} + '@oxlint/binding-openharmony-arm64@1.80.0': + resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.78.0': - resolution: {integrity: sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==} + '@oxlint/binding-win32-arm64-msvc@1.80.0': + resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.78.0': - resolution: {integrity: sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==} + '@oxlint/binding-win32-ia32-msvc@1.80.0': + resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.78.0': - resolution: {integrity: sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==} + '@oxlint/binding-win32-x64-msvc@1.80.0': + resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/plugins@1.78.0': - resolution: {integrity: sha512-Ypt8KeRYw+4jUtlPirfcHWMrn5ms12VrrFPD+Mds477/7tJxG1Kcz2Yrg2nVcTQEUx/GdlhS+BUg1kmxNm04Ug==} + '@oxlint/plugins@1.80.0': + resolution: {integrity: sha512-QRgH1XqQEYNHa4f1vvPQ5fAdNdncHGIUG1ZWLlGIZHky3qwCEeAKYitZNbZMtaXtAQAAFFTOwqUfzESvimqZNA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@peculiar/asn1-schema@2.8.0': @@ -3444,8 +3444,8 @@ packages: oniguruma-to-es@4.3.6: resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - oxlint@1.78.0: - resolution: {integrity: sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==} + oxlint@1.80.0: + resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5174,64 +5174,64 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} - '@oxlint/binding-android-arm-eabi@1.78.0': + '@oxlint/binding-android-arm-eabi@1.80.0': optional: true - '@oxlint/binding-android-arm64@1.78.0': + '@oxlint/binding-android-arm64@1.80.0': optional: true - '@oxlint/binding-darwin-arm64@1.78.0': + '@oxlint/binding-darwin-arm64@1.80.0': optional: true - '@oxlint/binding-darwin-x64@1.78.0': + '@oxlint/binding-darwin-x64@1.80.0': optional: true - '@oxlint/binding-freebsd-x64@1.78.0': + '@oxlint/binding-freebsd-x64@1.80.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.78.0': + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.78.0': + '@oxlint/binding-linux-arm-musleabihf@1.80.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.78.0': + '@oxlint/binding-linux-arm64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.78.0': + '@oxlint/binding-linux-arm64-musl@1.80.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.78.0': + '@oxlint/binding-linux-ppc64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.78.0': + '@oxlint/binding-linux-riscv64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.78.0': + '@oxlint/binding-linux-riscv64-musl@1.80.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.78.0': + '@oxlint/binding-linux-s390x-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.78.0': + '@oxlint/binding-linux-x64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-musl@1.78.0': + '@oxlint/binding-linux-x64-musl@1.80.0': optional: true - '@oxlint/binding-openharmony-arm64@1.78.0': + '@oxlint/binding-openharmony-arm64@1.80.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.78.0': + '@oxlint/binding-win32-arm64-msvc@1.80.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.78.0': + '@oxlint/binding-win32-ia32-msvc@1.80.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.78.0': + '@oxlint/binding-win32-x64-msvc@1.80.0': optional: true - '@oxlint/plugins@1.78.0': {} + '@oxlint/plugins@1.80.0': {} '@peculiar/asn1-schema@2.8.0': dependencies: @@ -7516,27 +7516,27 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - oxlint@1.78.0: + oxlint@1.80.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.78.0 - '@oxlint/binding-android-arm64': 1.78.0 - '@oxlint/binding-darwin-arm64': 1.78.0 - '@oxlint/binding-darwin-x64': 1.78.0 - '@oxlint/binding-freebsd-x64': 1.78.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.78.0 - '@oxlint/binding-linux-arm-musleabihf': 1.78.0 - '@oxlint/binding-linux-arm64-gnu': 1.78.0 - '@oxlint/binding-linux-arm64-musl': 1.78.0 - '@oxlint/binding-linux-ppc64-gnu': 1.78.0 - '@oxlint/binding-linux-riscv64-gnu': 1.78.0 - '@oxlint/binding-linux-riscv64-musl': 1.78.0 - '@oxlint/binding-linux-s390x-gnu': 1.78.0 - '@oxlint/binding-linux-x64-gnu': 1.78.0 - '@oxlint/binding-linux-x64-musl': 1.78.0 - '@oxlint/binding-openharmony-arm64': 1.78.0 - '@oxlint/binding-win32-arm64-msvc': 1.78.0 - '@oxlint/binding-win32-ia32-msvc': 1.78.0 - '@oxlint/binding-win32-x64-msvc': 1.78.0 + '@oxlint/binding-android-arm-eabi': 1.80.0 + '@oxlint/binding-android-arm64': 1.80.0 + '@oxlint/binding-darwin-arm64': 1.80.0 + '@oxlint/binding-darwin-x64': 1.80.0 + '@oxlint/binding-freebsd-x64': 1.80.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.80.0 + '@oxlint/binding-linux-arm-musleabihf': 1.80.0 + '@oxlint/binding-linux-arm64-gnu': 1.80.0 + '@oxlint/binding-linux-arm64-musl': 1.80.0 + '@oxlint/binding-linux-ppc64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-musl': 1.80.0 + '@oxlint/binding-linux-s390x-gnu': 1.80.0 + '@oxlint/binding-linux-x64-gnu': 1.80.0 + '@oxlint/binding-linux-x64-musl': 1.80.0 + '@oxlint/binding-openharmony-arm64': 1.80.0 + '@oxlint/binding-win32-arm64-msvc': 1.80.0 + '@oxlint/binding-win32-ia32-msvc': 1.80.0 + '@oxlint/binding-win32-x64-msvc': 1.80.0 p-cancelable@2.1.1: {} diff --git a/server/group-tasks.test.ts b/server/group-tasks.test.ts index a1684d093..9e699c71a 100644 --- a/server/group-tasks.test.ts +++ b/server/group-tasks.test.ts @@ -84,6 +84,15 @@ describe("channel tasks", () => { expect(store.deleteGroupTask(channel.id, first)).toBeNull(); }); + it("normalizes a supplied task title at the store boundary", async () => { + const { store } = await freshStore(); + const bot = store.createBot(); + const channel = store.createGroup("Product", [bot.id]); + const longTitle = "x".repeat(100); + + expect(store.createGroupTask(channel.id, ` ${longTitle} `)?.title).toBe(longTitle.slice(0, 80)); + }); + it("adopts a legacy channel thread without losing its folder or pin", async () => { const { store, Store } = await freshStore(); const bot = store.createBot(); diff --git a/server/index.test.ts b/server/index.test.ts index 3fd4cd5be..cb75a4f79 100644 --- a/server/index.test.ts +++ b/server/index.test.ts @@ -729,6 +729,7 @@ describe("harness HTTP API", () => { expect(removed.body.group.tasks).toHaveLength(1); expect((await api("DELETE", `/api/groups/${room.id}/tasks/${originalThread}`)).status).toBe(400); expect((await api("POST", `/api/groups/${room.id}/tasks/missing-thread`)).status).toBe(404); + expect((await api("POST", `/api/groups/${room.id}/tasks`, { title: 42 })).status).toBe(400); } finally { await api("DELETE", `/api/groups/${room.id}`); await api("DELETE", `/api/bots/${bot.id}`); diff --git a/server/index.ts b/server/index.ts index c06699d9e..269fbc61e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -109,7 +109,6 @@ import { Store, type GroupDefaultResponder, type GroupRecord, - type GroupTaskRecord, type Message, type TaskRecord, } from "./store.ts"; @@ -228,6 +227,7 @@ function authorizedComms(header: string | string[] | undefined): boolean { // A→B is allowed but B→C (and A→B→A loops) never start. const MAX_COMMS_DEPTH = 1; const MAX_WORKSPACE_BOTS = 100; +const createGroupTaskRequestSchema = z.object({ title: z.string().optional() }); // Resolved from the server root — see server/proxy-paths.ts. This descending // path happened to survive bundling, but it goes through the same anchor so // there is exactly one way proxies are located. @@ -449,11 +449,10 @@ store.seedIfEmpty(); * paired phone has even less business holding provider session identifiers * than the desktop window did. Stripped here rather than at each call site * so a new broadcast cannot forget. */ -const wireTask = ({ resumeCursors, lastInstanceId, ...task }: TaskRecord) => task; -const wireGroupTask = (task: GroupTaskRecord) => task; +const wireTask = ({ resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, ...task }: TaskRecord) => task; const wireBot = (bot: NonNullable>) => { - const { resumeCursors, tasks, ...rest } = bot; + const { resumeCursors: _resumeCursors, tasks, ...rest } = bot; return { ...rest, avatarUrl: rest.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map(wireTask) } : {}) }; }; @@ -519,7 +518,7 @@ const groupWithThread = (group: GroupRecord) => ({ ...publicGroupState(group), messages: store.messagesFor(group.threadId), activeLeafId: store.activeLeaf(group.threadId), - ...(group.dm ? {} : { tasks: store.groupTasks(group.id).map(wireGroupTask) }), + ...(group.dm ? {} : { tasks: store.groupTasks(group.id) }), }); // The store tells us what it wrote; this is the ONE place that turns those @@ -585,7 +584,7 @@ function pageSize(raw: string | null): number | null | undefined { * `/api/threads/:threadId/messages/:id/image` when it actually shows one. */ function slimMessage(message: Message): Message | Record { if (message.kind !== "screen" || !message.png) return message; - const { png, mime, ...rest } = message; + const { png: _png, mime: _mime, ...rest } = message; return { ...rest, hasImage: true }; } @@ -4022,7 +4021,7 @@ const server = createServer(async (req, res) => { if (format === "json") { // pixels stripped — an export is for reading and archiving, and a // base64 desktop frame is neither - const slim = messages.map(({ png, mime, ...rest }) => rest); + const slim = messages.map(({ png: _png, mime: _mime, ...rest }) => rest); res.writeHead(200, { "content-type": "application/json", "content-disposition": `attachment; filename="${filename}.json"`, @@ -4454,11 +4453,13 @@ const server = createServer(async (req, res) => { if (!body || typeof body !== "object" || Array.isArray(body)) { return json(res, 400, { error: "body must be a JSON object" }); } - const task = store.createGroupTask(group.id, typeof body.title === "string" ? body.title : undefined); + const request = createGroupTaskRequestSchema.safeParse(body); + if (!request.success) return json(res, 400, { error: "title must be text" }); + const task = store.createGroupTask(group.id, request.data.title); if (!task) return json(res, 500, { error: "couldn't create that task" }); const fresh = groupWithThread(store.group(group.id)!); broadcast({ kind: "group", group: fresh }); - return json(res, 201, { group: fresh, task: wireGroupTask(task) }); + return json(res, 201, { group: fresh, task }); } m = path.match(/^\/api\/groups\/([\w-]+)\/tasks\/([\w-]+)$/); @@ -4474,7 +4475,7 @@ const server = createServer(async (req, res) => { const fresh = groupWithThread(switched); broadcast({ kind: "group", group: fresh }); const responseGroup = url.searchParams.get("messages") === "0" - ? { ...publicGroupState(switched), tasks: store.groupTasks(switched.id).map(wireGroupTask) } + ? { ...publicGroupState(switched), tasks: store.groupTasks(switched.id) } : fresh; return json(res, 200, { group: responseGroup }); } @@ -4491,7 +4492,7 @@ const server = createServer(async (req, res) => { } const task = store.renameGroupTask(m[1], m[2], String(body.title ?? "")); if (!task) return json(res, 404, { error: "no such channel task" }); - return json(res, 200, { task: wireGroupTask(task) }); + return json(res, 200, { task }); } if (m && method === "DELETE") { const group = store.group(m[1]); diff --git a/server/store.test.ts b/server/store.test.ts index 1a0565e83..6e2799049 100644 --- a/server/store.test.ts +++ b/server/store.test.ts @@ -539,6 +539,23 @@ describe("Store change stream", () => { expect(events.at(-1)).toEqual({ type: "group.deleted", groupId: g.id }); }); + it("delivers each change to the listener snapshot captured before emission", () => { + const store = new Store(selection); + const bot = store.createBot(); + const seen: string[] = []; + let removeSecond = () => {}; + store.onChange(() => { + seen.push("first"); + removeSecond(); + store.onChange(() => seen.push("late")); + }); + removeSecond = store.onChange(() => seen.push("second")); + + store.patchBot(bot.id, { name: "Snapshot" }); + + expect(seen).toEqual(["first", "second"]); + }); + it("unsubscribe stops delivery", () => { const store = new Store(selection); const bot = store.createBot(); diff --git a/server/store.ts b/server/store.ts index 8575537d3..4cef9dd53 100644 --- a/server/store.ts +++ b/server/store.ts @@ -635,15 +635,14 @@ export class Store { continue; } if (!g.tasks?.length) { - g.tasks = [ - { - threadId: g.threadId, - title: this.firstUserLine(g.threadId) ?? UNTITLED_TASK, - createdAt: g.createdAt, - ...(g.pinnedCwd !== undefined ? { pinnedCwd: g.pinnedCwd } : {}), - ...(g.pinnedMessageId ? { pinnedMessageId: g.pinnedMessageId } : {}), - }, - ]; + const initialTask: GroupTaskRecord = { + threadId: g.threadId, + title: this.firstUserLine(g.threadId) ?? UNTITLED_TASK, + createdAt: g.createdAt, + }; + if (g.pinnedCwd !== undefined) initialTask.pinnedCwd = g.pinnedCwd; + if (g.pinnedMessageId) initialTask.pinnedMessageId = g.pinnedMessageId; + g.tasks = [initialTask]; groupsMigrated = true; } // Repair a malformed/stale active pointer conservatively. Every task @@ -690,7 +689,7 @@ export class Store { } private saveGroups() { - writeFileAtomic(GROUPS_FILE, JSON.stringify(this.groups.map(({ busyBotId, ...g }) => g), null, 2)); + writeFileAtomic(GROUPS_FILE, JSON.stringify(this.groups.map(({ busyBotId: _busyBotId, ...g }) => g), null, 2)); } // ── groups ──────────────────────────────────────────────────────────── @@ -737,9 +736,6 @@ export class Store { const group: GroupRecord = { id: newId(), threadId, - ...(dm - ? {} - : { tasks: [{ threadId, title: UNTITLED_TASK, createdAt }] }), name, memberIds, defaultResponder: dm @@ -751,13 +747,12 @@ export class Store { dm: dm || undefined, busyBotId: null, section, - ...(dm - ? {} - : { - setupCompletedAt: setup?.completed ? createdAt : null, - setupSkippedAt: null, - }), }; + if (!dm) { + group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt }]; + group.setupCompletedAt = setup?.completed ? createdAt : null; + group.setupSkippedAt = null; + } this.groups.unshift(group); this.saveGroups(); this.emit({ type: "group", groupId: group.id }); @@ -835,7 +830,7 @@ export class Store { if (!group || group.dm) return null; const task: GroupTaskRecord = { threadId: newId(), - title: title?.trim() || UNTITLED_TASK, + title: title?.trim().slice(0, 80) || UNTITLED_TASK, createdAt: Date.now(), }; group.tasks = [task, ...(group.tasks ?? [])]; diff --git a/src/state/store.test.ts b/src/state/store.test.ts index b886872ad..928392035 100644 --- a/src/state/store.test.ts +++ b/src/state/store.test.ts @@ -88,7 +88,7 @@ describe("replacement snapshot boundary", () => { }); describe("notification routing", () => { - const bots = [{ id: "bot-1", threadId: "main-thread", tasks: [{ threadId: "detached-thread" }] }] as never; + const bots = [{ id: "bot-1", threadId: "main-thread", tasks: [{ threadId: "detached-thread" }] }]; const groups = [{ id: "room-1", threadId: "room-thread", @@ -96,7 +96,7 @@ describe("notification routing", () => { { threadId: "room-thread", title: "Current", createdAt: 1 }, { threadId: "older-room-thread", title: "Older", createdAt: 0 }, ], - }] as never; + }]; it("selects the bot and switches to the notification's exact task", () => { const dispatch = vi.fn(); diff --git a/src/state/store.tsx b/src/state/store.tsx index 60ac58a2e..a2ff6b59c 100644 --- a/src/state/store.tsx +++ b/src/state/store.tsx @@ -584,10 +584,21 @@ export type Action = patch: BotUpdatePatch; }; +interface NotificationThreadOwner { + id: string; + threadId: string; + tasks?: Array<{ threadId: string }>; +} + +interface NotificationRoutingState { + bots: NotificationThreadOwner[]; + groups: NotificationThreadOwner[]; +} + export function openNotificationTarget( dispatch: (action: Action) => void, target: NotificationTarget, - state: Pick, + state: NotificationRoutingState, ) { // A room's approval/question notification carries the asker bot with the // GROUP's thread id; asking the bot to switch to that thread would 404.