From 97f1e9b7c4facd1c8804bd9b6e7247bb4905168a Mon Sep 17 00:00:00 2001 From: milind-soni Date: Fri, 28 Aug 2026 10:02:05 +0530 Subject: [PATCH] refactor(channels): clean task boundaries --- package.json | 4 +- pnpm-lock.yaml | 228 +++++++++++++++++++++++++++++++++++-- server/group-tasks.test.ts | 9 ++ server/index.test.ts | 1 + server/index.ts | 37 +++--- server/store.ts | 35 +++--- src/state/store.test.ts | 4 +- src/state/store.tsx | 13 ++- 8 files changed, 285 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index db419ca09..929acdba7 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,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", @@ -100,7 +100,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..84e8511ec 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 @@ -1328,42 +1328,84 @@ packages: cpu: [arm] os: [android] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1371,6 +1413,13 @@ packages: os: [linux] libc: [glibc] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1378,6 +1427,13 @@ packages: os: [linux] libc: [musl] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1385,6 +1441,13 @@ packages: os: [linux] libc: [glibc] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1392,6 +1455,13 @@ packages: os: [linux] libc: [glibc] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1399,6 +1469,13 @@ packages: os: [linux] libc: [musl] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1406,6 +1483,13 @@ packages: os: [linux] libc: [glibc] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1413,6 +1497,13 @@ packages: os: [linux] libc: [glibc] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1420,32 +1511,63 @@ packages: os: [linux] libc: [musl] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@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==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] + '@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==} 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/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.80.0': + resolution: {integrity: sha512-QRgH1XqQEYNHa4f1vvPQ5fAdNdncHGIUG1ZWLlGIZHky3qwCEeAKYitZNbZMtaXtAQAAFFTOwqUfzESvimqZNA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@peculiar/asn1-schema@2.8.0': @@ -3457,6 +3579,19 @@ packages: vite-plus: optional: true + oxlint@1.80.0: + resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -5177,61 +5312,118 @@ snapshots: '@oxlint/binding-android-arm-eabi@1.78.0': optional: true + '@oxlint/binding-android-arm-eabi@1.80.0': + optional: true + '@oxlint/binding-android-arm64@1.78.0': optional: true + '@oxlint/binding-android-arm64@1.80.0': + optional: true + '@oxlint/binding-darwin-arm64@1.78.0': optional: true + '@oxlint/binding-darwin-arm64@1.80.0': + optional: true + '@oxlint/binding-darwin-x64@1.78.0': optional: true + '@oxlint/binding-darwin-x64@1.80.0': + optional: true + '@oxlint/binding-freebsd-x64@1.78.0': optional: true + '@oxlint/binding-freebsd-x64@1.80.0': + optional: true + '@oxlint/binding-linux-arm-gnueabihf@1.78.0': optional: true + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + optional: true + '@oxlint/binding-linux-arm-musleabihf@1.78.0': optional: true + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + optional: true + '@oxlint/binding-linux-arm64-gnu@1.78.0': optional: true + '@oxlint/binding-linux-arm64-gnu@1.80.0': + optional: true + '@oxlint/binding-linux-arm64-musl@1.78.0': optional: true + '@oxlint/binding-linux-arm64-musl@1.80.0': + optional: true + '@oxlint/binding-linux-ppc64-gnu@1.78.0': optional: true + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + optional: true + '@oxlint/binding-linux-riscv64-gnu@1.78.0': optional: true + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + optional: true + '@oxlint/binding-linux-riscv64-musl@1.78.0': optional: true + '@oxlint/binding-linux-riscv64-musl@1.80.0': + optional: true + '@oxlint/binding-linux-s390x-gnu@1.78.0': optional: true + '@oxlint/binding-linux-s390x-gnu@1.80.0': + optional: true + '@oxlint/binding-linux-x64-gnu@1.78.0': optional: true + '@oxlint/binding-linux-x64-gnu@1.80.0': + optional: true + '@oxlint/binding-linux-x64-musl@1.78.0': optional: true + '@oxlint/binding-linux-x64-musl@1.80.0': + optional: true + '@oxlint/binding-openharmony-arm64@1.78.0': optional: true + '@oxlint/binding-openharmony-arm64@1.80.0': + optional: true + '@oxlint/binding-win32-arm64-msvc@1.78.0': optional: true + '@oxlint/binding-win32-arm64-msvc@1.80.0': + optional: true + '@oxlint/binding-win32-ia32-msvc@1.78.0': optional: true + '@oxlint/binding-win32-ia32-msvc@1.80.0': + optional: true + '@oxlint/binding-win32-x64-msvc@1.78.0': optional: true - '@oxlint/plugins@1.78.0': {} + '@oxlint/binding-win32-x64-msvc@1.80.0': + optional: true + + '@oxlint/plugins@1.80.0': {} '@peculiar/asn1-schema@2.8.0': dependencies: @@ -7538,6 +7730,28 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.78.0 '@oxlint/binding-win32-x64-msvc': 1.78.0 + oxlint@1.80.0: + optionalDependencies: + '@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: {} p-limit@3.1.0: 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 29a23f786..bee98f474 100644 --- a/server/index.test.ts +++ b/server/index.test.ts @@ -482,6 +482,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 f0244c2b9..f4971e839 100644 --- a/server/index.ts +++ b/server/index.ts @@ -91,7 +91,6 @@ import { Store, type GroupDefaultResponder, type GroupRecord, - type GroupTaskRecord, type Message, type TaskRecord, } from "./store.ts"; @@ -208,6 +207,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. @@ -324,11 +324,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) } : {}) }; }; @@ -346,12 +345,15 @@ const publicBot = (bot: NonNullable>) => ({ tasks: store.tasks(bot.id).map(wireTask), }); -const groupWithThread = (group: GroupRecord) => ({ - ...group, - messages: store.messagesFor(group.threadId), - activeLeafId: store.activeLeaf(group.threadId), - ...(group.dm ? {} : { tasks: store.groupTasks(group.id).map(wireGroupTask) }), -}); +const groupWithThread = (group: GroupRecord) => { + const wireGroup = { + ...group, + messages: store.messagesFor(group.threadId), + activeLeafId: store.activeLeaf(group.threadId), + }; + if (group.dm) return wireGroup; + return { ...wireGroup, tasks: store.groupTasks(group.id) }; +}; // The store tells us what it wrote; this is the ONE place that turns those // into SSE frames. No mutation path can persist without emitting — the @@ -413,7 +415,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 }; } @@ -488,7 +490,7 @@ function broadcast(payload: Record) { // detection stays honest, but never retain their base64 payloads. replayBuffer.push({ seq, kind, frame: kind === "screen" ? null : frame }); if (replayBuffer.length > REPLAY_MAX) replayBuffer.shift(); - for (const client of [...sseClients]) { + for (const client of sseClients) { if (!wants(client, kind)) continue; try { client.res.write(frame); @@ -3419,7 +3421,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"`, @@ -3832,12 +3834,13 @@ const server = createServer(async (req, res) => { if (channelTaskBlocked(group)) { return json(res, 409, { error: "this channel is working or waiting on you — finish that turn first" }); } - const body = await readBody(req); - const task = store.createGroupTask(group.id, typeof body.title === "string" ? body.title : undefined); + const request = createGroupTaskRequestSchema.safeParse(await readBody(req)); + 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-]+)$/); @@ -3864,7 +3867,7 @@ const server = createServer(async (req, res) => { const body = await readBody(req); 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.ts b/server/store.ts index 9e60908f6..fefe7f90d 100644 --- a/server/store.ts +++ b/server/store.ts @@ -592,15 +592,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 @@ -647,7 +646,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 ──────────────────────────────────────────────────────────── @@ -659,7 +658,7 @@ export class Store { } private emit(change: StoreChange) { - for (const listener of [...this.listeners]) { + for (const listener of this.listeners) { try { listener(change); } catch (error) { @@ -680,23 +679,25 @@ export class Store { createGroup(name: string, memberIds: string[], dm = false, section?: string): GroupRecord { const threadId = newId(); + const createdAt = Date.now(); const group: GroupRecord = { id: newId(), threadId, - ...(dm - ? {} - : { tasks: [{ threadId, title: UNTITLED_TASK, createdAt: Date.now() }] }), name, memberIds, defaultResponder: dm ? { kind: "mentions" } : { kind: "member", botId: memberIds[0] }, bulletin: "", unread: false, - createdAt: Date.now(), + createdAt, dm: dm || undefined, busyBotId: null, section, - ...(dm ? {} : { setupCompletedAt: null, setupSkippedAt: null }), }; + if (!dm) { + group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt }]; + group.setupCompletedAt = null; + group.setupSkippedAt = null; + } this.groups.unshift(group); this.saveGroups(); this.emit({ type: "group", groupId: group.id }); @@ -773,7 +774,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 59bb29ebc..426b1354e 100644 --- a/src/state/store.test.ts +++ b/src/state/store.test.ts @@ -11,7 +11,7 @@ import { } from "./store"; 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", @@ -19,7 +19,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 1e126ebc8..fa4d09a4a 100644 --- a/src/state/store.tsx +++ b/src/state/store.tsx @@ -516,10 +516,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.