From 4fdfabd5f19e495aa135f5f16760066bdfa49891 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 23 Feb 2026 16:17:48 +0200 Subject: [PATCH] feat: implement inbox locking mechanism and enhance team message handling - Introduced `withInboxLock` function to manage concurrent access to inbox files, preventing race conditions during message processing. - Refactored `TeamInboxWriter` and `TeamProvisioningService` to utilize the new locking mechanism, ensuring safe read/write operations on inbox files. - Updated `TeamListView` and related components to improve user experience during team management and message handling. - Added new dependencies for linting and formatting, enhancing code quality and maintainability. --- .husky/pre-commit | 1 + package.json | 387 +++++++++--------- pnpm-lock.yaml | 202 ++++++++- src/main/services/team/TeamInboxWriter.ts | 21 +- .../services/team/TeamProvisioningService.ts | 81 ++-- src/main/services/team/inboxLock.ts | 19 + src/renderer/components/team/TeamListView.tsx | 39 +- src/renderer/hooks/useTeamMessagesRead.ts | 11 +- 8 files changed, 489 insertions(+), 272 deletions(-) create mode 100644 .husky/pre-commit create mode 100644 src/main/services/team/inboxLock.ts diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..5ee7abd8 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm exec lint-staged diff --git a/package.json b/package.json index 7eaa6de2..5b809d3f 100644 --- a/package.json +++ b/package.json @@ -1,195 +1,208 @@ { - "name": "claude-agent-teams-ui", - "type": "module", - "version": "0.1.0", - "description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls", - "license": "AGPL-3.0", - "author": { - "name": "Илия (777genius)", - "email": "quantjumppro@gmail.com" + "name": "claude-agent-teams-ui", + "type": "module", + "version": "0.1.0", + "description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls", + "license": "AGPL-3.0", + "author": { + "name": "Илия (777genius)", + "email": "quantjumppro@gmail.com" + }, + "homepage": "https://github.com/777genius/claude_agent_teams_ui", + "repository": { + "type": "git", + "url": "https://github.com/777genius/claude_agent_teams_ui.git" + }, + "bugs": { + "url": "https://github.com/777genius/claude_agent_teams_ui/issues" + }, + "main": "dist-electron/main/index.cjs", + "scripts": { + "dev": "electron-vite dev", + "dev:kill": "pkill -f 'electron-vite|electron \\.' 2>/dev/null; echo 'Done'", + "build": "electron-vite build", + "dist": "electron-builder --mac --win --linux", + "dist:mac": "electron-builder --mac --publish always", + "dist:mac:arm64": "electron-builder --mac --arm64 --publish always", + "dist:mac:x64": "electron-builder --mac --x64 --publish always", + "dist:win": "electron-builder --win --publish always", + "dist:linux": "electron-builder --linux --publish always", + "preview": "electron-vite preview", + "typecheck": "tsc --noEmit", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css}\"", + "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build", + "fix": "pnpm lint:fix && pnpm format", + "quality": "pnpm check && pnpm format:check && npx knip", + "test:chunks": "tsx test/test-chunk-building.ts", + "test:semantic": "tsx test/test-semantic-steps.ts", + "test:noise": "tsx test/test-noise-filtering.ts", + "test:task-filtering": "tsx test/test-task-filtering.ts", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts", + "standalone": "tsx src/main/standalone.ts", + "standalone:build": "electron-vite build && vite build --config vite.standalone.config.ts", + "standalone:start": "node dist-standalone/index.cjs", + "prepare": "husky" + }, + "lint-staged": { + "src/**/*.{ts,tsx,js,jsx}": [ + "eslint --fix" + ], + "src/**/*.{ts,tsx,js,jsx,json,css}": [ + "prettier --write" + ] + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fastify/cors": "^11.2.0", + "@fastify/static": "^9.0.0", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-virtual": "^3.10.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "^3.6.0", + "electron-updater": "^6.7.3", + "fastify": "^5.7.4", + "idb-keyval": "^6.2.2", + "lucide-react": "^0.562.0", + "mdast-util-to-hast": "^13.2.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "ssh-config": "^5.0.4", + "ssh2": "^1.17.0", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7", + "unified": "^11.0.5", + "zustand": "^4.5.0" + }, + "devDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.6.0", + "@eslint/js": "^9.39.2", + "@tailwindcss/typography": "^0.5.19", + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "@types/node": "^25.0.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/ssh2": "^1.15.5", + "@vitejs/plugin-react": "^4.3.1", + "@vitest/coverage-v8": "^3.1.4", + "autoprefixer": "^10.4.17", + "electron": "^40.3.0", + "electron-builder": "^25.1.8", + "electron-vite": "^2.3.0", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-boundaries": "^5.3.1", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.26", + "eslint-plugin-security": "^3.0.1", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-sonarjs": "^3.0.6", + "eslint-plugin-tailwindcss": "^3.18.2", + "globals": "^17.2.0", + "happy-dom": "^20.0.2", + "husky": "^9.1.7", + "knip": "^5.82.1", + "lint-staged": "^16.2.7", + "postcss": "^8.4.35", + "prettier": "^3.8.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "tailwindcss": "^3.4.1", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.54.0", + "vite": "^5.4.2", + "vitest": "^3.1.4" + }, + "build": { + "appId": "com.claudecode.context", + "productName": "Claude Agent Teams UI", + "directories": { + "output": "release" }, - "homepage": "https://github.com/777genius/claude_agent_teams_ui", - "repository": { - "type": "git", - "url": "https://github.com/777genius/claude_agent_teams_ui.git" + "files": [ + "out/renderer/**", + "dist-electron/**", + "package.json" + ], + "asar": true, + "asarUnpack": [ + "out/renderer/**" + ], + "npmRebuild": false, + "extraMetadata": { + "main": "dist-electron/main/index.cjs" }, - "bugs": { - "url": "https://github.com/777genius/claude_agent_teams_ui/issues" + "mac": { + "category": "public.app-category.developer-tools", + "target": [ + "dmg", + "zip" + ], + "hardenedRuntime": true, + "gatekeeperAssess": false, + "notarize": true, + "entitlements": "resources/entitlements.mac.plist", + "entitlementsInherit": "resources/entitlements.mac.inherit.plist", + "icon": "resources/icons/mac/icon.icns" }, - "main": "dist-electron/main/index.cjs", - "scripts": { - "dev": "electron-vite dev", - "dev:kill": "pkill -f 'electron-vite|electron \\.' 2>/dev/null; echo 'Done'", - "build": "electron-vite build", - "dist": "electron-builder --mac --win --linux", - "dist:mac": "electron-builder --mac --publish always", - "dist:mac:arm64": "electron-builder --mac --arm64 --publish always", - "dist:mac:x64": "electron-builder --mac --x64 --publish always", - "dist:win": "electron-builder --win --publish always", - "dist:linux": "electron-builder --linux --publish always", - "preview": "electron-vite preview", - "typecheck": "tsc --noEmit", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix", - "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"", - "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css}\"", - "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm build", - "fix": "pnpm lint:fix && pnpm format", - "quality": "pnpm check && pnpm format:check && npx knip", - "test:chunks": "tsx test/test-chunk-building.ts", - "test:semantic": "tsx test/test-semantic-steps.ts", - "test:noise": "tsx test/test-noise-filtering.ts", - "test:task-filtering": "tsx test/test-task-filtering.ts", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts", - "standalone": "tsx src/main/standalone.ts", - "standalone:build": "electron-vite build && vite build --config vite.standalone.config.ts", - "standalone:start": "node dist-standalone/index.cjs" + "dmg": { + "sign": false }, - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@fastify/cors": "^11.2.0", - "@fastify/static": "^9.0.0", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@tanstack/react-virtual": "^3.10.8", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "1.0.4", - "date-fns": "^3.6.0", - "electron-updater": "^6.7.3", - "fastify": "^5.7.4", - "idb-keyval": "^6.2.2", - "lucide-react": "^0.562.0", - "mdast-util-to-hast": "^13.2.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^10.1.0", - "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "ssh-config": "^5.0.4", - "ssh2": "^1.17.0", - "tailwind-merge": "^3.5.0", - "tailwindcss-animate": "^1.0.7", - "unified": "^11.0.5", - "zustand": "^4.5.0" + "win": { + "target": [ + "nsis" + ], + "icon": "resources/icons/win/icon.ico" }, - "devDependencies": { - "@eslint-community/eslint-plugin-eslint-comments": "^4.6.0", - "@eslint/js": "^9.39.2", - "@tailwindcss/typography": "^0.5.19", - "@types/hast": "^3.0.4", - "@types/mdast": "^4.0.4", - "@types/node": "^25.0.7", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@types/ssh2": "^1.15.5", - "@vitejs/plugin-react": "^4.3.1", - "@vitest/coverage-v8": "^3.1.4", - "autoprefixer": "^10.4.17", - "electron": "^40.3.0", - "electron-builder": "^25.1.8", - "electron-vite": "^2.3.0", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.4", - "eslint-plugin-boundaries": "^5.3.1", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.26", - "eslint-plugin-security": "^3.0.1", - "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-sonarjs": "^3.0.6", - "eslint-plugin-tailwindcss": "^3.18.2", - "globals": "^17.2.0", - "happy-dom": "^20.0.2", - "knip": "^5.82.1", - "postcss": "^8.4.35", - "prettier": "^3.8.1", - "prettier-plugin-tailwindcss": "^0.7.2", - "tailwindcss": "^3.4.1", - "tsx": "^4.21.0", - "typescript": "^5.9.3", - "typescript-eslint": "^8.54.0", - "vite": "^5.4.2", - "vitest": "^3.1.4" + "linux": { + "target": [ + "AppImage", + "deb", + "rpm", + "pacman" + ], + "icon": "resources/icons/png", + "category": "Development" }, - "build": { - "appId": "com.claudecode.context", - "productName": "Claude Agent Teams UI", - "directories": { - "output": "release" - }, - "files": [ - "out/renderer/**", - "dist-electron/**", - "package.json" - ], - "asar": true, - "asarUnpack": [ - "out/renderer/**" - ], - "npmRebuild": false, - "extraMetadata": { - "main": "dist-electron/main/index.cjs" - }, - "mac": { - "category": "public.app-category.developer-tools", - "target": [ - "dmg", - "zip" - ], - "hardenedRuntime": true, - "gatekeeperAssess": false, - "notarize": true, - "entitlements": "resources/entitlements.mac.plist", - "entitlementsInherit": "resources/entitlements.mac.inherit.plist", - "icon": "resources/icons/mac/icon.icns" - }, - "dmg": { - "sign": false - }, - "win": { - "target": [ - "nsis" - ], - "icon": "resources/icons/win/icon.ico" - }, - "linux": { - "target": [ - "AppImage", - "deb", - "rpm", - "pacman" - ], - "icon": "resources/icons/png", - "category": "Development" - }, - "deb": { - "afterInstall": "resources/afterInstall.sh" - }, - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true - }, - "publish": [{ - "provider": "github", - "releaseType": "draft" - }] + "deb": { + "afterInstall": "resources/afterInstall.sh" }, - "packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501" -} \ No newline at end of file + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true + }, + "publish": [ + { + "provider": "github", + "releaseType": "draft" + } + ] + }, + "packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e40b1b9..2a438d88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,9 +204,15 @@ importers: happy-dom: specifier: ^20.0.2 version: 20.0.2 + husky: + specifier: ^9.1.7 + version: 9.1.7 knip: specifier: ^5.82.1 version: 5.82.1(@types/node@25.0.7)(typescript@5.9.3) + lint-staged: + specifier: ^16.2.7 + version: 16.2.7 postcss: specifier: ^8.4.35 version: 8.5.6 @@ -1920,6 +1926,10 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2285,6 +2295,10 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -2293,6 +2307,10 @@ packages: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} + cli-truncate@5.1.1: + resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} + engines: {node: '>=20'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2325,6 +2343,9 @@ packages: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2332,6 +2353,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2588,6 +2613,9 @@ packages: engines: {node: '>= 12.20.55'} hasBin: true + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2604,6 +2632,10 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} @@ -2829,6 +2861,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -3004,6 +3039,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -3171,6 +3210,11 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} @@ -3295,6 +3339,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-generator-function@1.1.2: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} @@ -3521,6 +3569,15 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lint-staged@16.2.7: + resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3557,6 +3614,10 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -3778,6 +3839,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -3858,6 +3923,10 @@ packages: nan@2.25.0: resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==} + nano-spawn@2.0.0: + resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} + engines: {node: '>=20.17'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3961,6 +4030,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4050,6 +4123,11 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -4390,6 +4468,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} @@ -4554,6 +4636,10 @@ packages: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -4627,6 +4713,10 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4635,6 +4725,14 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -5094,6 +5192,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -6785,6 +6887,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -7304,6 +7410,10 @@ snapshots: dependencies: restore-cursor: 3.1.0 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + cli-spinners@2.9.2: {} cli-truncate@2.1.0: @@ -7312,6 +7422,11 @@ snapshots: string-width: 4.2.3 optional: true + cli-truncate@5.1.1: + dependencies: + slice-ansi: 7.1.2 + string-width: 8.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -7346,12 +7461,16 @@ snapshots: color-support@1.1.3: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 comma-separated-tokens@2.0.3: {} + commander@14.0.3: {} + commander@2.20.3: optional: true @@ -7653,6 +7772,8 @@ snapshots: transitivePeerDependencies: - supports-color + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7668,6 +7789,8 @@ snapshots: env-paths@2.2.1: {} + environment@1.1.0: {} + err-code@2.0.3: {} es-abstract@1.24.1: @@ -8085,6 +8208,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.4: {} + expect-type@1.3.0: {} exponential-backoff@3.1.3: {} @@ -8292,6 +8417,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8528,6 +8655,8 @@ snapshots: dependencies: ms: 2.1.3 + husky@9.1.7: {} + iconv-corefoundation@1.1.7: dependencies: cli-truncate: 2.1.0 @@ -8645,6 +8774,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -8871,6 +9004,25 @@ snapshots: lines-and-columns@1.2.4: {} + lint-staged@16.2.7: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + micromatch: 4.0.8 + nano-spawn: 2.0.0 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.2 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.1.1 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -8898,6 +9050,14 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -9334,6 +9494,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + mimic-response@1.0.1: {} mimic-response@3.1.0: {} @@ -9410,6 +9572,8 @@ snapshots: nan@2.25.0: optional: true + nano-spawn@2.0.0: {} + nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -9519,6 +9683,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -9631,6 +9799,8 @@ snapshots: picomatch@4.0.3: {} + pidtree@0.6.0: {} + pify@2.3.0: {} pino-abstract-transport@3.0.0: @@ -9955,6 +10125,11 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + ret@0.5.0: {} retry@0.12.0: {} @@ -10158,6 +10333,11 @@ snapshots: is-fullwidth-code-point: 3.0.0 optional: true + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + smart-buffer@4.2.0: {} smol-toml@1.6.0: {} @@ -10224,6 +10404,8 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -10236,6 +10418,17 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.2 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.2 + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 @@ -10826,6 +11019,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} xmlbuilder@15.1.1: {} @@ -10836,8 +11035,7 @@ snapshots: yallist@4.0.0: {} - yaml@2.8.2: - optional: true + yaml@2.8.2: {} yargs-parser@21.1.1: {} diff --git a/src/main/services/team/TeamInboxWriter.ts b/src/main/services/team/TeamInboxWriter.ts index 7dce744a..3fa5badf 100644 --- a/src/main/services/team/TeamInboxWriter.ts +++ b/src/main/services/team/TeamInboxWriter.ts @@ -4,29 +4,10 @@ import * as fs from 'fs'; import * as path from 'path'; import { atomicWriteAsync } from './atomicWrite'; +import { withInboxLock } from './inboxLock'; import type { InboxMessage, SendMessageRequest, SendMessageResult } from '@shared/types'; -const writeLocks = new Map>(); - -async function withInboxLock(inboxPath: string, fn: () => Promise): Promise { - const prev = writeLocks.get(inboxPath) ?? Promise.resolve(); - let release!: () => void; - const mine = new Promise((resolve) => { - release = resolve; - }); - writeLocks.set(inboxPath, mine); - await prev; - try { - return await fn(); - } finally { - release(); - if (writeLocks.get(inboxPath) === mine) { - writeLocks.delete(inboxPath); - } - } -} - export class TeamInboxWriter { async sendMessage(teamName: string, request: SendMessageRequest): Promise { const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${request.member}.json`); diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index ece0d1e4..645eceb6 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -18,6 +18,7 @@ import { promisify } from 'util'; import { atomicWriteAsync } from './atomicWrite'; import { ClaudeBinaryResolver } from './ClaudeBinaryResolver'; +import { withInboxLock } from './inboxLock'; import { TeamConfigReader } from './TeamConfigReader'; import { TeamInboxReader } from './TeamInboxReader'; import { TeamMembersMetaStore } from './TeamMembersMetaStore'; @@ -1235,54 +1236,56 @@ export class TeamProvisioningService { ): Promise { const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${member}.json`); - let raw: string; - try { - raw = await fs.promises.readFile(inboxPath, 'utf8'); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + await withInboxLock(inboxPath, async () => { + let raw: string; + try { + raw = await fs.promises.readFile(inboxPath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return; + } + throw error; + } + + let parsed: unknown; + try { + parsed = JSON.parse(raw) as unknown; + } catch { return; } - throw error; - } + if (!Array.isArray(parsed)) return; - let parsed: unknown; - try { - parsed = JSON.parse(raw) as unknown; - } catch { - return; - } - if (!Array.isArray(parsed)) return; + const ids = new Set(messages.map((m) => m.messageId).filter((id): id is string => !!id)); + const fallbackKeys = new Set( + messages.filter((m) => !m.messageId).map((m) => `${m.timestamp}\0${m.from}\0${m.text}`) + ); - const ids = new Set(messages.map((m) => m.messageId).filter((id): id is string => !!id)); - const fallbackKeys = new Set( - messages.filter((m) => !m.messageId).map((m) => `${m.timestamp}\0${m.from}\0${m.text}`) - ); + let changed = false; + for (const item of parsed) { + if (!item || typeof item !== 'object') continue; + const row = item as Record; + const msgId = typeof row.messageId === 'string' ? row.messageId : null; + const timestamp = typeof row.timestamp === 'string' ? row.timestamp : null; + const from = typeof row.from === 'string' ? row.from : null; + const text = typeof row.text === 'string' ? row.text : null; - let changed = false; - for (const item of parsed) { - if (!item || typeof item !== 'object') continue; - const row = item as Record; - const msgId = typeof row.messageId === 'string' ? row.messageId : null; - const timestamp = typeof row.timestamp === 'string' ? row.timestamp : null; - const from = typeof row.from === 'string' ? row.from : null; - const text = typeof row.text === 'string' ? row.text : null; + const matchesId = msgId ? ids.has(msgId) : false; + const matchesFallback = + !msgId && timestamp && from && text + ? fallbackKeys.has(`${timestamp}\0${from}\0${text}`) + : false; - const matchesId = msgId ? ids.has(msgId) : false; - const matchesFallback = - !msgId && timestamp && from && text - ? fallbackKeys.has(`${timestamp}\0${from}\0${text}`) - : false; + if (!matchesId && !matchesFallback) continue; - if (!matchesId && !matchesFallback) continue; - - if (row.read !== true) { - row.read = true; - changed = true; + if (row.read !== true) { + row.read = true; + changed = true; + } } - } - if (!changed) return; - await atomicWriteAsync(inboxPath, JSON.stringify(parsed, null, 2)); + if (!changed) return; + await atomicWriteAsync(inboxPath, JSON.stringify(parsed, null, 2)); + }); } private trimRelayedSet(set: Set): Set { diff --git a/src/main/services/team/inboxLock.ts b/src/main/services/team/inboxLock.ts new file mode 100644 index 00000000..96ce677c --- /dev/null +++ b/src/main/services/team/inboxLock.ts @@ -0,0 +1,19 @@ +const writeLocks = new Map>(); + +export async function withInboxLock(inboxPath: string, fn: () => Promise): Promise { + const prev = writeLocks.get(inboxPath) ?? Promise.resolve(); + let release!: () => void; + const mine = new Promise((resolve) => { + release = resolve; + }); + writeLocks.set(inboxPath, mine); + await prev; + try { + return await fn(); + } finally { + release(); + if (writeLocks.get(inboxPath) === mine) { + writeLocks.delete(inboxPath); + } + } +} diff --git a/src/renderer/components/team/TeamListView.tsx b/src/renderer/components/team/TeamListView.tsx index e006b076..b80e51e0 100644 --- a/src/renderer/components/team/TeamListView.tsx +++ b/src/renderer/components/team/TeamListView.tsx @@ -21,7 +21,7 @@ import { CreateTeamDialog } from './dialogs/CreateTeamDialog'; import { TeamEmptyState } from './TeamEmptyState'; import type { TeamCopyData } from './dialogs/CreateTeamDialog'; -import type { TeamProvisioningProgress, TeamSummary } from '@shared/types'; +import type { TeamCreateRequest, TeamProvisioningProgress, TeamSummary } from '@shared/types'; function generateUniqueName(sourceName: string, existingNames: string[]): string { const base = sourceName.replace(/-\d+$/, ''); @@ -244,17 +244,15 @@ export const TeamListView = (): React.JSX.Element => { ); const [stoppingTeamName, setStoppingTeamName] = useState(null); - const handleStopTeam = useCallback((teamName: string, e: React.MouseEvent) => { + const handleStopTeam = useCallback(async (teamName: string, e: React.MouseEvent) => { e.stopPropagation(); setStoppingTeamName(teamName); - void api.teams - .stop(teamName) - .then(() => { - setAliveTeams((prev) => prev.filter((n) => n !== teamName)); - }) - .finally(() => { - setStoppingTeamName(null); - }); + try { + await api.teams.stop(teamName); + setAliveTeams((prev) => prev.filter((n) => n !== teamName)); + } finally { + setStoppingTeamName(null); + } }, []); useEffect(() => { @@ -267,6 +265,18 @@ export const TeamListView = (): React.JSX.Element => { const taskCountsByTeam = useMemo(() => buildTaskCountsByTeam(globalTasks), [globalTasks]); + const handleCreateDialogClose = useCallback(() => { + setShowCreateDialog(false); + setCopyData(null); + }, []); + + const handleCreateSubmit = useCallback( + async (request: TeamCreateRequest) => { + await createTeam(request); + }, + [createTeam] + ); + if (!electronMode) { return (
@@ -290,13 +300,8 @@ export const TeamListView = (): React.JSX.Element => { existingTeamNames={teams.map((t) => t.teamName)} initialData={copyData ?? undefined} defaultProjectPath={currentProjectPath} - onClose={() => { - setShowCreateDialog(false); - setCopyData(null); - }} - onCreate={async (request) => { - await createTeam(request); - }} + onClose={handleCreateDialogClose} + onCreate={handleCreateSubmit} onOpenTeam={openTeamTab} /> ); diff --git a/src/renderer/hooks/useTeamMessagesRead.ts b/src/renderer/hooks/useTeamMessagesRead.ts index b0f4eaf9..dd14df40 100644 --- a/src/renderer/hooks/useTeamMessagesRead.ts +++ b/src/renderer/hooks/useTeamMessagesRead.ts @@ -10,13 +10,10 @@ export function useTeamMessagesRead(teamName: string): { markRead: (messageKey: string) => void; } { const [version, setVersion] = useState(0); - const readSet = useMemo( - () => { - if (version < 0) return new Set(); - return teamName ? getReadSetStorage(teamName) : new Set(); - }, - [teamName, version] - ); + const readSet = useMemo(() => { + if (version < 0) return new Set(); + return teamName ? getReadSetStorage(teamName) : new Set(); + }, [teamName, version]); const markRead = useCallback( (messageKey: string) => {