chore(merge): sync local dev into spike/team-snapshot-split-plan
This commit is contained in:
commit
82ca8e29c2
23 changed files with 1449 additions and 108 deletions
|
|
@ -143,6 +143,7 @@
|
|||
"motion": "12.38.0",
|
||||
"node-diff3": "^3.2.0",
|
||||
"node-pty": "^1.1.0",
|
||||
"pidusage": "4.0.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-grid-layout": "^2.2.2",
|
||||
|
|
@ -174,6 +175,7 @@
|
|||
"@types/hast": "^3.0.4",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^25.0.7",
|
||||
"@types/pidusage": "2.0.5",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
|
|
|
|||
101
pnpm-lock.yaml
101
pnpm-lock.yaml
|
|
@ -239,6 +239,9 @@ importers:
|
|||
node-pty:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
pidusage:
|
||||
specifier: 4.0.1
|
||||
version: 4.0.1
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4
|
||||
|
|
@ -327,6 +330,9 @@ importers:
|
|||
'@types/node':
|
||||
specifier: ^25.0.7
|
||||
version: 25.0.7
|
||||
'@types/pidusage':
|
||||
specifier: 2.0.5
|
||||
version: 2.0.5
|
||||
'@types/react':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
|
|
@ -451,13 +457,13 @@ importers:
|
|||
version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))
|
||||
'@vueuse/nuxt':
|
||||
specifier: ^10.11.1
|
||||
version: 10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
version: 10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
nuxt:
|
||||
specifier: ^3.20.2
|
||||
version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||
version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2)
|
||||
nuxt-icon:
|
||||
specifier: ^0.6.10
|
||||
version: 0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
version: 0.6.10(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
||||
|
|
@ -482,7 +488,7 @@ importers:
|
|||
version: 7.4.47
|
||||
'@nuxt/eslint':
|
||||
specifier: ^1.12.1
|
||||
version: 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))
|
||||
eslint:
|
||||
specifier: ^9.39.2
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
|
|
@ -494,7 +500,7 @@ importers:
|
|||
version: 1.98.0
|
||||
vite-plugin-vuetify:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
version: 2.1.3(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
|
||||
mcp-server:
|
||||
dependencies:
|
||||
|
|
@ -4353,6 +4359,9 @@ packages:
|
|||
'@types/pg@8.15.6':
|
||||
resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==}
|
||||
|
||||
'@types/pidusage@2.0.5':
|
||||
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
||||
|
||||
'@types/plist@3.0.5':
|
||||
resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
|
||||
|
||||
|
|
@ -8591,6 +8600,10 @@ packages:
|
|||
engines: {node: '>=0.10'}
|
||||
hasBin: true
|
||||
|
||||
pidusage@4.0.1:
|
||||
resolution: {integrity: sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
pify@2.3.0:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -12589,20 +12602,20 @@ snapshots:
|
|||
|
||||
'@nuxt/devalue@2.0.2': {}
|
||||
|
||||
'@nuxt/devtools-kit@1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@nuxt/devtools-kit@1.7.0(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
'@nuxt/schema': 3.21.2
|
||||
execa: 7.2.0
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||
execa: 8.0.1
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
|
|
@ -12617,9 +12630,9 @@ snapshots:
|
|||
pkg-types: 2.3.0
|
||||
semver: 7.7.4
|
||||
|
||||
'@nuxt/devtools@3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
||||
'@nuxt/devtools@3.2.4(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))
|
||||
'@nuxt/devtools-wizard': 3.2.4
|
||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||
'@vue/devtools-core': 8.1.0(vue@3.5.30(typescript@5.9.3))
|
||||
|
|
@ -12647,9 +12660,9 @@ snapshots:
|
|||
sirv: 3.0.2
|
||||
structured-clone-es: 2.0.0
|
||||
tinyglobby: 0.2.15
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite-plugin-vue-tracer: 1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)
|
||||
vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))
|
||||
vite-plugin-vue-tracer: 1.3.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))
|
||||
which: 6.0.1
|
||||
ws: 8.20.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -12698,10 +12711,10 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))':
|
||||
dependencies:
|
||||
'@eslint/config-inspector': 1.5.0(eslint@9.39.2(jiti@2.6.1))
|
||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))
|
||||
'@nuxt/eslint-config': 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@nuxt/eslint-plugin': 1.15.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||
|
|
@ -12777,7 +12790,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/nitro-server@3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)':
|
||||
'@nuxt/nitro-server@3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@nuxt/devalue': 2.0.2
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
|
|
@ -12795,7 +12808,7 @@ snapshots:
|
|||
klona: 2.0.6
|
||||
mocked-exports: 0.1.1
|
||||
nitropack: 2.13.2(encoding@0.1.13)(idb-keyval@6.2.2)
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2)
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
pkg-types: 2.3.0
|
||||
|
|
@ -12860,7 +12873,7 @@ snapshots:
|
|||
rc9: 3.0.0
|
||||
std-env: 3.10.0
|
||||
|
||||
'@nuxt/vite-builder@3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)':
|
||||
'@nuxt/vite-builder@3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
'@rollup/plugin-replace': 6.0.3(rollup@4.60.0)
|
||||
|
|
@ -12879,7 +12892,7 @@ snapshots:
|
|||
magic-string: 0.30.21
|
||||
mlly: 1.8.2
|
||||
mocked-exports: 0.1.1
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2)
|
||||
nypm: 0.6.5
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
|
|
@ -15002,6 +15015,8 @@ snapshots:
|
|||
pg-protocol: 1.13.0
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/pidusage@2.0.5': {}
|
||||
|
||||
'@types/plist@3.0.5':
|
||||
dependencies:
|
||||
'@types/node': 25.0.7
|
||||
|
|
@ -15595,13 +15610,13 @@ snapshots:
|
|||
|
||||
'@vueuse/metadata@10.11.1': {}
|
||||
|
||||
'@vueuse/nuxt@10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
||||
'@vueuse/nuxt@10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
'@vueuse/core': 10.11.1(vue@3.5.30(typescript@5.9.3))
|
||||
'@vueuse/metadata': 10.11.1
|
||||
local-pkg: 0.5.1
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2)
|
||||
vue-demi: 0.14.10(vue@3.5.30(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
|
@ -20143,27 +20158,27 @@ snapshots:
|
|||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nuxt-icon@0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)):
|
||||
nuxt-icon@0.6.10(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@iconify/collections': 1.0.665
|
||||
'@iconify/vue': 4.3.0(vue@3.5.30(typescript@5.9.3))
|
||||
'@nuxt/devtools-kit': 1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@nuxt/devtools-kit': 1.7.0(magicast@0.5.2)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
- vite
|
||||
- vue
|
||||
|
||||
nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2):
|
||||
nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@dxup/nuxt': 0.4.0(magicast@0.5.2)(typescript@5.9.3)
|
||||
'@nuxt/cli': 3.34.0(@nuxt/schema@3.21.2)(cac@6.7.14)(magicast@0.5.2)
|
||||
'@nuxt/devtools': 3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
'@nuxt/devtools': 3.2.4(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
'@nuxt/nitro-server': 3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)
|
||||
'@nuxt/nitro-server': 3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(typescript@5.9.3)
|
||||
'@nuxt/schema': 3.21.2
|
||||
'@nuxt/telemetry': 2.7.0(@nuxt/kit@3.21.2(magicast@0.5.2))
|
||||
'@nuxt/vite-builder': 3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)
|
||||
'@nuxt/vite-builder': 3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)
|
||||
'@unhead/vue': 2.1.12(vue@3.5.30(typescript@5.9.3))
|
||||
'@vue/shared': 3.5.30
|
||||
c12: 3.3.3(magicast@0.5.2)
|
||||
|
|
@ -20647,6 +20662,10 @@ snapshots:
|
|||
|
||||
pidtree@0.6.0: {}
|
||||
|
||||
pidusage@4.0.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)):
|
||||
|
|
@ -22710,15 +22729,15 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-dev-rpc@1.1.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-hot-client: 2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)
|
||||
vite-hot-client: 2.1.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))
|
||||
|
||||
vite-hot-client@2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-hot-client@2.1.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)):
|
||||
dependencies:
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)
|
||||
|
||||
vite-node@3.2.4(@types/node@22.19.15)(sass@1.98.0)(terser@5.46.0):
|
||||
dependencies:
|
||||
|
|
@ -22792,7 +22811,7 @@ snapshots:
|
|||
optionator: 0.9.4
|
||||
typescript: 5.9.3
|
||||
|
||||
vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
|
|
@ -22802,29 +22821,29 @@ snapshots:
|
|||
perfect-debounce: 2.1.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)
|
||||
vite-dev-rpc: 1.1.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-tracer@1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)):
|
||||
vite-plugin-vue-tracer@1.3.0(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3)):
|
||||
dependencies:
|
||||
estree-walker: 3.0.3
|
||||
exsolve: 1.0.8
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
source-map-js: 1.2.1
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
|
||||
vite-plugin-vuetify@2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3):
|
||||
vite-plugin-vuetify@2.1.3(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3):
|
||||
dependencies:
|
||||
'@vuetify/loader-shared': 2.1.2(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
debug: 4.4.3
|
||||
upath: 2.0.1
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0)
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
vuetify: 3.12.3(typescript@5.9.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.30(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -23023,7 +23042,7 @@ snapshots:
|
|||
vue: 3.5.30(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
vite-plugin-vuetify: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
vite-plugin-vuetify: 2.1.3(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ export async function killTmuxPaneForCurrentPlatform(paneId: string): Promise<vo
|
|||
invalidateTmuxRuntimeStatusCache();
|
||||
}
|
||||
|
||||
export async function listTmuxPanePidsForCurrentPlatform(
|
||||
paneIds: readonly string[]
|
||||
): Promise<Map<string, number>> {
|
||||
return runtimeCommandExecutor.listPanePids(paneIds);
|
||||
}
|
||||
|
||||
export function killTmuxPaneForCurrentPlatformSync(paneId: string): void {
|
||||
runtimeCommandExecutor.killPaneSync(paneId);
|
||||
invalidateTmuxRuntimeStatusCache();
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ export {
|
|||
isTmuxRuntimeReadyForCurrentPlatform,
|
||||
killTmuxPaneForCurrentPlatform,
|
||||
killTmuxPaneForCurrentPlatformSync,
|
||||
listTmuxPanePidsForCurrentPlatform,
|
||||
} from './composition/runtimeSupport';
|
||||
|
|
|
|||
|
|
@ -54,6 +54,36 @@ export class TmuxPlatformCommandExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
async listPanePids(paneIds: readonly string[]): Promise<Map<string, number>> {
|
||||
const normalizedPaneIds = [...new Set(paneIds.map((paneId) => paneId.trim()).filter(Boolean))];
|
||||
if (normalizedPaneIds.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const result = await this.execTmux(
|
||||
['list-panes', '-a', '-F', '#{pane_id}\t#{pane_pid}'],
|
||||
3_000
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr || 'Failed to list tmux panes');
|
||||
}
|
||||
|
||||
const wanted = new Set(normalizedPaneIds);
|
||||
const panePidById = new Map<string, number>();
|
||||
for (const line of result.stdout.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
const [paneId = '', rawPid = ''] = trimmed.split('\t');
|
||||
const normalizedPaneId = paneId.trim();
|
||||
if (!wanted.has(normalizedPaneId)) continue;
|
||||
const pid = Number.parseInt(rawPid.trim(), 10);
|
||||
if (Number.isFinite(pid) && pid > 0) {
|
||||
panePidById.set(normalizedPaneId, pid);
|
||||
}
|
||||
}
|
||||
return panePidById;
|
||||
}
|
||||
|
||||
killPaneSync(paneId: string): void {
|
||||
if (process.platform === 'win32') {
|
||||
const preferredDistro = this.#wslService.getPersistedPreferredDistroSync();
|
||||
|
|
|
|||
|
|
@ -68,4 +68,26 @@ describe('TmuxPlatformCommandExecutor', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('lists pane pids for the requested pane ids only', async () => {
|
||||
const executor = new TmuxPlatformCommandExecutor(
|
||||
{
|
||||
getPersistedPreferredDistroSync: () => null,
|
||||
} as never,
|
||||
{} as never
|
||||
);
|
||||
vi.spyOn(executor, 'execTmux').mockResolvedValue({
|
||||
exitCode: 0,
|
||||
stdout: '%1\t111\n%2\t222\n%3\tnot-a-pid\n',
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
await expect(executor.listPanePids(['%2', '%3', '%2'])).resolves.toEqual(
|
||||
new Map([['%2', 222]])
|
||||
);
|
||||
expect(executor.execTmux).toHaveBeenCalledWith(
|
||||
['list-panes', '-a', '-F', '#{pane_id}\t#{pane_pid}'],
|
||||
3_000
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
TEAM_DELETE_TEAM,
|
||||
TEAM_GET_ALL_TASKS,
|
||||
TEAM_GET_ATTACHMENTS,
|
||||
TEAM_GET_AGENT_RUNTIME,
|
||||
TEAM_GET_CLAUDE_LOGS,
|
||||
TEAM_GET_DATA,
|
||||
TEAM_GET_DELETED_TASKS,
|
||||
|
|
@ -51,6 +52,7 @@ import {
|
|||
TEAM_REMOVE_TASK_RELATIONSHIP,
|
||||
TEAM_REPLACE_MEMBERS,
|
||||
TEAM_REQUEST_REVIEW,
|
||||
TEAM_RESTART_MEMBER,
|
||||
TEAM_RESTORE,
|
||||
TEAM_RESTORE_TASK,
|
||||
TEAM_SAVE_TASK_ATTACHMENT,
|
||||
|
|
@ -163,6 +165,7 @@ import type {
|
|||
LeadContextUsageSnapshot,
|
||||
MemberFullStats,
|
||||
MemberLogSummary,
|
||||
TeamAgentRuntimeSnapshot,
|
||||
MemberSpawnStatusesSnapshot,
|
||||
MessagesPage,
|
||||
SendMessageRequest,
|
||||
|
|
@ -578,6 +581,8 @@ export function registerTeamHandlers(ipcMain: IpcMain): void {
|
|||
ipcMain.handle(TEAM_LEAD_ACTIVITY, handleLeadActivity);
|
||||
ipcMain.handle(TEAM_LEAD_CONTEXT, handleLeadContext);
|
||||
ipcMain.handle(TEAM_MEMBER_SPAWN_STATUSES, handleMemberSpawnStatuses);
|
||||
ipcMain.handle(TEAM_GET_AGENT_RUNTIME, handleGetAgentRuntime);
|
||||
ipcMain.handle(TEAM_RESTART_MEMBER, handleRestartMember);
|
||||
ipcMain.handle(TEAM_SOFT_DELETE_TASK, handleSoftDeleteTask);
|
||||
ipcMain.handle(TEAM_RESTORE_TASK, handleRestoreTask);
|
||||
ipcMain.handle(TEAM_GET_DELETED_TASKS, handleGetDeletedTasks);
|
||||
|
|
@ -652,6 +657,8 @@ export function removeTeamHandlers(ipcMain: IpcMain): void {
|
|||
ipcMain.removeHandler(TEAM_LEAD_ACTIVITY);
|
||||
ipcMain.removeHandler(TEAM_LEAD_CONTEXT);
|
||||
ipcMain.removeHandler(TEAM_MEMBER_SPAWN_STATUSES);
|
||||
ipcMain.removeHandler(TEAM_GET_AGENT_RUNTIME);
|
||||
ipcMain.removeHandler(TEAM_RESTART_MEMBER);
|
||||
ipcMain.removeHandler(TEAM_SOFT_DELETE_TASK);
|
||||
ipcMain.removeHandler(TEAM_RESTORE_TASK);
|
||||
ipcMain.removeHandler(TEAM_GET_DELETED_TASKS);
|
||||
|
|
@ -2865,6 +2872,37 @@ async function handleMemberSpawnStatuses(
|
|||
);
|
||||
}
|
||||
|
||||
async function handleGetAgentRuntime(
|
||||
_event: IpcMainInvokeEvent,
|
||||
teamName: unknown
|
||||
): Promise<IpcResult<TeamAgentRuntimeSnapshot>> {
|
||||
const validated = validateTeamName(teamName);
|
||||
if (!validated.valid) {
|
||||
return { success: false, error: validated.error ?? 'Invalid teamName' };
|
||||
}
|
||||
return wrapTeamHandler('getAgentRuntime', async () =>
|
||||
getTeamProvisioningService().getTeamAgentRuntimeSnapshot(validated.value!)
|
||||
);
|
||||
}
|
||||
|
||||
async function handleRestartMember(
|
||||
_event: IpcMainInvokeEvent,
|
||||
teamName: unknown,
|
||||
memberName: unknown
|
||||
): Promise<IpcResult<void>> {
|
||||
const validatedTeamName = validateTeamName(teamName);
|
||||
if (!validatedTeamName.valid) {
|
||||
return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
|
||||
}
|
||||
const validatedMemberName = validateMemberName(memberName);
|
||||
if (!validatedMemberName.valid) {
|
||||
return { success: false, error: validatedMemberName.error ?? 'Invalid memberName' };
|
||||
}
|
||||
return wrapTeamHandler('restartMember', async () =>
|
||||
getTeamProvisioningService().restartMember(validatedTeamName.value!, validatedMemberName.value!)
|
||||
);
|
||||
}
|
||||
|
||||
async function handleStopTeam(
|
||||
_event: IpcMainInvokeEvent,
|
||||
teamName: unknown
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { killTmuxPaneForCurrentPlatformSync } from '@features/tmux-installer/main';
|
||||
import {
|
||||
killTmuxPaneForCurrentPlatformSync,
|
||||
listTmuxPanePidsForCurrentPlatform,
|
||||
} from '@features/tmux-installer/main';
|
||||
import { ConfigManager } from '@main/services/infrastructure/ConfigManager';
|
||||
import { NotificationManager } from '@main/services/infrastructure/NotificationManager';
|
||||
import { getAppIconPath } from '@main/utils/appIcon';
|
||||
|
|
@ -14,6 +17,7 @@ import {
|
|||
getTasksBasePath,
|
||||
getTeamsBasePath,
|
||||
} from '@main/utils/pathDecoder';
|
||||
import { isProcessAlive } from '@main/utils/processHealth';
|
||||
import { killProcessByPid } from '@main/utils/processKill';
|
||||
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
|
||||
import { shouldAutoAllow } from '@main/utils/toolApprovalRules';
|
||||
|
|
@ -64,6 +68,7 @@ import { randomUUID } from 'crypto';
|
|||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import pidusage from 'pidusage';
|
||||
|
||||
import {
|
||||
type GeminiRuntimeAuthState,
|
||||
|
|
@ -168,7 +173,11 @@ import type {
|
|||
MemberSpawnStatusEntry,
|
||||
PersistedTeamLaunchPhase,
|
||||
PersistedTeamLaunchSummary,
|
||||
TeamAgentRuntimeBackendType,
|
||||
TeamAgentRuntimeEntry,
|
||||
TeamAgentRuntimeSnapshot,
|
||||
TeamChangeEvent,
|
||||
TeamConfig,
|
||||
TeamCreateRequest,
|
||||
TeamCreateResponse,
|
||||
TeamLaunchAggregateState,
|
||||
|
|
@ -792,7 +801,38 @@ function createInitialMemberSpawnStatusEntry(): MemberSpawnStatusEntry {
|
|||
}
|
||||
|
||||
interface LiveTeamAgentRuntimeMetadata {
|
||||
alive: boolean;
|
||||
backendType?: TeamAgentRuntimeBackendType;
|
||||
agentId?: string;
|
||||
pid?: number;
|
||||
model?: string;
|
||||
tmuxPaneId?: string;
|
||||
}
|
||||
|
||||
function normalizeTeamAgentRuntimeBackendType(
|
||||
value: string | undefined,
|
||||
isLead: boolean
|
||||
): TeamAgentRuntimeBackendType | undefined {
|
||||
if (isLead) return 'lead';
|
||||
const normalized = value?.trim().toLowerCase();
|
||||
if (normalized === 'tmux' || normalized === 'iterm2' || normalized === 'in-process') {
|
||||
return normalized;
|
||||
}
|
||||
return normalized ? 'process' : undefined;
|
||||
}
|
||||
|
||||
function matchesMemberNameOrBase(candidateName: string, memberName: string): boolean {
|
||||
if (candidateName === memberName) {
|
||||
return true;
|
||||
}
|
||||
const parsed = parseNumericSuffixName(candidateName);
|
||||
return parsed !== null && parsed.suffix >= 2 && parsed.base === memberName;
|
||||
}
|
||||
|
||||
function matchesTeamMemberIdentity(leftName: string, rightName: string): boolean {
|
||||
return (
|
||||
matchesMemberNameOrBase(leftName, rightName) || matchesMemberNameOrBase(rightName, leftName)
|
||||
);
|
||||
}
|
||||
|
||||
interface MemberSpawnInboxCursor {
|
||||
|
|
@ -939,6 +979,24 @@ function sleep(ms: number): Promise<void> {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForPidsToExit(
|
||||
pids: readonly number[],
|
||||
opts: { timeoutMs: number; pollMs: number }
|
||||
): Promise<void> {
|
||||
if (pids.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deadline = Date.now() + opts.timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const remaining = pids.filter((pid) => isProcessAlive(pid));
|
||||
if (remaining.length === 0) {
|
||||
return;
|
||||
}
|
||||
await sleep(opts.pollMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function tryReadRegularFileUtf8(
|
||||
filePath: string,
|
||||
opts: { timeoutMs: number; maxBytes: number }
|
||||
|
|
@ -1484,6 +1542,52 @@ export function buildAddMemberSpawnMessage(
|
|||
);
|
||||
}
|
||||
|
||||
export function buildRestartMemberSpawnMessage(
|
||||
teamName: string,
|
||||
displayName: string,
|
||||
leadName: string,
|
||||
member: Pick<
|
||||
TeamCreateRequest['members'][number],
|
||||
'name' | 'role' | 'workflow' | 'providerId' | 'model' | 'effort'
|
||||
>
|
||||
): string {
|
||||
const roleHint =
|
||||
typeof member.role === 'string' && member.role.trim()
|
||||
? ` with role "${member.role.trim()}"`
|
||||
: '';
|
||||
const workflowHint =
|
||||
typeof member.workflow === 'string' && member.workflow.trim()
|
||||
? ` Their workflow: ${member.workflow.trim()}`
|
||||
: '';
|
||||
|
||||
const prompt = buildMemberSpawnPrompt(
|
||||
{
|
||||
name: member.name,
|
||||
...(member.role ? { role: member.role } : {}),
|
||||
...(member.workflow ? { workflow: member.workflow } : {}),
|
||||
...(member.providerId ? { providerId: member.providerId } : {}),
|
||||
...(member.model ? { model: member.model } : {}),
|
||||
...(member.effort ? { effort: member.effort } : {}),
|
||||
},
|
||||
displayName,
|
||||
teamName,
|
||||
leadName
|
||||
);
|
||||
const providerPart =
|
||||
member.providerId && member.providerId !== 'anthropic'
|
||||
? `, provider="${member.providerId}"`
|
||||
: '';
|
||||
const modelPart = member.model?.trim() ? `, model="${member.model.trim()}"` : '';
|
||||
const effortPart = member.effort ? `, effort="${member.effort}"` : '';
|
||||
|
||||
return (
|
||||
`Teammate "${member.name}"${roleHint} was restarted from the UI. ` +
|
||||
`Please respawn them immediately using the **Agent** tool with team_name="${teamName}", name="${member.name}", subagent_type="general-purpose"${providerPart}${modelPart}${effortPart}, and the exact prompt below. ` +
|
||||
`This is a restart of an existing persistent teammate, not a new teammate.${workflowHint ? workflowHint : ''}\n\n` +
|
||||
indentMultiline(prompt, ' ')
|
||||
);
|
||||
}
|
||||
|
||||
interface RuntimeBootstrapMemberSpec {
|
||||
name: string;
|
||||
prompt?: string;
|
||||
|
|
@ -2185,17 +2289,17 @@ interface McpJsonRpcResponse<TResult> {
|
|||
}
|
||||
|
||||
interface McpToolsListResult {
|
||||
tools?: Array<{
|
||||
tools?: {
|
||||
name?: string;
|
||||
_meta?: Record<string, unknown>;
|
||||
}>;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface McpToolCallResult {
|
||||
content?: Array<{
|
||||
content?: {
|
||||
type?: string;
|
||||
text?: string;
|
||||
}>;
|
||||
}[];
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -2366,6 +2470,7 @@ export class TeamProvisioningService {
|
|||
private static readonly SAME_TEAM_MATCH_WINDOW_MS = 30_000;
|
||||
private static readonly SAME_TEAM_RUN_START_SKEW_MS = 1_000;
|
||||
private static readonly SAME_TEAM_PERSIST_RETRY_MS = 2_000;
|
||||
private static readonly AGENT_RUNTIME_SNAPSHOT_CACHE_TTL_MS = 2_000;
|
||||
|
||||
private readonly runs = new Map<string, ProvisioningRun>();
|
||||
private readonly provisioningRunByTeam = new Map<string, string>();
|
||||
|
|
@ -2382,6 +2487,14 @@ export class TeamProvisioningService {
|
|||
string,
|
||||
NativeSameTeamFingerprint[]
|
||||
>();
|
||||
private readonly agentRuntimeSnapshotCache = new Map<
|
||||
string,
|
||||
{ expiresAtMs: number; snapshot: TeamAgentRuntimeSnapshot }
|
||||
>();
|
||||
private readonly liveTeamAgentRuntimeMetadataCache = new Map<
|
||||
string,
|
||||
{ expiresAtMs: number; metadata: Map<string, LiveTeamAgentRuntimeMetadata> }
|
||||
>();
|
||||
private readonly launchStateStore = new TeamLaunchStateStore();
|
||||
private readonly memberLogsFinder: TeamMemberLogsFinder;
|
||||
private teamChangeEmitter: ((event: TeamChangeEvent) => void) | null = null;
|
||||
|
|
@ -3811,17 +3924,18 @@ export class TeamProvisioningService {
|
|||
const runId = this.getTrackedRunId(teamName);
|
||||
if (!runId) {
|
||||
return this.reconcilePersistedLaunchState(teamName).then(({ snapshot, statuses }) => {
|
||||
this.attachLiveRuntimeMetadataToStatuses(teamName, statuses);
|
||||
return {
|
||||
statuses,
|
||||
runId: null,
|
||||
teamLaunchState: snapshot?.teamLaunchState,
|
||||
launchPhase: snapshot?.launchPhase,
|
||||
expectedMembers: snapshot?.expectedMembers,
|
||||
updatedAt: snapshot?.updatedAt,
|
||||
summary: snapshot?.summary,
|
||||
source: snapshot ? 'persisted' : 'persisted',
|
||||
};
|
||||
return this.attachLiveRuntimeMetadataToStatuses(teamName, statuses).then(
|
||||
(nextStatuses) => ({
|
||||
statuses: nextStatuses,
|
||||
runId: null,
|
||||
teamLaunchState: snapshot?.teamLaunchState,
|
||||
launchPhase: snapshot?.launchPhase,
|
||||
expectedMembers: snapshot?.expectedMembers,
|
||||
updatedAt: snapshot?.updatedAt,
|
||||
summary: snapshot?.summary,
|
||||
source: snapshot ? 'persisted' : 'persisted',
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
const run = this.runs.get(runId);
|
||||
|
|
@ -3842,8 +3956,10 @@ export class TeamProvisioningService {
|
|||
statuses: this.buildRuntimeSpawnStatusRecord(run),
|
||||
});
|
||||
const snapshot = persisted ?? liveSnapshot;
|
||||
const statuses = snapshotToMemberSpawnStatuses(snapshot);
|
||||
this.attachLiveRuntimeMetadataToStatuses(teamName, statuses);
|
||||
const statuses = await this.attachLiveRuntimeMetadataToStatuses(
|
||||
teamName,
|
||||
snapshotToMemberSpawnStatuses(snapshot)
|
||||
);
|
||||
return {
|
||||
statuses,
|
||||
runId,
|
||||
|
|
@ -3856,6 +3972,266 @@ export class TeamProvisioningService {
|
|||
};
|
||||
}
|
||||
|
||||
async getTeamAgentRuntimeSnapshot(teamName: string): Promise<TeamAgentRuntimeSnapshot> {
|
||||
const cached = this.agentRuntimeSnapshotCache.get(teamName);
|
||||
if (cached && cached.expiresAtMs > Date.now()) {
|
||||
return cached.snapshot;
|
||||
}
|
||||
|
||||
const updatedAt = nowIso();
|
||||
const runId = this.getTrackedRunId(teamName);
|
||||
const run = runId ? (this.runs.get(runId) ?? null) : null;
|
||||
|
||||
let configuredMembers: TeamConfig['members'] = [];
|
||||
try {
|
||||
configuredMembers = (await this.configReader.getConfig(teamName))?.members ?? [];
|
||||
} catch {
|
||||
configuredMembers = [];
|
||||
}
|
||||
|
||||
const liveRuntimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName);
|
||||
const runtimePids = new Set<number>();
|
||||
const leadPid = run?.child?.pid;
|
||||
if (typeof leadPid === 'number' && Number.isFinite(leadPid) && leadPid > 0) {
|
||||
runtimePids.add(leadPid);
|
||||
}
|
||||
for (const metadata of liveRuntimeByMember.values()) {
|
||||
const memberPid = metadata.pid;
|
||||
if (typeof memberPid === 'number' && Number.isFinite(memberPid) && memberPid > 0) {
|
||||
runtimePids.add(memberPid);
|
||||
}
|
||||
}
|
||||
const rssBytesByPid = await this.readProcessRssBytesByPid([...runtimePids]);
|
||||
const persistedRuntimeMembers = this.readPersistedRuntimeMembers(teamName);
|
||||
const snapshotMembers: Record<string, TeamAgentRuntimeEntry> = {};
|
||||
|
||||
const getPersistedRuntimeMember = (
|
||||
memberName: string
|
||||
): PersistedRuntimeMemberLike | undefined => {
|
||||
return persistedRuntimeMembers.find((member) => {
|
||||
const candidateName = typeof member.name === 'string' ? member.name.trim() : '';
|
||||
return candidateName.length > 0 && matchesMemberNameOrBase(candidateName, memberName);
|
||||
});
|
||||
};
|
||||
|
||||
const getLiveRuntimeMember = (memberName: string): LiveTeamAgentRuntimeMetadata | undefined => {
|
||||
let fallback: LiveTeamAgentRuntimeMetadata | undefined;
|
||||
for (const [candidateName, metadata] of liveRuntimeByMember.entries()) {
|
||||
if (candidateName === memberName) {
|
||||
return metadata;
|
||||
}
|
||||
if (matchesMemberNameOrBase(candidateName, memberName)) {
|
||||
fallback = metadata;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
for (const member of configuredMembers) {
|
||||
const memberName = typeof member?.name === 'string' ? member.name.trim() : '';
|
||||
if (!memberName) continue;
|
||||
|
||||
const isLead = isLeadMember({ name: memberName, agentType: member.agentType });
|
||||
if (isLead) {
|
||||
const pid = run?.child?.pid;
|
||||
const rssBytes = pid ? rssBytesByPid.get(pid) : undefined;
|
||||
const runtimeModel =
|
||||
run?.request.model?.trim() ||
|
||||
(run?.spawnContext
|
||||
? extractCliFlagValue(run.spawnContext.args.join(' '), '--model')
|
||||
: undefined) ||
|
||||
member.model?.trim() ||
|
||||
undefined;
|
||||
snapshotMembers[memberName] = {
|
||||
memberName,
|
||||
alive: Boolean(pid && !run?.processKilled && !run?.cancelRequested),
|
||||
restartable: false,
|
||||
backendType: 'lead',
|
||||
...(pid ? { pid } : {}),
|
||||
...(runtimeModel ? { runtimeModel } : {}),
|
||||
...(rssBytes != null ? { rssBytes } : {}),
|
||||
updatedAt,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const persistedRuntimeMember = getPersistedRuntimeMember(memberName);
|
||||
const liveRuntimeMember = getLiveRuntimeMember(memberName);
|
||||
const backendType = normalizeTeamAgentRuntimeBackendType(
|
||||
persistedRuntimeMember?.backendType,
|
||||
false
|
||||
);
|
||||
const restartable = backendType !== 'in-process';
|
||||
const runtimeModel = liveRuntimeMember?.model ?? member.model?.trim() ?? undefined;
|
||||
|
||||
snapshotMembers[memberName] = {
|
||||
memberName,
|
||||
alive: liveRuntimeMember?.alive ?? false,
|
||||
restartable,
|
||||
...(backendType ? { backendType } : {}),
|
||||
...(liveRuntimeMember?.pid ? { pid: liveRuntimeMember.pid } : {}),
|
||||
...(runtimeModel ? { runtimeModel } : {}),
|
||||
...(liveRuntimeMember?.pid && rssBytesByPid.has(liveRuntimeMember.pid)
|
||||
? { rssBytes: rssBytesByPid.get(liveRuntimeMember.pid) }
|
||||
: {}),
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
const snapshot: TeamAgentRuntimeSnapshot = {
|
||||
teamName,
|
||||
updatedAt,
|
||||
runId: run?.runId ?? null,
|
||||
members: snapshotMembers,
|
||||
};
|
||||
|
||||
this.agentRuntimeSnapshotCache.set(teamName, {
|
||||
expiresAtMs: Date.now() + TeamProvisioningService.AGENT_RUNTIME_SNAPSHOT_CACHE_TTL_MS,
|
||||
snapshot,
|
||||
});
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async restartMember(teamName: string, memberName: string): Promise<void> {
|
||||
const runId = this.getAliveRunId(teamName);
|
||||
if (!runId) {
|
||||
throw new Error(`Team "${teamName}" is not currently running`);
|
||||
}
|
||||
const run = this.runs.get(runId);
|
||||
if (!run || run.processKilled || run.cancelRequested) {
|
||||
throw new Error(`Team "${teamName}" is not currently running`);
|
||||
}
|
||||
|
||||
const config = await this.configReader.getConfig(teamName);
|
||||
const configuredMembers = config?.members ?? [];
|
||||
const configuredMember = configuredMembers.find(
|
||||
(member) => member?.name?.trim() === memberName
|
||||
);
|
||||
if (!configuredMember) {
|
||||
throw new Error(`Member "${memberName}" is not configured in team "${teamName}"`);
|
||||
}
|
||||
if (configuredMember.removedAt) {
|
||||
throw new Error(`Member "${memberName}" has been removed`);
|
||||
}
|
||||
if (isLeadMember({ name: memberName, agentType: configuredMember.agentType })) {
|
||||
throw new Error('Lead restart is not supported from member controls');
|
||||
}
|
||||
|
||||
const persistedRuntimeMembers = this.readPersistedRuntimeMembers(teamName).filter((member) => {
|
||||
const candidateName = typeof member.name === 'string' ? member.name.trim() : '';
|
||||
return candidateName.length > 0 && matchesMemberNameOrBase(candidateName, memberName);
|
||||
});
|
||||
|
||||
const backendTypes = new Set(
|
||||
persistedRuntimeMembers
|
||||
.map((member) => member.backendType?.trim().toLowerCase())
|
||||
.filter((value): value is string => Boolean(value))
|
||||
);
|
||||
if (backendTypes.has('in-process')) {
|
||||
throw new Error(
|
||||
`Member "${memberName}" uses an in-process runtime and cannot be restarted here`
|
||||
);
|
||||
}
|
||||
|
||||
const liveRuntimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName);
|
||||
const livePids = new Set<number>();
|
||||
let hasAliveRuntimeWithoutPid = false;
|
||||
for (const [candidateName, metadata] of liveRuntimeByMember.entries()) {
|
||||
if (!matchesMemberNameOrBase(candidateName, memberName)) {
|
||||
continue;
|
||||
}
|
||||
if (metadata.pid) {
|
||||
livePids.add(metadata.pid);
|
||||
continue;
|
||||
}
|
||||
if (metadata.alive && metadata.backendType !== 'in-process') {
|
||||
hasAliveRuntimeWithoutPid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAliveRuntimeWithoutPid) {
|
||||
throw new Error(
|
||||
`Member "${memberName}" is running, but its backend does not expose a restartable pid yet`
|
||||
);
|
||||
}
|
||||
|
||||
for (const persistedRuntimeMember of persistedRuntimeMembers) {
|
||||
const paneId =
|
||||
typeof persistedRuntimeMember.tmuxPaneId === 'string'
|
||||
? persistedRuntimeMember.tmuxPaneId.trim()
|
||||
: '';
|
||||
const backendType = persistedRuntimeMember.backendType?.trim().toLowerCase();
|
||||
if (!paneId || backendType !== 'tmux') {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
killTmuxPaneForCurrentPlatformSync(paneId);
|
||||
logger.info(
|
||||
`[${teamName}] Killed teammate pane ${memberName} (${paneId}) for manual restart`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`[${teamName}] Failed to kill teammate pane ${memberName} (${paneId}) for manual restart: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const pid of livePids) {
|
||||
try {
|
||||
killProcessByPid(pid);
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`[${teamName}] Failed to kill teammate process ${memberName} pid=${pid} for manual restart: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (livePids.size > 0) {
|
||||
await waitForPidsToExit(Array.from(livePids), {
|
||||
timeoutMs: 1_500,
|
||||
pollMs: 100,
|
||||
});
|
||||
}
|
||||
|
||||
this.agentRuntimeSnapshotCache.delete(teamName);
|
||||
this.liveTeamAgentRuntimeMetadataCache.delete(teamName);
|
||||
this.setMemberSpawnStatus(run, memberName, 'offline');
|
||||
this.setMemberSpawnStatus(run, memberName, 'spawning');
|
||||
this.appendMemberBootstrapDiagnostic(run, memberName, 'manual restart requested from UI');
|
||||
|
||||
const leadName =
|
||||
configuredMembers.find((member) => isLeadMember(member))?.name?.trim() || 'team-lead';
|
||||
const restartMessage = buildRestartMemberSpawnMessage(
|
||||
teamName,
|
||||
config?.name?.trim() || teamName,
|
||||
leadName,
|
||||
{
|
||||
name: memberName,
|
||||
role: configuredMember.role,
|
||||
workflow: configuredMember.workflow,
|
||||
providerId: configuredMember.providerId,
|
||||
model: configuredMember.model,
|
||||
effort: configuredMember.effort,
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
await this.sendMessageToRun(run, restartMessage);
|
||||
} catch (error) {
|
||||
this.setMemberSpawnStatus(
|
||||
run,
|
||||
memberName,
|
||||
'error',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private getMemberLaunchGraceKey(run: ProvisioningRun, memberName: string): string {
|
||||
return `member-launch-grace:${run.runId}:${memberName}`;
|
||||
}
|
||||
|
|
@ -7271,7 +7647,7 @@ export class TeamProvisioningService {
|
|||
return;
|
||||
}
|
||||
|
||||
const liveAgentNames = this.getLiveTeamAgentNames(run.teamName);
|
||||
const liveAgentNames = await this.getLiveTeamAgentNames(run.teamName);
|
||||
|
||||
// Flag any expected member not found in config.json (excluding the lead)
|
||||
for (const expected of run.expectedMembers) {
|
||||
|
|
@ -7381,63 +7757,310 @@ export class TeamProvisioningService {
|
|||
}
|
||||
}
|
||||
|
||||
private hasLiveTeamAgentProcess(teamName: string, memberName: string): boolean {
|
||||
return this.getLiveTeamAgentRuntimeMetadata(teamName).has(memberName);
|
||||
}
|
||||
|
||||
private attachLiveRuntimeMetadataToStatuses(
|
||||
private async attachLiveRuntimeMetadataToStatuses(
|
||||
teamName: string,
|
||||
statuses: Record<string, MemberSpawnStatusEntry>
|
||||
): void {
|
||||
for (const [memberName, metadata] of this.getLiveTeamAgentRuntimeMetadata(teamName).entries()) {
|
||||
const current = statuses[memberName];
|
||||
): Promise<Record<string, MemberSpawnStatusEntry>> {
|
||||
const runtimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName);
|
||||
const nextStatuses = { ...statuses };
|
||||
for (const [memberName, metadata] of runtimeByMember.entries()) {
|
||||
const current = nextStatuses[memberName];
|
||||
if (!current || !metadata.model) {
|
||||
continue;
|
||||
}
|
||||
statuses[memberName] = {
|
||||
nextStatuses[memberName] = {
|
||||
...current,
|
||||
runtimeModel: metadata.model,
|
||||
};
|
||||
}
|
||||
return nextStatuses;
|
||||
}
|
||||
|
||||
private getLiveTeamAgentNames(teamName: string): Set<string> {
|
||||
return new Set(this.getLiveTeamAgentRuntimeMetadata(teamName).keys());
|
||||
private async getLiveTeamAgentNames(teamName: string): Promise<Set<string>> {
|
||||
const runtimeByMember = await this.getLiveTeamAgentRuntimeMetadata(teamName);
|
||||
return new Set(
|
||||
[...runtimeByMember.entries()]
|
||||
.filter(([, metadata]) => metadata.alive)
|
||||
.map(([memberName]) => memberName)
|
||||
);
|
||||
}
|
||||
|
||||
private getLiveTeamAgentRuntimeMetadata(
|
||||
private findConfiguredMemberModel(
|
||||
configuredMembers: TeamConfig['members'] | undefined,
|
||||
memberName: string
|
||||
): string | undefined {
|
||||
for (const member of configuredMembers ?? []) {
|
||||
const candidateName = typeof member?.name === 'string' ? member.name.trim() : '';
|
||||
if (!candidateName || !matchesTeamMemberIdentity(candidateName, memberName)) {
|
||||
continue;
|
||||
}
|
||||
const model = member.model?.trim();
|
||||
if (model) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private findMetaMemberModel(
|
||||
metaMembers: Awaited<ReturnType<TeamMembersMetaStore['getMembers']>>,
|
||||
memberName: string
|
||||
): string | undefined {
|
||||
for (const member of metaMembers) {
|
||||
const candidateName = member.name?.trim() ?? '';
|
||||
if (!candidateName || !matchesTeamMemberIdentity(candidateName, memberName)) {
|
||||
continue;
|
||||
}
|
||||
const model = member.model?.trim();
|
||||
if (model) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private findEffectiveRunMemberModel(
|
||||
run: ProvisioningRun | null,
|
||||
memberName: string
|
||||
): string | undefined {
|
||||
if (!run) {
|
||||
return undefined;
|
||||
}
|
||||
for (const member of run.effectiveMembers ?? []) {
|
||||
const candidateName = member.name?.trim() ?? '';
|
||||
if (!candidateName || !matchesTeamMemberIdentity(candidateName, memberName)) {
|
||||
continue;
|
||||
}
|
||||
const model = member.model?.trim();
|
||||
if (model) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private findTrackedMemberSpawnStatus(
|
||||
run: ProvisioningRun | null,
|
||||
memberName: string
|
||||
): MemberSpawnStatusEntry | undefined {
|
||||
if (!run) {
|
||||
return undefined;
|
||||
}
|
||||
const statusMap = run.memberSpawnStatuses instanceof Map ? run.memberSpawnStatuses : undefined;
|
||||
if (!statusMap) {
|
||||
return undefined;
|
||||
}
|
||||
const direct = statusMap.get(memberName);
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
for (const [candidateName, entry] of statusMap.entries()) {
|
||||
if (matchesTeamMemberIdentity(candidateName, memberName)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getLiveTeamAgentRuntimeMetadata(
|
||||
teamName: string
|
||||
): Map<string, LiveTeamAgentRuntimeMetadata> {
|
||||
): Promise<Map<string, LiveTeamAgentRuntimeMetadata>> {
|
||||
const cached = this.liveTeamAgentRuntimeMetadataCache.get(teamName);
|
||||
if (cached && cached.expiresAtMs > Date.now()) {
|
||||
return cached.metadata;
|
||||
}
|
||||
|
||||
const runId = this.getTrackedRunId(teamName);
|
||||
const run = runId ? (this.runs.get(runId) ?? null) : null;
|
||||
|
||||
let configuredMembers: TeamConfig['members'] = [];
|
||||
try {
|
||||
configuredMembers = (await this.configReader.getConfig(teamName))?.members ?? [];
|
||||
} catch {
|
||||
configuredMembers = [];
|
||||
}
|
||||
|
||||
let metaMembers: Awaited<ReturnType<TeamMembersMetaStore['getMembers']>> = [];
|
||||
try {
|
||||
metaMembers = await this.membersMetaStore.getMembers(teamName);
|
||||
} catch {
|
||||
metaMembers = [];
|
||||
}
|
||||
|
||||
const persistedRuntimeMembers = this.readPersistedRuntimeMembers(teamName);
|
||||
const metadataByMember = new Map<string, LiveTeamAgentRuntimeMetadata>();
|
||||
const upsertMetadata = (
|
||||
memberName: string,
|
||||
patch: Partial<LiveTeamAgentRuntimeMetadata>
|
||||
): void => {
|
||||
const current = metadataByMember.get(memberName) ?? { alive: false };
|
||||
metadataByMember.set(memberName, {
|
||||
...current,
|
||||
...patch,
|
||||
alive: patch.alive ?? current.alive,
|
||||
});
|
||||
};
|
||||
|
||||
for (const member of persistedRuntimeMembers) {
|
||||
const memberName = typeof member.name === 'string' ? member.name.trim() : '';
|
||||
if (!memberName || isLeadMember({ name: memberName })) {
|
||||
continue;
|
||||
}
|
||||
const runtimeModel =
|
||||
this.findConfiguredMemberModel(configuredMembers, memberName) ??
|
||||
this.findEffectiveRunMemberModel(run, memberName) ??
|
||||
this.findMetaMemberModel(metaMembers, memberName);
|
||||
upsertMetadata(memberName, {
|
||||
backendType: normalizeTeamAgentRuntimeBackendType(member.backendType, false),
|
||||
agentId:
|
||||
typeof member.agentId === 'string' ? member.agentId.trim() || undefined : undefined,
|
||||
tmuxPaneId:
|
||||
typeof member.tmuxPaneId === 'string' ? member.tmuxPaneId.trim() || undefined : undefined,
|
||||
...(runtimeModel ? { model: runtimeModel } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
for (const member of configuredMembers) {
|
||||
const memberName = typeof member?.name === 'string' ? member.name.trim() : '';
|
||||
if (!memberName || isLeadMember({ name: memberName, agentType: member.agentType })) {
|
||||
continue;
|
||||
}
|
||||
const runtimeModel =
|
||||
member.model?.trim() ||
|
||||
this.findEffectiveRunMemberModel(run, memberName) ||
|
||||
this.findMetaMemberModel(metaMembers, memberName);
|
||||
upsertMetadata(memberName, {
|
||||
...(runtimeModel ? { model: runtimeModel } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
for (const member of run?.effectiveMembers ?? []) {
|
||||
const memberName = member.name?.trim() ?? '';
|
||||
if (!memberName || isLeadMember(member) || memberName.toLowerCase() === 'user') {
|
||||
continue;
|
||||
}
|
||||
upsertMetadata(memberName, {
|
||||
...(member.model?.trim() ? { model: member.model.trim() } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
const paneIds = [...metadataByMember.values()]
|
||||
.map((metadata) => metadata.tmuxPaneId?.trim() ?? '')
|
||||
.filter((paneId) => paneId.length > 0);
|
||||
let panePidById = new Map<string, number>();
|
||||
if (paneIds.length > 0) {
|
||||
try {
|
||||
panePidById = await listTmuxPanePidsForCurrentPlatform(paneIds);
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`[${teamName}] Failed to read tmux pane pids for runtime snapshot: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [memberName, metadata] of metadataByMember.entries()) {
|
||||
const paneId = metadata.tmuxPaneId?.trim() ?? '';
|
||||
const backendType = metadata.backendType;
|
||||
const panePid = paneId ? panePidById.get(paneId) : undefined;
|
||||
const status = this.findTrackedMemberSpawnStatus(run, memberName);
|
||||
const alive =
|
||||
typeof panePid === 'number' && panePid > 0
|
||||
? true
|
||||
: backendType === 'tmux'
|
||||
? false
|
||||
: Boolean(status?.runtimeAlive || status?.bootstrapConfirmed);
|
||||
metadataByMember.set(memberName, {
|
||||
...metadata,
|
||||
alive,
|
||||
...(typeof panePid === 'number' && panePid > 0 ? { pid: panePid } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
this.liveTeamAgentRuntimeMetadataCache.set(teamName, {
|
||||
expiresAtMs: Date.now() + TeamProvisioningService.AGENT_RUNTIME_SNAPSHOT_CACHE_TTL_MS,
|
||||
metadata: metadataByMember,
|
||||
});
|
||||
return metadataByMember;
|
||||
}
|
||||
|
||||
private readUnixProcessTableRows(): {
|
||||
pid: number;
|
||||
command: string;
|
||||
}[] {
|
||||
if (process.platform === 'win32') {
|
||||
return new Map();
|
||||
return [];
|
||||
}
|
||||
|
||||
let output = '';
|
||||
try {
|
||||
output = execFileSync('ps', ['-ax', '-o', 'command='], {
|
||||
output = execFileSync('ps', ['-ax', '-o', 'pid=,command='], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: { pid: number; command: string }[] = [];
|
||||
for (const line of output.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
const match = /^(\d+)\s+(.*)$/.exec(trimmed);
|
||||
if (!match) continue;
|
||||
const pid = Number.parseInt(match[1], 10);
|
||||
const command = match[2]?.trim() ?? '';
|
||||
if (!Number.isFinite(pid) || pid <= 0 || command.length === 0) {
|
||||
continue;
|
||||
}
|
||||
rows.push({
|
||||
pid,
|
||||
command,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private async readProcessRssBytesByPid(pids: readonly number[]): Promise<Map<number, number>> {
|
||||
const uniquePids = [...new Set(pids.filter((pid) => Number.isFinite(pid) && pid > 0))];
|
||||
if (uniquePids.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const teamMarker = `--team-name ${teamName}`;
|
||||
const metadataByAgent = new Map<string, LiveTeamAgentRuntimeMetadata>();
|
||||
for (const line of output.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.includes(teamMarker)) continue;
|
||||
const match = /--agent-id\s+([^\s@]+)@/.exec(trimmed);
|
||||
if (!match) continue;
|
||||
const agentName = match[1]?.trim();
|
||||
if (agentName) {
|
||||
const model = extractCliFlagValue(trimmed, '--model');
|
||||
metadataByAgent.set(agentName, {
|
||||
...(model ? { model } : {}),
|
||||
});
|
||||
const rssBytesByPid = new Map<number, number>();
|
||||
const options = { maxage: 0 };
|
||||
try {
|
||||
const statsByPid = await pidusage(uniquePids, options);
|
||||
for (const [rawPid, stat] of Object.entries(statsByPid)) {
|
||||
const pid = Number.parseInt(rawPid, 10);
|
||||
const rssBytes = stat?.memory;
|
||||
if (Number.isFinite(pid) && pid > 0 && Number.isFinite(rssBytes) && rssBytes >= 0) {
|
||||
rssBytesByPid.set(pid, rssBytes);
|
||||
}
|
||||
}
|
||||
return rssBytesByPid;
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`pidusage batch runtime snapshot failed; falling back to per-pid reads: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
return metadataByAgent;
|
||||
|
||||
await Promise.all(
|
||||
uniquePids.map(async (pid) => {
|
||||
try {
|
||||
const stat = await pidusage(pid, options);
|
||||
if (Number.isFinite(stat.memory) && stat.memory >= 0) {
|
||||
rssBytesByPid.set(pid, stat.memory);
|
||||
}
|
||||
} catch {
|
||||
// Process likely exited between discovery and sampling.
|
||||
}
|
||||
})
|
||||
);
|
||||
return rssBytesByPid;
|
||||
}
|
||||
|
||||
private async clearPersistedLaunchState(teamName: string): Promise<void> {
|
||||
|
|
@ -7603,7 +8226,7 @@ export class TeamProvisioningService {
|
|||
// best-effort
|
||||
}
|
||||
|
||||
const liveAgentNames = this.getLiveTeamAgentNames(teamName);
|
||||
const liveAgentNames = await this.getLiveTeamAgentNames(teamName);
|
||||
const nextMembers = { ...persisted.members };
|
||||
const now = nowIso();
|
||||
for (const expected of persisted.expectedMembers) {
|
||||
|
|
@ -8233,6 +8856,8 @@ export class TeamProvisioningService {
|
|||
* Always uses SIGKILL via killTeamProcess() to prevent CLI cleanup.
|
||||
*/
|
||||
stopTeam(teamName: string): void {
|
||||
this.agentRuntimeSnapshotCache.delete(teamName);
|
||||
this.liveTeamAgentRuntimeMetadataCache.delete(teamName);
|
||||
this.stopPersistentTeamMembers(teamName);
|
||||
|
||||
const runId = this.getTrackedRunId(teamName);
|
||||
|
|
@ -10830,6 +11455,8 @@ export class TeamProvisioningService {
|
|||
this.aliveRunByTeam.delete(run.teamName);
|
||||
}
|
||||
if (!hasNewerTrackedRun) {
|
||||
this.agentRuntimeSnapshotCache.delete(run.teamName);
|
||||
this.liveTeamAgentRuntimeMetadataCache.delete(run.teamName);
|
||||
this.leadInboxRelayInFlight.delete(run.teamName);
|
||||
this.relayedLeadInboxMessageIds.delete(run.teamName);
|
||||
this.pendingCrossTeamFirstReplies.delete(run.teamName);
|
||||
|
|
|
|||
|
|
@ -376,6 +376,12 @@ export const TEAM_LEAD_CONTEXT = 'team:leadContext';
|
|||
/** Get per-member spawn statuses for a team */
|
||||
export const TEAM_MEMBER_SPAWN_STATUSES = 'team:memberSpawnStatuses';
|
||||
|
||||
/** Get live per-agent runtime stats for a team */
|
||||
export const TEAM_GET_AGENT_RUNTIME = 'team:getAgentRuntime';
|
||||
|
||||
/** Restart a specific teammate runtime */
|
||||
export const TEAM_RESTART_MEMBER = 'team:restartMember';
|
||||
|
||||
/** Soft-delete a task (set status to 'deleted' with deletedAt timestamp) */
|
||||
export const TEAM_SOFT_DELETE_TASK = 'team:softDeleteTask';
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ import {
|
|||
TEAM_LEAD_CONTEXT,
|
||||
TEAM_LIST,
|
||||
TEAM_MEMBER_SPAWN_STATUSES,
|
||||
TEAM_GET_AGENT_RUNTIME,
|
||||
TEAM_PERMANENTLY_DELETE,
|
||||
TEAM_PREPARE_PROVISIONING,
|
||||
TEAM_PROCESS_ALIVE,
|
||||
|
|
@ -157,6 +158,7 @@ import {
|
|||
TEAM_REMOVE_TASK_RELATIONSHIP,
|
||||
TEAM_REPLACE_MEMBERS,
|
||||
TEAM_REQUEST_REVIEW,
|
||||
TEAM_RESTART_MEMBER,
|
||||
TEAM_RESTORE,
|
||||
TEAM_RESTORE_TASK,
|
||||
TEAM_SAVE_TASK_ATTACHMENT,
|
||||
|
|
@ -266,6 +268,7 @@ import type {
|
|||
LeadContextUsageSnapshot,
|
||||
MemberFullStats,
|
||||
MemberLogSummary,
|
||||
TeamAgentRuntimeSnapshot,
|
||||
MemberSpawnStatusesSnapshot,
|
||||
MessagesPage,
|
||||
NotificationTrigger,
|
||||
|
|
@ -1068,6 +1071,12 @@ const electronAPI: ElectronAPI = {
|
|||
getMemberSpawnStatuses: async (teamName: string) => {
|
||||
return invokeIpcWithResult<MemberSpawnStatusesSnapshot>(TEAM_MEMBER_SPAWN_STATUSES, teamName);
|
||||
},
|
||||
getTeamAgentRuntime: async (teamName: string) => {
|
||||
return invokeIpcWithResult<TeamAgentRuntimeSnapshot>(TEAM_GET_AGENT_RUNTIME, teamName);
|
||||
},
|
||||
restartMember: async (teamName: string, memberName: string) => {
|
||||
return invokeIpcWithResult<void>(TEAM_RESTART_MEMBER, teamName, memberName);
|
||||
},
|
||||
softDeleteTask: async (teamName: string, taskId: string) => {
|
||||
return invokeIpcWithResult<void>(TEAM_SOFT_DELETE_TASK, teamName, taskId);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -917,6 +917,17 @@ export class HttpAPIClient implements ElectronAPI {
|
|||
getMemberSpawnStatuses: async () => {
|
||||
return { statuses: {}, runId: null };
|
||||
},
|
||||
getTeamAgentRuntime: async (teamName: string) => {
|
||||
return {
|
||||
teamName,
|
||||
updatedAt: new Date().toISOString(),
|
||||
runId: null,
|
||||
members: {},
|
||||
};
|
||||
},
|
||||
restartMember: async (): Promise<void> => {
|
||||
throw new Error('Member restart is not available in browser mode');
|
||||
},
|
||||
softDeleteTask: async (_teamName: string, _taskId: string): Promise<void> => {
|
||||
// Not available via HTTP client — no-op
|
||||
},
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ import type { ContextInjection } from '@renderer/types/contextInjection';
|
|||
import type { Session } from '@renderer/types/data';
|
||||
import type { InlineChip } from '@renderer/types/inlineChip';
|
||||
import type {
|
||||
TeamAgentRuntimeEntry,
|
||||
MemberSpawnStatusEntry,
|
||||
ResolvedTeamMember,
|
||||
TaskRef,
|
||||
|
|
@ -301,7 +302,7 @@ type TeamMemberListBridgeProps = Omit<
|
|||
};
|
||||
type TeamMemberDetailDialogBridgeProps = Omit<
|
||||
ComponentProps<typeof MemberDetailDialog>,
|
||||
'leadActivity' | 'spawnEntry'
|
||||
'leadActivity' | 'spawnEntry' | 'runtimeEntry'
|
||||
>;
|
||||
type TeamSidebarRailBridgeProps = Omit<
|
||||
ComponentProps<typeof TeamSidebarRail>,
|
||||
|
|
@ -339,6 +340,17 @@ function buildMemberSpawnStatusMap(
|
|||
return map.size > 0 ? map : undefined;
|
||||
}
|
||||
|
||||
function buildTeamAgentRuntimeMap(
|
||||
runtimeSnapshot: Record<string, TeamAgentRuntimeEntry> | undefined
|
||||
): Map<string, TeamAgentRuntimeEntry> | undefined {
|
||||
if (!runtimeSnapshot) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const map = new Map<string, TeamAgentRuntimeEntry>(Object.entries(runtimeSnapshot));
|
||||
return map.size > 0 ? map : undefined;
|
||||
}
|
||||
|
||||
const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({
|
||||
teamName,
|
||||
isTeamProvisioning,
|
||||
|
|
@ -376,6 +388,54 @@ const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({
|
|||
return null;
|
||||
});
|
||||
|
||||
const TEAM_AGENT_RUNTIME_REFRESH_MS = 5_000;
|
||||
|
||||
const TeamAgentRuntimeWatcher = memo(function TeamAgentRuntimeWatcher({
|
||||
teamName,
|
||||
isTeamProvisioning,
|
||||
isTeamAlive,
|
||||
isThisTabActive,
|
||||
}: {
|
||||
teamName: string;
|
||||
isTeamProvisioning: boolean;
|
||||
isTeamAlive?: boolean;
|
||||
isThisTabActive: boolean;
|
||||
}): null {
|
||||
const { leadActivity, fetchTeamAgentRuntime } = useStore(
|
||||
useShallow((s) => ({
|
||||
leadActivity: s.leadActivityByTeam[teamName],
|
||||
fetchTeamAgentRuntime: s.fetchTeamAgentRuntime,
|
||||
}))
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isThisTabActive) return;
|
||||
const shouldWatch =
|
||||
isTeamProvisioning ||
|
||||
isTeamAlive === true ||
|
||||
leadActivity === 'active' ||
|
||||
leadActivity === 'idle';
|
||||
if (!shouldWatch) return;
|
||||
|
||||
void fetchTeamAgentRuntime(teamName);
|
||||
const timer = window.setInterval(() => {
|
||||
void fetchTeamAgentRuntime(teamName);
|
||||
}, TEAM_AGENT_RUNTIME_REFRESH_MS);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [
|
||||
fetchTeamAgentRuntime,
|
||||
isTeamAlive,
|
||||
isTeamProvisioning,
|
||||
isThisTabActive,
|
||||
leadActivity,
|
||||
teamName,
|
||||
]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const LeadContextWatcher = memo(function LeadContextWatcher({
|
||||
teamName,
|
||||
tabId,
|
||||
|
|
@ -694,18 +754,24 @@ const TeamMemberListBridge = memo(function TeamMemberListBridge({
|
|||
teamName,
|
||||
...props
|
||||
}: TeamMemberListBridgeProps): React.JSX.Element {
|
||||
const { leadActivity, progress, memberSpawnStatuses, memberSpawnSnapshot } = useStore(
|
||||
useShallow((s) => ({
|
||||
leadActivity: s.leadActivityByTeam[teamName],
|
||||
progress: getCurrentProvisioningProgressForTeam(s, teamName),
|
||||
memberSpawnStatuses: s.memberSpawnStatusesByTeam[teamName],
|
||||
memberSpawnSnapshot: s.memberSpawnSnapshotsByTeam[teamName],
|
||||
}))
|
||||
);
|
||||
const { leadActivity, progress, memberSpawnStatuses, memberSpawnSnapshot, runtimeSnapshot } =
|
||||
useStore(
|
||||
useShallow((s) => ({
|
||||
leadActivity: s.leadActivityByTeam[teamName],
|
||||
progress: getCurrentProvisioningProgressForTeam(s, teamName),
|
||||
memberSpawnStatuses: s.memberSpawnStatusesByTeam[teamName],
|
||||
memberSpawnSnapshot: s.memberSpawnSnapshotsByTeam[teamName],
|
||||
runtimeSnapshot: s.teamAgentRuntimeByTeam[teamName],
|
||||
}))
|
||||
);
|
||||
const memberSpawnStatusMap = useMemo(
|
||||
() => buildMemberSpawnStatusMap(memberSpawnStatuses),
|
||||
[memberSpawnStatuses]
|
||||
);
|
||||
const memberRuntimeMap = useMemo(
|
||||
() => buildTeamAgentRuntimeMap(runtimeSnapshot?.members),
|
||||
[runtimeSnapshot?.members]
|
||||
);
|
||||
const isLaunchSettling = useMemo(() => {
|
||||
if (progress?.state !== 'ready') {
|
||||
return false;
|
||||
|
|
@ -724,6 +790,7 @@ const TeamMemberListBridge = memo(function TeamMemberListBridge({
|
|||
{...props}
|
||||
leadActivity={leadActivity}
|
||||
memberSpawnStatuses={memberSpawnStatusMap}
|
||||
memberRuntimeEntries={memberRuntimeMap}
|
||||
isLaunchSettling={isLaunchSettling}
|
||||
/>
|
||||
);
|
||||
|
|
@ -785,6 +852,7 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
|
|||
memberSpawnStatuses,
|
||||
memberSpawnSnapshot,
|
||||
spawnEntry,
|
||||
runtimeEntry,
|
||||
} = useStore(
|
||||
useShallow((s) => ({
|
||||
leadActivity: s.leadActivityByTeam[teamName],
|
||||
|
|
@ -794,6 +862,7 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
|
|||
memberSpawnStatuses: s.memberSpawnStatusesByTeam[teamName],
|
||||
memberSpawnSnapshot: s.memberSpawnSnapshotsByTeam[teamName],
|
||||
spawnEntry: member ? s.memberSpawnStatusesByTeam[teamName]?.[member.name] : undefined,
|
||||
runtimeEntry: member ? s.teamAgentRuntimeByTeam[teamName]?.members[member.name] : undefined,
|
||||
}))
|
||||
);
|
||||
const isLaunchSettling = useMemo(() => {
|
||||
|
|
@ -817,6 +886,7 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
|
|||
isLaunchSettling={isLaunchSettling}
|
||||
leadActivity={leadActivity}
|
||||
spawnEntry={spawnEntry}
|
||||
runtimeEntry={runtimeEntry}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
@ -1122,6 +1192,7 @@ export const TeamDetailView = ({
|
|||
lastSendMessageResult,
|
||||
reviewActionError,
|
||||
addMember,
|
||||
restartMember,
|
||||
removeMember,
|
||||
updateMemberRole,
|
||||
launchTeam,
|
||||
|
|
@ -1168,6 +1239,7 @@ export const TeamDetailView = ({
|
|||
lastSendMessageResult: s.lastSendMessageResult,
|
||||
reviewActionError: s.reviewActionError,
|
||||
addMember: s.addMember,
|
||||
restartMember: s.restartMember,
|
||||
removeMember: s.removeMember,
|
||||
updateMemberRole: s.updateMemberRole,
|
||||
launchTeam: s.launchTeam,
|
||||
|
|
@ -1876,6 +1948,14 @@ export const TeamDetailView = ({
|
|||
isTeamAlive={data?.isAlive}
|
||||
/>
|
||||
);
|
||||
const teamAgentRuntimeWatcher = (
|
||||
<TeamAgentRuntimeWatcher
|
||||
teamName={teamName}
|
||||
isTeamProvisioning={isTeamProvisioning}
|
||||
isTeamAlive={data?.isAlive}
|
||||
isThisTabActive={isThisTabActive}
|
||||
/>
|
||||
);
|
||||
const leadContextWatcher = (
|
||||
<LeadContextWatcher
|
||||
teamName={teamName}
|
||||
|
|
@ -2552,6 +2632,7 @@ export const TeamDetailView = ({
|
|||
initialActivityFilter={selectedMemberView?.initialActivityFilter}
|
||||
isTeamAlive={data.isAlive}
|
||||
isTeamProvisioning={isTeamProvisioning}
|
||||
launchParams={launchParams}
|
||||
onClose={closeSelectedMemberDialog}
|
||||
onSendMessage={() => {
|
||||
const name = selectedMember?.name ?? '';
|
||||
|
|
@ -2567,6 +2648,7 @@ export const TeamDetailView = ({
|
|||
closeSelectedMemberDialog();
|
||||
openCreateTaskDialog('', '', name);
|
||||
}}
|
||||
onRestartMember={(memberName) => restartMember(teamName, memberName)}
|
||||
onTaskClick={(task) => {
|
||||
closeSelectedMemberDialog();
|
||||
setSelectedTask(task);
|
||||
|
|
@ -2915,6 +2997,7 @@ export const TeamDetailView = ({
|
|||
return (
|
||||
<>
|
||||
{spawnStatusWatcher}
|
||||
{teamAgentRuntimeWatcher}
|
||||
{leadContextWatcher}
|
||||
{renderBody()}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,17 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/u
|
|||
import { useMemberStats } from '@renderer/hooks/useMemberStats';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { selectMemberMessagesForTeamMember } from '@renderer/store/slices/teamSlice';
|
||||
import { resolveMemberRuntimeSummary } from '@renderer/utils/memberRuntimeSummary';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { BarChart3, FileText, ListPlus, MessageSquare, UserMinus } from 'lucide-react';
|
||||
import {
|
||||
BarChart3,
|
||||
FileText,
|
||||
ListPlus,
|
||||
Loader2,
|
||||
MessageSquare,
|
||||
RotateCcw,
|
||||
UserMinus,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { buildMemberActivityEntries } from './memberActivityEntries';
|
||||
import { MemberDetailHeader } from './MemberDetailHeader';
|
||||
|
|
@ -21,9 +30,11 @@ import { MemberTasksTab } from './MemberTasksTab';
|
|||
import type {
|
||||
LeadActivityState,
|
||||
MemberSpawnStatusEntry,
|
||||
TeamAgentRuntimeEntry,
|
||||
ResolvedTeamMember,
|
||||
TeamTaskWithKanban,
|
||||
} from '@shared/types';
|
||||
import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
|
||||
|
||||
interface MemberDetailDialogProps {
|
||||
open: boolean;
|
||||
|
|
@ -38,11 +49,14 @@ interface MemberDetailDialogProps {
|
|||
isLaunchSettling?: boolean;
|
||||
leadActivity?: LeadActivityState;
|
||||
spawnEntry?: MemberSpawnStatusEntry;
|
||||
runtimeEntry?: TeamAgentRuntimeEntry;
|
||||
launchParams?: TeamLaunchParams;
|
||||
onClose: () => void;
|
||||
onSendMessage: () => void;
|
||||
onAssignTask: () => void;
|
||||
onTaskClick: (task: TeamTaskWithKanban) => void;
|
||||
onRemoveMember?: () => void;
|
||||
onRestartMember?: (memberName: string) => Promise<void> | void;
|
||||
onUpdateRole?: (memberName: string, role: string | undefined) => Promise<void> | void;
|
||||
updatingRole?: boolean;
|
||||
onViewMemberChanges?: (memberName: string, filePath?: string) => void;
|
||||
|
|
@ -61,11 +75,14 @@ export const MemberDetailDialog = ({
|
|||
isLaunchSettling,
|
||||
leadActivity,
|
||||
spawnEntry,
|
||||
runtimeEntry,
|
||||
launchParams,
|
||||
onClose,
|
||||
onSendMessage,
|
||||
onAssignTask,
|
||||
onTaskClick,
|
||||
onRemoveMember,
|
||||
onRestartMember,
|
||||
onUpdateRole,
|
||||
updatingRole,
|
||||
onViewMemberChanges,
|
||||
|
|
@ -101,12 +118,24 @@ export const MemberDetailDialog = ({
|
|||
);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<MemberDetailTab>(initialTab);
|
||||
const [restarting, setRestarting] = useState(false);
|
||||
const [restartError, setRestartError] = useState<string | null>(null);
|
||||
|
||||
const runtimeSummary = useMemo(
|
||||
() =>
|
||||
member
|
||||
? resolveMemberRuntimeSummary(member, launchParams, spawnEntry, runtimeEntry)
|
||||
: undefined,
|
||||
[launchParams, member, runtimeEntry, spawnEntry]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !member) {
|
||||
return;
|
||||
}
|
||||
setActiveTab(initialTab);
|
||||
setRestartError(null);
|
||||
setRestarting(false);
|
||||
}, [initialTab, member, open]);
|
||||
|
||||
const {
|
||||
|
|
@ -126,6 +155,7 @@ export const MemberDetailDialog = ({
|
|||
<DialogHeader className="shrink-0">
|
||||
<MemberDetailHeader
|
||||
member={member}
|
||||
runtimeSummary={runtimeSummary}
|
||||
isTeamAlive={isTeamAlive}
|
||||
isTeamProvisioning={isTeamProvisioning}
|
||||
leadActivity={isLeadMember(member) ? leadActivity : undefined}
|
||||
|
|
@ -214,12 +244,52 @@ export const MemberDetailDialog = ({
|
|||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
{restartError ? (
|
||||
<div className="mr-auto text-xs text-red-400">{restartError}</div>
|
||||
) : runtimeEntry?.pid ? (
|
||||
<div className="mr-auto text-xs text-[var(--color-text-muted)]">
|
||||
PID {runtimeEntry.pid}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mr-auto" />
|
||||
)}
|
||||
{member.removedAt ? (
|
||||
<span className="text-xs text-[var(--color-text-muted)]">
|
||||
Removed {new Date(member.removedAt).toLocaleDateString()}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{onRestartMember &&
|
||||
!isLeadMember(member) &&
|
||||
(isTeamAlive || isTeamProvisioning) &&
|
||||
runtimeEntry?.restartable !== false && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
disabled={restarting}
|
||||
onClick={async () => {
|
||||
setRestartError(null);
|
||||
setRestarting(true);
|
||||
try {
|
||||
await onRestartMember(member.name);
|
||||
} catch (error) {
|
||||
setRestartError(
|
||||
error instanceof Error ? error.message : 'Failed to restart member'
|
||||
);
|
||||
} finally {
|
||||
setRestarting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{restarting ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
) : (
|
||||
<RotateCcw size={14} />
|
||||
)}
|
||||
Restart
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="gap-1.5" onClick={onSendMessage}>
|
||||
<MessageSquare size={14} />
|
||||
Send Message
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import type {
|
|||
|
||||
interface MemberDetailHeaderProps {
|
||||
member: ResolvedTeamMember;
|
||||
runtimeSummary?: string;
|
||||
isTeamAlive?: boolean;
|
||||
isTeamProvisioning?: boolean;
|
||||
leadActivity?: LeadActivityState;
|
||||
|
|
@ -38,6 +39,7 @@ interface MemberDetailHeaderProps {
|
|||
|
||||
export const MemberDetailHeader = ({
|
||||
member,
|
||||
runtimeSummary,
|
||||
isTeamAlive,
|
||||
isTeamProvisioning,
|
||||
leadActivity,
|
||||
|
|
@ -139,6 +141,9 @@ export const MemberDetailHeader = ({
|
|||
{/* NOTE: lead context token display disabled — usage formula is inaccurate */}
|
||||
</>
|
||||
)}
|
||||
{!editing && runtimeSummary ? (
|
||||
<div className="mt-1 text-xs text-[var(--color-text-muted)]">{runtimeSummary}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
|
|||
import type { TaskStatusCounts } from '@renderer/utils/pathNormalize';
|
||||
import type {
|
||||
LeadActivityState,
|
||||
TeamAgentRuntimeEntry,
|
||||
MemberSpawnStatusEntry,
|
||||
ResolvedTeamMember,
|
||||
TeamTaskWithKanban,
|
||||
|
|
@ -21,6 +22,7 @@ interface MemberListProps {
|
|||
taskMap?: Map<string, TeamTaskWithKanban>;
|
||||
pendingRepliesByMember?: Record<string, number>;
|
||||
memberSpawnStatuses?: Map<string, MemberSpawnStatusEntry>;
|
||||
memberRuntimeEntries?: Map<string, TeamAgentRuntimeEntry>;
|
||||
isLaunchSettling?: boolean;
|
||||
isTeamAlive?: boolean;
|
||||
isTeamProvisioning?: boolean;
|
||||
|
|
@ -169,6 +171,30 @@ function areLaunchParamsEquivalent(
|
|||
);
|
||||
}
|
||||
|
||||
function areMemberRuntimeEntriesEquivalent(
|
||||
left: Map<string, TeamAgentRuntimeEntry> | undefined,
|
||||
right: Map<string, TeamAgentRuntimeEntry> | undefined
|
||||
): boolean {
|
||||
if (left === right) return true;
|
||||
if (!left || !right) return left === right;
|
||||
if (left.size !== right.size) return false;
|
||||
for (const [key, leftEntry] of left) {
|
||||
const rightEntry = right.get(key);
|
||||
if (
|
||||
leftEntry.memberName !== rightEntry?.memberName ||
|
||||
leftEntry.alive !== rightEntry?.alive ||
|
||||
leftEntry.restartable !== rightEntry?.restartable ||
|
||||
leftEntry.backendType !== rightEntry?.backendType ||
|
||||
leftEntry.pid !== rightEntry?.pid ||
|
||||
leftEntry.runtimeModel !== rightEntry?.runtimeModel ||
|
||||
leftEntry.rssBytes !== rightEntry?.rssBytes
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function areMemberListPropsEqual(
|
||||
prev: Readonly<MemberListProps>,
|
||||
next: Readonly<MemberListProps>
|
||||
|
|
@ -179,6 +205,7 @@ function areMemberListPropsEqual(
|
|||
areMemberTaskMapsEquivalent(prev.taskMap, next.taskMap) &&
|
||||
arePendingRepliesEquivalent(prev.pendingRepliesByMember, next.pendingRepliesByMember) &&
|
||||
areMemberSpawnStatusesEquivalent(prev.memberSpawnStatuses, next.memberSpawnStatuses) &&
|
||||
areMemberRuntimeEntriesEquivalent(prev.memberRuntimeEntries, next.memberRuntimeEntries) &&
|
||||
prev.isLaunchSettling === next.isLaunchSettling &&
|
||||
prev.isTeamAlive === next.isTeamAlive &&
|
||||
prev.isTeamProvisioning === next.isTeamProvisioning &&
|
||||
|
|
@ -193,6 +220,7 @@ export const MemberList = memo(function MemberList({
|
|||
taskMap,
|
||||
pendingRepliesByMember,
|
||||
memberSpawnStatuses,
|
||||
memberRuntimeEntries,
|
||||
isLaunchSettling,
|
||||
isTeamAlive,
|
||||
isTeamProvisioning,
|
||||
|
|
@ -240,9 +268,10 @@ export const MemberList = memo(function MemberList({
|
|||
const buildRuntimeSummary = useCallback(
|
||||
(
|
||||
member: ResolvedTeamMember,
|
||||
spawnEntry: MemberSpawnStatusEntry | undefined
|
||||
spawnEntry: MemberSpawnStatusEntry | undefined,
|
||||
runtimeEntry: TeamAgentRuntimeEntry | undefined
|
||||
): string | undefined => {
|
||||
return resolveMemberRuntimeSummary(member, launchParams, spawnEntry);
|
||||
return resolveMemberRuntimeSummary(member, launchParams, spawnEntry, runtimeEntry);
|
||||
},
|
||||
[launchParams]
|
||||
);
|
||||
|
|
@ -275,6 +304,7 @@ export const MemberList = memo(function MemberList({
|
|||
reviewCandidate && reviewCandidate.id !== member.currentTaskId ? reviewCandidate : null;
|
||||
const awaitingReply = isTeamAlive !== false && Boolean(pendingRepliesByMember?.[member.name]);
|
||||
const spawnEntry = memberSpawnStatuses?.get(member.name);
|
||||
const runtimeEntry = memberRuntimeEntries?.get(member.name);
|
||||
return (
|
||||
<MemberCard
|
||||
key={member.name}
|
||||
|
|
@ -288,7 +318,11 @@ export const MemberList = memo(function MemberList({
|
|||
reviewTask={isRemoved ? null : reviewTask}
|
||||
isAwaitingReply={isRemoved ? false : awaitingReply}
|
||||
isRemoved={isRemoved}
|
||||
runtimeSummary={buildRuntimeSummary(member, isRemoved ? undefined : spawnEntry)}
|
||||
runtimeSummary={buildRuntimeSummary(
|
||||
member,
|
||||
isRemoved ? undefined : spawnEntry,
|
||||
isRemoved ? undefined : runtimeEntry
|
||||
)}
|
||||
spawnStatus={isRemoved ? undefined : spawnEntry?.status}
|
||||
spawnError={isRemoved ? undefined : spawnEntry?.error}
|
||||
spawnLivenessSource={isRemoved ? undefined : spawnEntry?.livenessSource}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import type {
|
|||
LeadActivityState,
|
||||
LeadContextUsage,
|
||||
MemberActivityMetaEntry,
|
||||
TeamAgentRuntimeEntry,
|
||||
TeamAgentRuntimeSnapshot,
|
||||
MemberSpawnStatusEntry,
|
||||
MemberSpawnStatusesSnapshot,
|
||||
PersistedTeamLaunchSummary,
|
||||
|
|
@ -251,6 +253,7 @@ function collectTeamScopedStateRemovals(
|
|||
TeamSlice,
|
||||
| 'provisioningRuns'
|
||||
| 'teamDataCacheByName'
|
||||
| 'teamAgentRuntimeByTeam'
|
||||
| 'teamMessagesByName'
|
||||
| 'memberActivityMetaByTeam'
|
||||
| 'provisioningSnapshotByTeam'
|
||||
|
|
@ -272,6 +275,7 @@ function collectTeamScopedStateRemovals(
|
|||
Object.entries(state.provisioningRuns).filter(([, run]) => run.teamName !== teamName)
|
||||
) as Record<string, TeamProvisioningProgress>;
|
||||
const nextTeamDataCache = omitTeamKey(state.teamDataCacheByName, teamName);
|
||||
const nextTeamAgentRuntime = omitTeamKey(state.teamAgentRuntimeByTeam, teamName);
|
||||
const nextTeamMessages = omitTeamKey(state.teamMessagesByName, teamName);
|
||||
const nextMemberActivityMeta = omitTeamKey(state.memberActivityMetaByTeam, teamName);
|
||||
const nextProvisioningSnapshot = omitTeamKey(state.provisioningSnapshotByTeam, teamName);
|
||||
|
|
@ -295,6 +299,7 @@ function collectTeamScopedStateRemovals(
|
|||
? { provisioningRuns: nextProvisioningRuns }
|
||||
: {}),
|
||||
...(nextTeamDataCache ? { teamDataCacheByName: nextTeamDataCache } : {}),
|
||||
...(nextTeamAgentRuntime ? { teamAgentRuntimeByTeam: nextTeamAgentRuntime } : {}),
|
||||
...(nextTeamMessages ? { teamMessagesByName: nextTeamMessages } : {}),
|
||||
...(nextMemberActivityMeta ? { memberActivityMetaByTeam: nextMemberActivityMeta } : {}),
|
||||
...(nextProvisioningSnapshot ? { provisioningSnapshotByTeam: nextProvisioningSnapshot } : {}),
|
||||
|
|
@ -786,6 +791,47 @@ function maybeLogMemberSpawnUiEqualSuppressed(
|
|||
);
|
||||
}
|
||||
|
||||
function areTeamAgentRuntimeEntriesEqual(
|
||||
left: TeamAgentRuntimeEntry | undefined,
|
||||
right: TeamAgentRuntimeEntry | undefined
|
||||
): boolean {
|
||||
if (left === right) return true;
|
||||
if (!left || !right) return left === right;
|
||||
return (
|
||||
left.memberName === right.memberName &&
|
||||
left.alive === right.alive &&
|
||||
left.restartable === right.restartable &&
|
||||
left.backendType === right.backendType &&
|
||||
left.pid === right.pid &&
|
||||
left.runtimeModel === right.runtimeModel &&
|
||||
left.rssBytes === right.rssBytes
|
||||
);
|
||||
}
|
||||
|
||||
function areTeamAgentRuntimeSnapshotsEqual(
|
||||
left: TeamAgentRuntimeSnapshot | undefined,
|
||||
right: TeamAgentRuntimeSnapshot
|
||||
): boolean {
|
||||
if (!left) return false;
|
||||
if (left.teamName !== right.teamName || left.runId !== right.runId) {
|
||||
return false;
|
||||
}
|
||||
const leftKeys = Object.keys(left.members);
|
||||
const rightKeys = Object.keys(right.members);
|
||||
if (leftKeys.length !== rightKeys.length) {
|
||||
return false;
|
||||
}
|
||||
for (const key of leftKeys) {
|
||||
if (!(key in right.members)) {
|
||||
return false;
|
||||
}
|
||||
if (!areTeamAgentRuntimeEntriesEqual(left.members[key], right.members[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function compareInboxMessagesByTimestamp(a: InboxMessage, b: InboxMessage): number {
|
||||
const aTime = Date.parse(a.timestamp);
|
||||
const bTime = Date.parse(b.timestamp);
|
||||
|
|
@ -1979,7 +2025,9 @@ export interface TeamSlice {
|
|||
/** Per-team per-member spawn statuses during team provisioning/launch. */
|
||||
memberSpawnStatusesByTeam: Record<string, Record<string, MemberSpawnStatusEntry>>;
|
||||
memberSpawnSnapshotsByTeam: Record<string, MemberSpawnStatusesSnapshot>;
|
||||
teamAgentRuntimeByTeam: Record<string, TeamAgentRuntimeSnapshot>;
|
||||
fetchMemberSpawnStatuses: (teamName: string) => Promise<void>;
|
||||
fetchTeamAgentRuntime: (teamName: string) => Promise<void>;
|
||||
provisioningErrorByTeam: Record<string, string | null>;
|
||||
clearProvisioningError: (teamName?: string) => void;
|
||||
/** Per-team launch parameters (model, effort, extended context) — persisted in localStorage. */
|
||||
|
|
@ -2074,6 +2122,7 @@ export interface TeamSlice {
|
|||
request: AddTaskCommentRequest
|
||||
) => Promise<TaskComment>;
|
||||
addMember: (teamName: string, request: AddMemberRequest) => Promise<void>;
|
||||
restartMember: (teamName: string, memberName: string) => Promise<void>;
|
||||
removeMember: (teamName: string, memberName: string) => Promise<void>;
|
||||
updateMemberRole: (
|
||||
teamName: string,
|
||||
|
|
@ -2303,6 +2352,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
toolHistoryByTeam: {},
|
||||
memberSpawnStatusesByTeam: {},
|
||||
memberSpawnSnapshotsByTeam: {},
|
||||
teamAgentRuntimeByTeam: {},
|
||||
provisioningErrorByTeam: {},
|
||||
clearProvisioningError: (teamName?: string) =>
|
||||
set((state) => {
|
||||
|
|
@ -2399,6 +2449,36 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
// ignore — spawn statuses are best-effort
|
||||
}
|
||||
},
|
||||
fetchTeamAgentRuntime: async (teamName: string) => {
|
||||
if (!api.teams?.getTeamAgentRuntime) return;
|
||||
try {
|
||||
const snapshot = await api.teams.getTeamAgentRuntime(teamName);
|
||||
set((prev) => {
|
||||
if (snapshot.runId != null && prev.ignoredRuntimeRunIds[snapshot.runId] === teamName) {
|
||||
return {};
|
||||
}
|
||||
if (
|
||||
snapshot.runId != null &&
|
||||
prev.currentRuntimeRunIdByTeam[teamName] != null &&
|
||||
prev.currentRuntimeRunIdByTeam[teamName] !== snapshot.runId
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
const previousSnapshot = prev.teamAgentRuntimeByTeam[teamName];
|
||||
if (areTeamAgentRuntimeSnapshotsEqual(previousSnapshot, snapshot)) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
teamAgentRuntimeByTeam: {
|
||||
...prev.teamAgentRuntimeByTeam,
|
||||
[teamName]: snapshot,
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
// ignore — runtime snapshots are best-effort
|
||||
}
|
||||
},
|
||||
kanbanFilterQuery: null,
|
||||
globalTaskDetail: null,
|
||||
pendingMemberProfile: null,
|
||||
|
|
@ -4098,6 +4178,14 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
await get().refreshTeamData(teamName);
|
||||
},
|
||||
|
||||
restartMember: async (teamName: string, memberName: string) => {
|
||||
await unwrapIpc('team:restartMember', () => api.teams.restartMember(teamName, memberName));
|
||||
await Promise.all([
|
||||
get().fetchMemberSpawnStatuses(teamName),
|
||||
get().fetchTeamAgentRuntime(teamName),
|
||||
]);
|
||||
},
|
||||
|
||||
removeMember: async (teamName: string, memberName: string) => {
|
||||
await unwrapIpc('team:removeMember', () => api.teams.removeMember(teamName, memberName));
|
||||
await get().refreshTeamData(teamName);
|
||||
|
|
@ -4244,6 +4332,8 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
delete nextSpawnStatuses[request.teamName];
|
||||
const nextSpawnSnapshots = { ...state.memberSpawnSnapshotsByTeam };
|
||||
delete nextSpawnSnapshots[request.teamName];
|
||||
const nextRuntime = { ...state.teamAgentRuntimeByTeam };
|
||||
delete nextRuntime[request.teamName];
|
||||
const nextActiveTools = { ...state.activeToolsByTeam };
|
||||
delete nextActiveTools[request.teamName];
|
||||
const nextFinishedVisible = { ...state.finishedVisibleByTeam };
|
||||
|
|
@ -4265,6 +4355,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
provisioningErrorByTeam: nextErrors,
|
||||
memberSpawnStatusesByTeam: nextSpawnStatuses,
|
||||
memberSpawnSnapshotsByTeam: nextSpawnSnapshots,
|
||||
teamAgentRuntimeByTeam: nextRuntime,
|
||||
activeToolsByTeam: nextActiveTools,
|
||||
finishedVisibleByTeam: nextFinishedVisible,
|
||||
toolHistoryByTeam: nextToolHistory,
|
||||
|
|
@ -4430,6 +4521,8 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
delete nextSpawnStatuses[request.teamName];
|
||||
const nextSpawnSnapshots = { ...state.memberSpawnSnapshotsByTeam };
|
||||
delete nextSpawnSnapshots[request.teamName];
|
||||
const nextRuntime = { ...state.teamAgentRuntimeByTeam };
|
||||
delete nextRuntime[request.teamName];
|
||||
const nextActiveTools = { ...state.activeToolsByTeam };
|
||||
delete nextActiveTools[request.teamName];
|
||||
const nextFinishedVisible = { ...state.finishedVisibleByTeam };
|
||||
|
|
@ -4451,6 +4544,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
provisioningErrorByTeam: nextErrors,
|
||||
memberSpawnStatusesByTeam: nextSpawnStatuses,
|
||||
memberSpawnSnapshotsByTeam: nextSpawnSnapshots,
|
||||
teamAgentRuntimeByTeam: nextRuntime,
|
||||
activeToolsByTeam: nextActiveTools,
|
||||
finishedVisibleByTeam: nextFinishedVisible,
|
||||
toolHistoryByTeam: nextToolHistory,
|
||||
|
|
@ -4608,9 +4702,11 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
|
||||
const nextSpawnStatuses = { ...state.memberSpawnStatusesByTeam };
|
||||
const nextSpawnSnapshots = { ...state.memberSpawnSnapshotsByTeam };
|
||||
const nextRuntime = { ...state.teamAgentRuntimeByTeam };
|
||||
if (isCanonicalRun) {
|
||||
delete nextSpawnStatuses[existing.teamName];
|
||||
delete nextSpawnSnapshots[existing.teamName];
|
||||
delete nextRuntime[existing.teamName];
|
||||
}
|
||||
const nextActiveTools = { ...state.activeToolsByTeam };
|
||||
const nextFinishedVisible = { ...state.finishedVisibleByTeam };
|
||||
|
|
@ -4627,6 +4723,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
currentRuntimeRunIdByTeam: nextRuntimeRunIdByTeam,
|
||||
memberSpawnStatusesByTeam: nextSpawnStatuses,
|
||||
memberSpawnSnapshotsByTeam: nextSpawnSnapshots,
|
||||
teamAgentRuntimeByTeam: nextRuntime,
|
||||
activeToolsByTeam: nextActiveTools,
|
||||
finishedVisibleByTeam: nextFinishedVisible,
|
||||
toolHistoryByTeam: nextToolHistory,
|
||||
|
|
@ -4751,11 +4848,16 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
set((prev) => {
|
||||
const next = { ...prev.memberSpawnStatusesByTeam };
|
||||
const nextSnapshots = { ...prev.memberSpawnSnapshotsByTeam };
|
||||
const nextRuntime = { ...prev.teamAgentRuntimeByTeam };
|
||||
const currentStatuses = next[progress.teamName];
|
||||
if (!currentStatuses) {
|
||||
if (progress.state !== 'ready') {
|
||||
delete nextRuntime[progress.teamName];
|
||||
}
|
||||
return {
|
||||
memberSpawnStatusesByTeam: next,
|
||||
memberSpawnSnapshotsByTeam: nextSnapshots,
|
||||
teamAgentRuntimeByTeam: nextRuntime,
|
||||
};
|
||||
}
|
||||
if (progress.state === 'ready') {
|
||||
|
|
@ -4763,6 +4865,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
return {
|
||||
memberSpawnStatusesByTeam: next,
|
||||
memberSpawnSnapshotsByTeam: nextSnapshots,
|
||||
teamAgentRuntimeByTeam: nextRuntime,
|
||||
};
|
||||
}
|
||||
const retainedStatuses = Object.fromEntries(
|
||||
|
|
@ -4774,9 +4877,11 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
delete next[progress.teamName];
|
||||
delete nextSnapshots[progress.teamName];
|
||||
}
|
||||
delete nextRuntime[progress.teamName];
|
||||
return {
|
||||
memberSpawnStatusesByTeam: next,
|
||||
memberSpawnSnapshotsByTeam: nextSnapshots,
|
||||
teamAgentRuntimeByTeam: nextRuntime,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector';
|
||||
import { formatBytes } from '@renderer/utils/formatters';
|
||||
import { inferTeamProviderIdFromModel } from '@shared/utils/teamProvider';
|
||||
|
||||
import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
|
||||
import type { MemberSpawnStatusEntry, ResolvedTeamMember, TeamProviderId } from '@shared/types';
|
||||
import type {
|
||||
MemberSpawnStatusEntry,
|
||||
ResolvedTeamMember,
|
||||
TeamAgentRuntimeEntry,
|
||||
TeamProviderId,
|
||||
} from '@shared/types';
|
||||
|
||||
function isMemberLaunchPending(spawnEntry: MemberSpawnStatusEntry | undefined): boolean {
|
||||
if (!spawnEntry) {
|
||||
|
|
@ -20,22 +26,27 @@ function isMemberLaunchPending(spawnEntry: MemberSpawnStatusEntry | undefined):
|
|||
export function resolveMemberRuntimeSummary(
|
||||
member: ResolvedTeamMember,
|
||||
launchParams: TeamLaunchParams | undefined,
|
||||
spawnEntry: MemberSpawnStatusEntry | undefined
|
||||
spawnEntry: MemberSpawnStatusEntry | undefined,
|
||||
runtimeEntry?: TeamAgentRuntimeEntry
|
||||
): string | undefined {
|
||||
const configuredProvider: TeamProviderId =
|
||||
member.providerId ?? launchParams?.providerId ?? 'anthropic';
|
||||
const configuredModel = member.model?.trim() || launchParams?.model?.trim() || '';
|
||||
const configuredEffort = member.effort ?? launchParams?.effort;
|
||||
const runtimeModel = spawnEntry?.runtimeModel?.trim();
|
||||
const runtimeModel = spawnEntry?.runtimeModel?.trim() || runtimeEntry?.runtimeModel?.trim();
|
||||
const memorySuffix =
|
||||
typeof runtimeEntry?.rssBytes === 'number' && runtimeEntry.rssBytes > 0
|
||||
? ` · ${formatBytes(runtimeEntry.rssBytes)}`
|
||||
: '';
|
||||
|
||||
if (runtimeModel && (isMemberLaunchPending(spawnEntry) || configuredModel.length === 0)) {
|
||||
const runtimeProvider = inferTeamProviderIdFromModel(runtimeModel) ?? configuredProvider;
|
||||
return formatTeamModelSummary(runtimeProvider, runtimeModel, configuredEffort);
|
||||
return `${formatTeamModelSummary(runtimeProvider, runtimeModel, configuredEffort)}${memorySuffix}`;
|
||||
}
|
||||
|
||||
if (isMemberLaunchPending(spawnEntry)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return formatTeamModelSummary(configuredProvider, configuredModel, configuredEffort);
|
||||
return `${formatTeamModelSummary(configuredProvider, configuredModel, configuredEffort)}${memorySuffix}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import type {
|
|||
LeadContextUsageSnapshot,
|
||||
MemberFullStats,
|
||||
MemberLogSummary,
|
||||
TeamAgentRuntimeSnapshot,
|
||||
MemberSpawnStatusesSnapshot,
|
||||
MessagesPage,
|
||||
ProjectBranchChangeEvent,
|
||||
|
|
@ -534,6 +535,8 @@ export interface TeamsAPI {
|
|||
getLeadActivity: (teamName: string) => Promise<LeadActivitySnapshot>;
|
||||
getLeadContext: (teamName: string) => Promise<LeadContextUsageSnapshot>;
|
||||
getMemberSpawnStatuses: (teamName: string) => Promise<MemberSpawnStatusesSnapshot>;
|
||||
getTeamAgentRuntime: (teamName: string) => Promise<TeamAgentRuntimeSnapshot>;
|
||||
restartMember: (teamName: string, memberName: string) => Promise<void>;
|
||||
softDeleteTask: (teamName: string, taskId: string) => Promise<void>;
|
||||
restoreTask: (teamName: string, taskId: string) => Promise<void>;
|
||||
getDeletedTasks: (teamName: string) => Promise<TeamTask[]>;
|
||||
|
|
|
|||
|
|
@ -907,6 +907,26 @@ export interface MemberSpawnStatusesSnapshot {
|
|||
|
||||
export type MemberSpawnLivenessSource = 'heartbeat' | 'process';
|
||||
|
||||
export type TeamAgentRuntimeBackendType = 'lead' | 'tmux' | 'iterm2' | 'in-process' | 'process';
|
||||
|
||||
export interface TeamAgentRuntimeEntry {
|
||||
memberName: string;
|
||||
alive: boolean;
|
||||
restartable: boolean;
|
||||
backendType?: TeamAgentRuntimeBackendType;
|
||||
pid?: number;
|
||||
runtimeModel?: string;
|
||||
rssBytes?: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TeamAgentRuntimeSnapshot {
|
||||
teamName: string;
|
||||
updatedAt: string;
|
||||
runId: string | null;
|
||||
members: Record<string, TeamAgentRuntimeEntry>;
|
||||
}
|
||||
|
||||
export interface TeamChangeEvent {
|
||||
type:
|
||||
| 'config'
|
||||
|
|
|
|||
|
|
@ -24,6 +24,19 @@ vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
|
|||
ClaudeBinaryResolver: { resolve: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@features/tmux-installer/main', () => ({
|
||||
killTmuxPaneForCurrentPlatformSync: vi.fn(),
|
||||
listTmuxPanePidsForCurrentPlatform: vi.fn(async () => new Map()),
|
||||
isTmuxRuntimeReadyForCurrentPlatform: vi.fn(async () => true),
|
||||
}));
|
||||
|
||||
vi.mock('pidusage', () => {
|
||||
const pidusageMock = vi.fn();
|
||||
return {
|
||||
default: pidusageMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@main/services/team/TeamTaskReader', () => ({
|
||||
TeamTaskReader: class {
|
||||
async getTasks() {
|
||||
|
|
@ -61,6 +74,8 @@ import { getTeamLaunchStatePath } from '@main/services/team/TeamLaunchStateStore
|
|||
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
||||
import { spawnCli } from '@main/utils/childProcess';
|
||||
import { AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES } from 'agent-teams-controller';
|
||||
import { listTmuxPanePidsForCurrentPlatform } from '@features/tmux-installer/main';
|
||||
import pidusage from 'pidusage';
|
||||
|
||||
function allowConsoleLogs() {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
|
@ -91,6 +106,18 @@ function createRunningChild() {
|
|||
});
|
||||
}
|
||||
|
||||
function createPidusageStat(pid: number, memory: number) {
|
||||
return {
|
||||
cpu: 0,
|
||||
memory,
|
||||
ppid: 1,
|
||||
pid,
|
||||
ctime: 0,
|
||||
elapsed: 0,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function writeLaunchConfig(
|
||||
teamName: string,
|
||||
projectPath: string,
|
||||
|
|
@ -256,6 +283,100 @@ describe('TeamProvisioningService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getTeamAgentRuntimeSnapshot', () => {
|
||||
it('uses batched pidusage rss values for lead and teammates', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).configReader = {
|
||||
getConfig: vi.fn(async () => ({
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead' },
|
||||
{ name: 'alice', model: 'gpt-5.4-mini' },
|
||||
],
|
||||
})),
|
||||
};
|
||||
(svc as any).readPersistedRuntimeMembers = vi.fn(() => [
|
||||
{
|
||||
name: 'alice',
|
||||
agentId: 'alice@runtime-team',
|
||||
tmuxPaneId: '%1',
|
||||
backendType: 'tmux',
|
||||
},
|
||||
]);
|
||||
(svc as any).aliveRunByTeam.set('runtime-team', 'run-1');
|
||||
(svc as any).runs.set('run-1', {
|
||||
runId: 'run-1',
|
||||
child: { pid: 111 },
|
||||
request: { model: 'gpt-5.4' },
|
||||
processKilled: false,
|
||||
cancelRequested: false,
|
||||
spawnContext: null,
|
||||
});
|
||||
vi.mocked(listTmuxPanePidsForCurrentPlatform).mockResolvedValueOnce(new Map([['%1', 222]]));
|
||||
|
||||
vi.mocked(pidusage).mockResolvedValueOnce({
|
||||
'111': createPidusageStat(111, 123_000_000),
|
||||
'222': createPidusageStat(222, 456_000_000),
|
||||
} as any);
|
||||
|
||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||
|
||||
expect(pidusage).toHaveBeenCalledWith([111, 222], { maxage: 0 });
|
||||
expect(snapshot.members['team-lead']).toMatchObject({
|
||||
pid: 111,
|
||||
rssBytes: 123_000_000,
|
||||
runtimeModel: 'gpt-5.4',
|
||||
});
|
||||
expect(snapshot.members.alice).toMatchObject({
|
||||
pid: 222,
|
||||
rssBytes: 456_000_000,
|
||||
runtimeModel: 'gpt-5.4-mini',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to per-pid pidusage reads when batched sampling fails', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).configReader = {
|
||||
getConfig: vi.fn(async () => ({
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead' },
|
||||
{ name: 'alice', model: 'gpt-5.4-mini' },
|
||||
],
|
||||
})),
|
||||
};
|
||||
(svc as any).readPersistedRuntimeMembers = vi.fn(() => [
|
||||
{
|
||||
name: 'alice',
|
||||
agentId: 'alice@runtime-team',
|
||||
tmuxPaneId: '%1',
|
||||
backendType: 'tmux',
|
||||
},
|
||||
]);
|
||||
(svc as any).aliveRunByTeam.set('runtime-team', 'run-1');
|
||||
(svc as any).runs.set('run-1', {
|
||||
runId: 'run-1',
|
||||
child: { pid: 111 },
|
||||
request: { model: 'gpt-5.4' },
|
||||
processKilled: false,
|
||||
cancelRequested: false,
|
||||
spawnContext: null,
|
||||
});
|
||||
vi.mocked(listTmuxPanePidsForCurrentPlatform).mockResolvedValueOnce(new Map([['%1', 222]]));
|
||||
|
||||
vi.mocked(pidusage)
|
||||
.mockRejectedValueOnce(new Error('ps: process exited'))
|
||||
.mockResolvedValueOnce(createPidusageStat(111, 123_000_000) as any)
|
||||
.mockResolvedValueOnce(createPidusageStat(222, 456_000_000) as any);
|
||||
|
||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||
|
||||
expect(pidusage).toHaveBeenNthCalledWith(1, [111, 222], { maxage: 0 });
|
||||
expect(pidusage).toHaveBeenNthCalledWith(2, 111, { maxage: 0 });
|
||||
expect(pidusage).toHaveBeenNthCalledWith(3, 222, { maxage: 0 });
|
||||
expect(snapshot.members['team-lead']?.rssBytes).toBe(123_000_000);
|
||||
expect(snapshot.members.alice?.rssBytes).toBe(456_000_000);
|
||||
});
|
||||
});
|
||||
|
||||
it('removes generated MCP config when createTeam spawn fails synchronously', async () => {
|
||||
allowConsoleLogs();
|
||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/mock/claude');
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
|
|||
|
||||
import {
|
||||
buildAddMemberSpawnMessage,
|
||||
buildRestartMemberSpawnMessage,
|
||||
TeamProvisioningService,
|
||||
} from '@main/services/team/TeamProvisioningService';
|
||||
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
||||
|
|
@ -279,6 +280,21 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
|
|||
await svc.cancelProvisioning(runId);
|
||||
});
|
||||
|
||||
it('restart teammate message keeps the exact teammate identity and avoids duplicate semantics', () => {
|
||||
const message = buildRestartMemberSpawnMessage('forge-labs', 'Forge Labs', 'lead', {
|
||||
name: 'alice',
|
||||
role: 'Reviewer',
|
||||
providerId: 'codex',
|
||||
model: 'gpt-5.4-mini',
|
||||
effort: 'medium',
|
||||
});
|
||||
|
||||
expect(message).toContain('Teammate "alice" with role "Reviewer" was restarted from the UI.');
|
||||
expect(message).toContain('team_name="forge-labs", name="alice"');
|
||||
expect(message).toContain('provider="codex", model="gpt-5.4-mini", effort="medium"');
|
||||
expect(message).toContain('This is a restart of an existing persistent teammate, not a new teammate.');
|
||||
});
|
||||
|
||||
it('createTeam materializes an explicit Codex default model for teammates before bootstrap spawn', async () => {
|
||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
|
||||
const { child } = createFakeChild();
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@ const hoisted = vi.hoisted(() => ({
|
|||
launchTeam: vi.fn(),
|
||||
getProvisioningStatus: vi.fn(),
|
||||
getMemberSpawnStatuses: vi.fn(),
|
||||
getTeamAgentRuntime: vi.fn(),
|
||||
cancelProvisioning: vi.fn(),
|
||||
deleteTeam: vi.fn(),
|
||||
restoreTeam: vi.fn(),
|
||||
permanentlyDeleteTeam: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
restartMember: vi.fn(),
|
||||
requestReview: vi.fn(),
|
||||
updateKanban: vi.fn(),
|
||||
invalidateTaskChangeSummaries: vi.fn(),
|
||||
|
|
@ -44,11 +46,13 @@ vi.mock('@renderer/api', () => ({
|
|||
launchTeam: hoisted.launchTeam,
|
||||
getProvisioningStatus: hoisted.getProvisioningStatus,
|
||||
getMemberSpawnStatuses: hoisted.getMemberSpawnStatuses,
|
||||
getTeamAgentRuntime: hoisted.getTeamAgentRuntime,
|
||||
cancelProvisioning: hoisted.cancelProvisioning,
|
||||
deleteTeam: hoisted.deleteTeam,
|
||||
restoreTeam: hoisted.restoreTeam,
|
||||
permanentlyDeleteTeam: hoisted.permanentlyDeleteTeam,
|
||||
sendMessage: hoisted.sendMessage,
|
||||
restartMember: hoisted.restartMember,
|
||||
requestReview: hoisted.requestReview,
|
||||
updateKanban: hoisted.updateKanban,
|
||||
onProvisioningProgress: hoisted.onProvisioningProgress,
|
||||
|
|
@ -169,6 +173,27 @@ function createDeferredPromise<T>() {
|
|||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function createRuntimeSnapshot(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
teamName: 'my-team',
|
||||
updatedAt: '2026-03-12T10:00:00.000Z',
|
||||
runId: 'runtime-run',
|
||||
members: {
|
||||
alice: {
|
||||
memberName: 'alice',
|
||||
alive: true,
|
||||
restartable: true,
|
||||
backendType: 'tmux',
|
||||
pid: 4242,
|
||||
runtimeModel: 'gpt-5.4-mini',
|
||||
rssBytes: 256 * 1024 * 1024,
|
||||
updatedAt: '2026-03-12T10:00:00.000Z',
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('teamSlice actions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -202,10 +227,12 @@ describe('teamSlice actions', () => {
|
|||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
hoisted.getMemberSpawnStatuses.mockResolvedValue({ statuses: {}, runId: null });
|
||||
hoisted.getTeamAgentRuntime.mockResolvedValue(createRuntimeSnapshot({ runId: null, members: {} }));
|
||||
hoisted.cancelProvisioning.mockResolvedValue(undefined);
|
||||
hoisted.deleteTeam.mockResolvedValue(undefined);
|
||||
hoisted.restoreTeam.mockResolvedValue(undefined);
|
||||
hoisted.permanentlyDeleteTeam.mockResolvedValue(undefined);
|
||||
hoisted.restartMember.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('maps inbox verify failure to user-friendly text', async () => {
|
||||
|
|
@ -2349,6 +2376,64 @@ describe('teamSlice actions', () => {
|
|||
expect(store.getState().currentRuntimeRunIdByTeam['my-team']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('stores runtime snapshots and suppresses semantic no-op refreshes', async () => {
|
||||
const store = createSliceStore();
|
||||
const snapshot = createRuntimeSnapshot();
|
||||
hoisted.getTeamAgentRuntime.mockResolvedValue(snapshot);
|
||||
|
||||
await store.getState().fetchTeamAgentRuntime('my-team');
|
||||
const firstSnapshot = store.getState().teamAgentRuntimeByTeam['my-team'];
|
||||
|
||||
expect(firstSnapshot).toEqual(snapshot);
|
||||
|
||||
hoisted.getTeamAgentRuntime.mockResolvedValue({
|
||||
...snapshot,
|
||||
updatedAt: '2026-03-12T10:00:05.000Z',
|
||||
members: {
|
||||
alice: {
|
||||
...snapshot.members.alice,
|
||||
updatedAt: '2026-03-12T10:00:05.000Z',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await store.getState().fetchTeamAgentRuntime('my-team');
|
||||
|
||||
expect(store.getState().teamAgentRuntimeByTeam['my-team']).toBe(firstSnapshot);
|
||||
});
|
||||
|
||||
it('restartMember refreshes spawn statuses and runtime snapshot', async () => {
|
||||
const store = createSliceStore();
|
||||
hoisted.getMemberSpawnStatuses.mockResolvedValue({
|
||||
statuses: {
|
||||
alice: createMemberSpawnStatus({ status: 'spawning', launchState: 'starting' }),
|
||||
},
|
||||
runId: 'runtime-run',
|
||||
});
|
||||
hoisted.getTeamAgentRuntime.mockResolvedValue(createRuntimeSnapshot());
|
||||
|
||||
await store.getState().restartMember('my-team', 'alice');
|
||||
|
||||
expect(hoisted.restartMember).toHaveBeenCalledWith('my-team', 'alice');
|
||||
expect(store.getState().memberSpawnStatusesByTeam['my-team']).toEqual({
|
||||
alice: expect.objectContaining({ status: 'spawning', launchState: 'starting' }),
|
||||
});
|
||||
expect(store.getState().teamAgentRuntimeByTeam['my-team']).toEqual(createRuntimeSnapshot());
|
||||
});
|
||||
|
||||
it('clears stale runtime snapshots on delete', async () => {
|
||||
const store = createSliceStore();
|
||||
store.setState({
|
||||
teamAgentRuntimeByTeam: {
|
||||
'my-team': createRuntimeSnapshot(),
|
||||
},
|
||||
});
|
||||
|
||||
await store.getState().deleteTeam('my-team');
|
||||
|
||||
expect(store.getState().teamAgentRuntimeByTeam['my-team']).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('refreshTeamData provisioning safety', () => {
|
||||
it('does not set fatal error on TEAM_PROVISIONING', async () => {
|
||||
const store = createSliceStore();
|
||||
|
|
|
|||
|
|
@ -63,4 +63,21 @@ describe('resolveMemberRuntimeSummary', () => {
|
|||
|
||||
expect(resolveMemberRuntimeSummary(member, undefined, spawnEntry)).toBe('5.4 Mini · Medium');
|
||||
});
|
||||
|
||||
it('appends runtime memory when a live process snapshot is available', () => {
|
||||
const member = createMember({ model: 'gpt-5.4-mini' });
|
||||
const runtimeEntry = {
|
||||
memberName: 'alice',
|
||||
alive: true,
|
||||
restartable: true,
|
||||
pid: 4242,
|
||||
runtimeModel: 'gpt-5.4-mini',
|
||||
rssBytes: 256 * 1024 * 1024,
|
||||
updatedAt: '2026-04-18T18:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(resolveMemberRuntimeSummary(member, undefined, undefined, runtimeEntry)).toBe(
|
||||
'5.4 Mini · Medium · 256.0 MB'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue