fix(graph): refresh runtime state in graph views
This commit is contained in:
parent
dffc527424
commit
9dd1572763
16 changed files with 513 additions and 46 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -53,6 +53,7 @@ temp/
|
||||||
|
|
||||||
eslint-fix/
|
eslint-fix/
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
.eslintcache-fast
|
||||||
remotion/*
|
remotion/*
|
||||||
|
|
||||||
.home/
|
.home/
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,13 @@ Default local run target:
|
||||||
- Do not start the browser/web dev mode for normal development or smoke checks. The browser path is limited and lacks the full desktop runtime, IPC, terminal, provider auth, and team lifecycle behavior.
|
- Do not start the browser/web dev mode for normal development or smoke checks. The browser path is limited and lacks the full desktop runtime, IPC, terminal, provider auth, and team lifecycle behavior.
|
||||||
- When documenting or recommending startup commands, point contributors to the desktop app unless a task explicitly asks for browser-mode internals.
|
- When documenting or recommending startup commands, point contributors to the desktop app unless a task explicitly asks for browser-mode internals.
|
||||||
|
|
||||||
|
Fast local lint:
|
||||||
|
|
||||||
|
- Use `pnpm lint:fast:files -- <changed files>` for quick preflight on files you touched.
|
||||||
|
- Use `pnpm lint:fast` for a faster source-tree lint pass when full type-aware lint is too slow.
|
||||||
|
- `lint:fast` intentionally uses `eslint.fast.config.js` without TypeScript project-service rules. It is not a replacement for `pnpm typecheck` or the full `pnpm lint` gate.
|
||||||
|
- Keep using `pnpm typecheck` after TypeScript changes, and use full `pnpm lint` when validating a broad PR or changing lint-sensitive architecture boundaries.
|
||||||
|
|
||||||
For new features:
|
For new features:
|
||||||
|
|
||||||
- Default home for medium and large features: `src/features/<feature-name>/`
|
- Default home for medium and large features: `src/features/<feature-name>/`
|
||||||
|
|
|
||||||
256
eslint.fast.config.js
Normal file
256
eslint.fast.config.js
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import boundaries from 'eslint-plugin-boundaries';
|
||||||
|
import reactPlugin from 'eslint-plugin-react';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||||
|
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||||
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
import security from 'eslint-plugin-security';
|
||||||
|
import sonarjs from 'eslint-plugin-sonarjs';
|
||||||
|
import tailwindcss from 'eslint-plugin-tailwindcss';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
name: 'fast-linter-options',
|
||||||
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores([
|
||||||
|
'dist/**',
|
||||||
|
'dist-electron/**',
|
||||||
|
'build/**',
|
||||||
|
'node_modules/**',
|
||||||
|
'out/**',
|
||||||
|
'landing/.nuxt/**',
|
||||||
|
]),
|
||||||
|
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-known-plugin-namespaces',
|
||||||
|
plugins: {
|
||||||
|
boundaries,
|
||||||
|
import: importPlugin,
|
||||||
|
security,
|
||||||
|
sonarjs,
|
||||||
|
tailwindcss,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-require-imports': 'warn',
|
||||||
|
'no-control-regex': 'warn',
|
||||||
|
'no-unsafe-finally': 'warn',
|
||||||
|
'no-useless-escape': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-typescript',
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
projectService: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'off',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'no-var': 'error',
|
||||||
|
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-imports',
|
||||||
|
files: ['src/**/*.{js,jsx,ts,tsx}', 'test/**/*.{ts,tsx}', 'packages/agent-graph/src/**/*.{ts,tsx}'],
|
||||||
|
plugins: {
|
||||||
|
'simple-import-sort': simpleImportSort,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'simple-import-sort/imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
['^\\u0000'],
|
||||||
|
['^node:'],
|
||||||
|
['^react', '^react-dom'],
|
||||||
|
['^@?\\w'],
|
||||||
|
['^@/'],
|
||||||
|
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||||
|
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||||
|
['^.+\\u0000$'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'simple-import-sort/exports': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-node-globals',
|
||||||
|
files: ['src/main/**/*.ts', 'src/preload/**/*.ts', 'scripts/**/*.{js,mjs,ts}', 'test/**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-browser-globals',
|
||||||
|
files: ['src/renderer/**/*.{ts,tsx}', 'src/features/**/renderer/**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-react',
|
||||||
|
files: ['src/renderer/**/*.{tsx,ts}', 'src/features/**/renderer/**/*.{tsx,ts}'],
|
||||||
|
plugins: {
|
||||||
|
react: reactPlugin,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
'jsx-a11y': jsxA11y,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactPlugin.configs.recommended.rules,
|
||||||
|
...reactPlugin.configs['jsx-runtime'].rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
...jsxA11y.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
|
'react-hooks/rules-of-hooks': 'warn',
|
||||||
|
'react-hooks/globals': 'off',
|
||||||
|
'react-hooks/purity': 'off',
|
||||||
|
'react-hooks/refs': 'off',
|
||||||
|
'react-hooks/set-state-in-effect': 'off',
|
||||||
|
'react-hooks/preserve-manual-memoization': 'off',
|
||||||
|
'react-hooks/immutability': 'off',
|
||||||
|
'jsx-a11y/click-events-have-key-events': 'warn',
|
||||||
|
'jsx-a11y/no-static-element-interactions': 'warn',
|
||||||
|
'jsx-a11y/label-has-associated-control': 'warn',
|
||||||
|
'jsx-a11y/no-noninteractive-tabindex': 'warn',
|
||||||
|
'jsx-a11y/no-autofocus': 'off',
|
||||||
|
'react/function-component-definition': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
namedComponents: 'arrow-function',
|
||||||
|
unnamedComponents: 'arrow-function',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react/jsx-key': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
checkFragmentShorthand: true,
|
||||||
|
checkKeyMustBeforeSpread: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react/self-closing-comp': ['error', { component: true, html: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-feature-entrypoints',
|
||||||
|
files: [
|
||||||
|
'src/main/**/*.{ts,tsx}',
|
||||||
|
'src/preload/**/*.{ts,tsx}',
|
||||||
|
'src/renderer/**/*.{ts,tsx}',
|
||||||
|
'src/shared/**/*.{ts,tsx}',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
group: [
|
||||||
|
'@features/*/contracts/*',
|
||||||
|
'@features/*/core/**',
|
||||||
|
'@features/*/main/*',
|
||||||
|
'@features/*/preload/*',
|
||||||
|
'@features/*/renderer/*',
|
||||||
|
],
|
||||||
|
message: 'Import feature public entrypoints only.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-feature-core-guards',
|
||||||
|
files: ['src/features/*/core/{domain,application}/**/*.ts'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
{ name: 'electron', message: 'Feature core must stay Electron-free.' },
|
||||||
|
{ name: 'fastify', message: 'Feature core must stay transport-free.' },
|
||||||
|
{ name: 'child_process', message: 'Feature core must not spawn processes directly.' },
|
||||||
|
{
|
||||||
|
name: 'node:child_process',
|
||||||
|
message: 'Feature core must not spawn processes directly.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
group: ['@main/*', '@preload/*', '@renderer/*'],
|
||||||
|
message: 'Feature core must stay process-agnostic.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: ['@features/*/main/**', '@features/*/preload/**', '@features/*/renderer/**'],
|
||||||
|
message: 'Feature core must not import runtime or transport layers.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'fast-feature-renderer-ui-guards',
|
||||||
|
files: ['src/features/*/renderer/ui/**/*.{ts,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
{ name: '@renderer/api', message: 'renderer/ui must stay presentational.' },
|
||||||
|
{ name: '@renderer/store', message: 'renderer/ui must stay store-free.' },
|
||||||
|
{ name: 'electron', message: 'renderer/ui must stay Electron-free.' },
|
||||||
|
],
|
||||||
|
patterns: [
|
||||||
|
{ group: ['@main/*'], message: 'renderer/ui must not import main modules.' },
|
||||||
|
{ group: ['@renderer/store/*'], message: 'renderer/ui must stay store-free.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
@ -51,6 +51,8 @@
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"typecheck:workspace": "pnpm typecheck && pnpm --filter agent-teams-mcp typecheck && pnpm --filter agent-teams-mcp typecheck:test",
|
"typecheck:workspace": "pnpm typecheck && pnpm --filter agent-teams-mcp typecheck && pnpm --filter agent-teams-mcp typecheck:test",
|
||||||
"lint": "eslint src/ --cache --cache-location .eslintcache --cache-strategy content",
|
"lint": "eslint src/ --cache --cache-location .eslintcache --cache-strategy content",
|
||||||
|
"lint:fast": "eslint --config eslint.fast.config.js --cache --cache-location .eslintcache-fast --cache-strategy content src/",
|
||||||
|
"lint:fast:files": "eslint --config eslint.fast.config.js --cache --cache-location .eslintcache-fast --cache-strategy content",
|
||||||
"lint:mcp": "pnpm --filter agent-teams-mcp lint",
|
"lint:mcp": "pnpm --filter agent-teams-mcp lint",
|
||||||
"lint:fix": "eslint src/ --fix --cache --cache-location .eslintcache --cache-strategy content",
|
"lint:fix": "eslint src/ --fix --cache --cache-location .eslintcache --cache-strategy content",
|
||||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
|
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
{
|
{
|
||||||
"version": "0.0.39",
|
"version": "0.0.40",
|
||||||
"sourceRef": "v0.0.39",
|
"sourceRef": "v0.0.40",
|
||||||
"sourceRepository": "777genius/agent_teams_orchestrator",
|
"sourceRepository": "777genius/agent_teams_orchestrator",
|
||||||
"releaseRepository": "777genius/agent-teams-ai",
|
"releaseRepository": "777genius/agent-teams-ai",
|
||||||
"releaseTag": "v2.0.0",
|
"releaseTag": "v2.0.0",
|
||||||
"assets": {
|
"assets": {
|
||||||
"darwin-arm64": {
|
"darwin-arm64": {
|
||||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.39.tar.gz",
|
"file": "agent-teams-runtime-darwin-arm64-v0.0.40.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"darwin-x64": {
|
"darwin-x64": {
|
||||||
"file": "agent-teams-runtime-darwin-x64-v0.0.39.tar.gz",
|
"file": "agent-teams-runtime-darwin-x64-v0.0.40.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"linux-x64": {
|
"linux-x64": {
|
||||||
"file": "agent-teams-runtime-linux-x64-v0.0.39.tar.gz",
|
"file": "agent-teams-runtime-linux-x64-v0.0.40.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"win32-x64": {
|
"win32-x64": {
|
||||||
"file": "agent-teams-runtime-win32-x64-v0.0.39.zip",
|
"file": "agent-teams-runtime-win32-x64-v0.0.40.zip",
|
||||||
"archiveKind": "zip",
|
"archiveKind": "zip",
|
||||||
"binaryName": "claude-multimodel.exe"
|
"binaryName": "claude-multimodel.exe"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ import type {
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
MemberSpawnStatusesSnapshot,
|
MemberSpawnStatusesSnapshot,
|
||||||
ResolvedTeamMember,
|
ResolvedTeamMember,
|
||||||
|
TeamAgentRuntimeEntry,
|
||||||
TeamProcess,
|
TeamProcess,
|
||||||
TeamProvisioningProgress,
|
TeamProvisioningProgress,
|
||||||
TeamViewSnapshot,
|
TeamViewSnapshot,
|
||||||
|
|
@ -76,6 +77,7 @@ import type { LeadContextUsage } from '@shared/types/team';
|
||||||
export interface TeamGraphData extends TeamViewSnapshot {
|
export interface TeamGraphData extends TeamViewSnapshot {
|
||||||
members: ResolvedTeamMember[];
|
members: ResolvedTeamMember[];
|
||||||
messageFeed: InboxMessage[];
|
messageFeed: InboxMessage[];
|
||||||
|
runtimeEntriesByMember?: Record<string, TeamAgentRuntimeEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toGraphLaunchVisualState(
|
function toGraphLaunchVisualState(
|
||||||
|
|
@ -438,6 +440,7 @@ export class TeamGraphAdapter {
|
||||||
): void {
|
): void {
|
||||||
const percent = leadContext?.contextUsedPercent;
|
const percent = leadContext?.contextUsedPercent;
|
||||||
const leadMember = data.members.find((member) => member.name === leadName);
|
const leadMember = data.members.find((member) => member.name === leadName);
|
||||||
|
const runtimeEntry = data.runtimeEntriesByMember?.[leadName];
|
||||||
const isTeamVisualOnline = data.isAlive || isTeamProvisioning;
|
const isTeamVisualOnline = data.isAlive || isTeamProvisioning;
|
||||||
const activeTool = TeamGraphAdapter.#selectVisibleTool(
|
const activeTool = TeamGraphAdapter.#selectVisibleTool(
|
||||||
activeTools?.[leadName],
|
activeTools?.[leadName],
|
||||||
|
|
@ -454,6 +457,7 @@ export class TeamGraphAdapter {
|
||||||
spawnLivenessSource: undefined,
|
spawnLivenessSource: undefined,
|
||||||
spawnRuntimeAlive: undefined,
|
spawnRuntimeAlive: undefined,
|
||||||
spawnBootstrapStalled: undefined,
|
spawnBootstrapStalled: undefined,
|
||||||
|
runtimeEntry,
|
||||||
runtimeAdvisory: leadMember.runtimeAdvisory,
|
runtimeAdvisory: leadMember.runtimeAdvisory,
|
||||||
isLaunchSettling: false,
|
isLaunchSettling: false,
|
||||||
isTeamAlive: data.isAlive,
|
isTeamAlive: data.isAlive,
|
||||||
|
|
@ -545,6 +549,7 @@ export class TeamGraphAdapter {
|
||||||
const memberId =
|
const memberId =
|
||||||
memberNodeIdByAlias.get(member.name) ?? buildGraphMemberNodeIdForMember(teamName, member);
|
memberNodeIdByAlias.get(member.name) ?? buildGraphMemberNodeIdForMember(teamName, member);
|
||||||
const spawn = spawnStatuses?.[member.name];
|
const spawn = spawnStatuses?.[member.name];
|
||||||
|
const runtimeEntry = data.runtimeEntriesByMember?.[member.name];
|
||||||
const activeTool = TeamGraphAdapter.#selectVisibleTool(
|
const activeTool = TeamGraphAdapter.#selectVisibleTool(
|
||||||
activeTools?.[member.name],
|
activeTools?.[member.name],
|
||||||
finishedVisible?.[member.name]
|
finishedVisible?.[member.name]
|
||||||
|
|
@ -576,6 +581,9 @@ export class TeamGraphAdapter {
|
||||||
spawnAgentToolAccepted: spawn?.agentToolAccepted,
|
spawnAgentToolAccepted: spawn?.agentToolAccepted,
|
||||||
spawnHardFailure: spawn?.hardFailure,
|
spawnHardFailure: spawn?.hardFailure,
|
||||||
spawnLivenessKind: spawn?.livenessKind,
|
spawnLivenessKind: spawn?.livenessKind,
|
||||||
|
spawnFirstSpawnAcceptedAt: spawn?.firstSpawnAcceptedAt,
|
||||||
|
spawnUpdatedAt: spawn?.updatedAt,
|
||||||
|
runtimeEntry,
|
||||||
runtimeAdvisory: member.runtimeAdvisory,
|
runtimeAdvisory: member.runtimeAdvisory,
|
||||||
isLaunchSettling,
|
isLaunchSettling,
|
||||||
isTeamAlive: data.isAlive,
|
isTeamAlive: data.isAlive,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ interface GraphMemberPopoverContext {
|
||||||
| null;
|
| null;
|
||||||
teamMembers: ReturnType<typeof selectResolvedMembersForTeamName>;
|
teamMembers: ReturnType<typeof selectResolvedMembersForTeamName>;
|
||||||
spawnEntry: AppState['memberSpawnStatusesByTeam'][string][string] | undefined;
|
spawnEntry: AppState['memberSpawnStatusesByTeam'][string][string] | undefined;
|
||||||
|
runtimeEntry: AppState['teamAgentRuntimeByTeam'][string]['members'][string] | undefined;
|
||||||
leadActivity: AppState['leadActivityByTeam'][string] | undefined;
|
leadActivity: AppState['leadActivityByTeam'][string] | undefined;
|
||||||
progress: ReturnType<typeof getCurrentProvisioningProgressForTeam> | null;
|
progress: ReturnType<typeof getCurrentProvisioningProgressForTeam> | null;
|
||||||
memberSpawnSnapshot: AppState['memberSpawnSnapshotsByTeam'][string] | undefined;
|
memberSpawnSnapshot: AppState['memberSpawnSnapshotsByTeam'][string] | undefined;
|
||||||
|
|
@ -41,6 +42,9 @@ function selectGraphMemberPopoverContext(
|
||||||
: null,
|
: null,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
spawnEntry: teamName ? state.memberSpawnStatusesByTeam[teamName]?.[memberName] : undefined,
|
spawnEntry: teamName ? state.memberSpawnStatusesByTeam[teamName]?.[memberName] : undefined,
|
||||||
|
runtimeEntry: teamName
|
||||||
|
? state.teamAgentRuntimeByTeam[teamName]?.members[memberName]
|
||||||
|
: undefined,
|
||||||
leadActivity: teamName ? state.leadActivityByTeam[teamName] : undefined,
|
leadActivity: teamName ? state.leadActivityByTeam[teamName] : undefined,
|
||||||
progress: teamName ? getCurrentProvisioningProgressForTeam(state, teamName) : null,
|
progress: teamName ? getCurrentProvisioningProgressForTeam(state, teamName) : null,
|
||||||
memberSpawnSnapshot: teamName ? state.memberSpawnSnapshotsByTeam[teamName] : undefined,
|
memberSpawnSnapshot: teamName ? state.memberSpawnSnapshotsByTeam[teamName] : undefined,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
|
import { useTeamAgentRuntimeWatcher } from '@renderer/components/team/useTeamAgentRuntimeWatcher';
|
||||||
import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage';
|
import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import {
|
import {
|
||||||
|
|
@ -73,6 +74,7 @@ export function useTeamGraphAdapter(
|
||||||
members,
|
members,
|
||||||
messages,
|
messages,
|
||||||
spawnStatuses,
|
spawnStatuses,
|
||||||
|
runtimeSnapshot,
|
||||||
leadActivity,
|
leadActivity,
|
||||||
leadContext,
|
leadContext,
|
||||||
pendingApprovals,
|
pendingApprovals,
|
||||||
|
|
@ -93,6 +95,7 @@ export function useTeamGraphAdapter(
|
||||||
members: isActive ? selectResolvedMembersForTeamName(s, teamName) : EMPTY_MEMBERS,
|
members: isActive ? selectResolvedMembersForTeamName(s, teamName) : EMPTY_MEMBERS,
|
||||||
messages: isActive ? selectTeamMessages(s, teamName) : EMPTY_MESSAGES,
|
messages: isActive ? selectTeamMessages(s, teamName) : EMPTY_MESSAGES,
|
||||||
spawnStatuses: isActive && teamName ? s.memberSpawnStatusesByTeam[teamName] : undefined,
|
spawnStatuses: isActive && teamName ? s.memberSpawnStatusesByTeam[teamName] : undefined,
|
||||||
|
runtimeSnapshot: isActive && teamName ? s.teamAgentRuntimeByTeam[teamName] : undefined,
|
||||||
leadActivity: isActive && teamName ? s.leadActivityByTeam[teamName] : undefined,
|
leadActivity: isActive && teamName ? s.leadActivityByTeam[teamName] : undefined,
|
||||||
leadContext: isActive && teamName ? s.leadContextByTeam[teamName] : undefined,
|
leadContext: isActive && teamName ? s.leadContextByTeam[teamName] : undefined,
|
||||||
pendingApprovals: isActive ? s.pendingApprovals : EMPTY_PENDING_APPROVALS,
|
pendingApprovals: isActive ? s.pendingApprovals : EMPTY_PENDING_APPROVALS,
|
||||||
|
|
@ -113,6 +116,11 @@ export function useTeamGraphAdapter(
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useTeamAgentRuntimeWatcher({
|
||||||
|
teamName,
|
||||||
|
enabled: isActive,
|
||||||
|
});
|
||||||
|
|
||||||
const pendingApprovalAgents = useMemo(() => {
|
const pendingApprovalAgents = useMemo(() => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
return EMPTY_PENDING_APPROVAL_AGENTS;
|
return EMPTY_PENDING_APPROVAL_AGENTS;
|
||||||
|
|
@ -134,8 +142,9 @@ export function useTeamGraphAdapter(
|
||||||
...teamSnapshot,
|
...teamSnapshot,
|
||||||
members,
|
members,
|
||||||
messageFeed: messages,
|
messageFeed: messages,
|
||||||
|
runtimeEntriesByMember: runtimeSnapshot?.members,
|
||||||
};
|
};
|
||||||
}, [members, messages, teamSnapshot]);
|
}, [members, messages, runtimeSnapshot?.members, teamSnapshot]);
|
||||||
|
|
||||||
const commentReadState = useSyncExternalStore(
|
const commentReadState = useSyncExternalStore(
|
||||||
isActive ? subscribe : subscribeNoop,
|
isActive ? subscribe : subscribeNoop,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ const ACTIVITY_SHELL_HEIGHT =
|
||||||
ACTIVITY_LANE.maxVisibleItems * ACTIVITY_LANE.rowHeight +
|
ACTIVITY_LANE.maxVisibleItems * ACTIVITY_LANE.rowHeight +
|
||||||
ACTIVITY_LANE.overflowHeight;
|
ACTIVITY_LANE.overflowHeight;
|
||||||
const NEW_ACTIVITY_HIGHLIGHT_MS = 1_000;
|
const NEW_ACTIVITY_HIGHLIGHT_MS = 1_000;
|
||||||
|
const INTERACTIVE_ACTIVITY_CONTROL_CLASS = 'pointer-events-auto';
|
||||||
|
|
||||||
interface GraphActivityHudProps {
|
interface GraphActivityHudProps {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
|
@ -456,7 +457,7 @@ export const GraphActivityHud = ({
|
||||||
key={entry.graphItem.id}
|
key={entry.graphItem.id}
|
||||||
data-activity-entry-id={entry.graphItem.id}
|
data-activity-entry-id={entry.graphItem.id}
|
||||||
className={[
|
className={[
|
||||||
'h-[72px] min-h-[72px] min-w-0 max-w-full cursor-pointer overflow-hidden rounded-md border transition-[border-color,background-color,box-shadow] duration-500',
|
`${INTERACTIVE_ACTIVITY_CONTROL_CLASS} h-[72px] min-h-[72px] min-w-0 max-w-full cursor-pointer overflow-hidden rounded-md border transition-[border-color,background-color,box-shadow] duration-500`,
|
||||||
isHighlighted
|
isHighlighted
|
||||||
? 'border-sky-300/70 bg-[rgba(14,34,62,0.56)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)]'
|
? 'border-sky-300/70 bg-[rgba(14,34,62,0.56)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)]'
|
||||||
: 'border-transparent',
|
: 'border-transparent',
|
||||||
|
|
@ -536,7 +537,7 @@ export const GraphActivityHud = ({
|
||||||
ref={(element) => {
|
ref={(element) => {
|
||||||
shellRefs.current.set(lane.node.id, element);
|
shellRefs.current.set(lane.node.id, element);
|
||||||
}}
|
}}
|
||||||
className="pointer-events-auto absolute z-10 origin-top-left select-none opacity-0"
|
className="pointer-events-none absolute z-10 origin-top-left select-none opacity-0"
|
||||||
style={{
|
style={{
|
||||||
width: `${laneWidth}px`,
|
width: `${laneWidth}px`,
|
||||||
maxWidth: `${laneWidth}px`,
|
maxWidth: `${laneWidth}px`,
|
||||||
|
|
@ -561,7 +562,7 @@ export const GraphActivityHud = ({
|
||||||
{lane.overflowCount > 0 ? (
|
{lane.overflowCount > 0 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="h-8 min-h-8 w-full rounded-md border border-white/10 bg-[rgba(8,14,28,0.64)] px-3 py-1 text-center text-[11px] font-medium text-slate-300 transition-colors hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]"
|
className={`${INTERACTIVE_ACTIVITY_CONTROL_CLASS} h-8 min-h-8 w-full rounded-md border border-white/10 bg-[rgba(8,14,28,0.64)] px-3 py-1 text-center text-[11px] font-medium text-slate-300 transition-colors hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]`}
|
||||||
onClick={() => handleOpenOwnerActivity(lane.node)}
|
onClick={() => handleOpenOwnerActivity(lane.node)}
|
||||||
>
|
>
|
||||||
+{lane.overflowCount} more
|
+{lane.overflowCount} more
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,7 @@ const MemberPopoverContent = ({
|
||||||
teamData,
|
teamData,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
spawnEntry,
|
spawnEntry,
|
||||||
|
runtimeEntry,
|
||||||
leadActivity,
|
leadActivity,
|
||||||
progress,
|
progress,
|
||||||
memberSpawnSnapshot,
|
memberSpawnSnapshot,
|
||||||
|
|
@ -358,6 +359,9 @@ const MemberPopoverContent = ({
|
||||||
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
spawnAgentToolAccepted: spawnEntry?.agentToolAccepted,
|
||||||
spawnHardFailure: spawnEntry?.hardFailure,
|
spawnHardFailure: spawnEntry?.hardFailure,
|
||||||
spawnLivenessKind: spawnEntry?.livenessKind,
|
spawnLivenessKind: spawnEntry?.livenessKind,
|
||||||
|
spawnFirstSpawnAcceptedAt: spawnEntry?.firstSpawnAcceptedAt,
|
||||||
|
spawnUpdatedAt: spawnEntry?.updatedAt,
|
||||||
|
runtimeEntry,
|
||||||
runtimeAdvisory: member.runtimeAdvisory,
|
runtimeAdvisory: member.runtimeAdvisory,
|
||||||
isLaunchSettling: provisioningPresentation?.hasMembersStillJoining ?? false,
|
isLaunchSettling: provisioningPresentation?.hasMembersStillJoining ?? false,
|
||||||
isTeamAlive: teamData?.isAlive,
|
isTeamAlive: teamData?.isAlive,
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ import { TeamChangesSection } from './TeamChangesSection';
|
||||||
import { TeamProvisioningBanner } from './TeamProvisioningBanner';
|
import { TeamProvisioningBanner } from './TeamProvisioningBanner';
|
||||||
import { loadTeamSessionMetadata } from './teamSessionFetchGuards';
|
import { loadTeamSessionMetadata } from './teamSessionFetchGuards';
|
||||||
import { TeamSessionsSection } from './TeamSessionsSection';
|
import { TeamSessionsSection } from './TeamSessionsSection';
|
||||||
|
import { useTeamAgentRuntimeWatcher } from './useTeamAgentRuntimeWatcher';
|
||||||
|
|
||||||
import type { KanbanFilterState } from './kanban/KanbanFilterPopover';
|
import type { KanbanFilterState } from './kanban/KanbanFilterPopover';
|
||||||
import type { KanbanSortState } from './kanban/KanbanSortPopover';
|
import type { KanbanSortState } from './kanban/KanbanSortPopover';
|
||||||
|
|
@ -915,8 +916,6 @@ const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const TEAM_AGENT_RUNTIME_REFRESH_MS = 5_000;
|
|
||||||
|
|
||||||
const TeamAgentRuntimeWatcher = memo(function TeamAgentRuntimeWatcher({
|
const TeamAgentRuntimeWatcher = memo(function TeamAgentRuntimeWatcher({
|
||||||
teamName,
|
teamName,
|
||||||
isTeamProvisioning,
|
isTeamProvisioning,
|
||||||
|
|
@ -928,37 +927,12 @@ const TeamAgentRuntimeWatcher = memo(function TeamAgentRuntimeWatcher({
|
||||||
isTeamAlive?: boolean;
|
isTeamAlive?: boolean;
|
||||||
isThisTabActive: boolean;
|
isThisTabActive: boolean;
|
||||||
}): null {
|
}): null {
|
||||||
const { leadActivity, fetchTeamAgentRuntime } = useStore(
|
useTeamAgentRuntimeWatcher({
|
||||||
useShallow((s) => ({
|
teamName,
|
||||||
leadActivity: s.leadActivityByTeam[teamName],
|
enabled: isThisTabActive,
|
||||||
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,
|
isTeamAlive,
|
||||||
isTeamProvisioning,
|
isTeamProvisioning,
|
||||||
isThisTabActive,
|
});
|
||||||
leadActivity,
|
|
||||||
teamName,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
59
src/renderer/components/team/useTeamAgentRuntimeWatcher.ts
Normal file
59
src/renderer/components/team/useTeamAgentRuntimeWatcher.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useStore } from '@renderer/store';
|
||||||
|
import { isTeamProvisioningActive, selectTeamDataForName } from '@renderer/store/slices/teamSlice';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
const TEAM_AGENT_RUNTIME_REFRESH_MS = 5_000;
|
||||||
|
|
||||||
|
interface TeamAgentRuntimeWatcherOptions {
|
||||||
|
teamName: string;
|
||||||
|
enabled: boolean;
|
||||||
|
isTeamProvisioning?: boolean;
|
||||||
|
isTeamAlive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTeamAgentRuntimeWatcher({
|
||||||
|
teamName,
|
||||||
|
enabled,
|
||||||
|
isTeamProvisioning,
|
||||||
|
isTeamAlive,
|
||||||
|
}: TeamAgentRuntimeWatcherOptions): void {
|
||||||
|
const { leadActivity, storeIsTeamAlive, storeIsTeamProvisioning, fetchTeamAgentRuntime } =
|
||||||
|
useStore(
|
||||||
|
useShallow((s) => ({
|
||||||
|
leadActivity: s.leadActivityByTeam[teamName],
|
||||||
|
storeIsTeamAlive: selectTeamDataForName(s, teamName)?.isAlive,
|
||||||
|
storeIsTeamProvisioning: isTeamProvisioningActive(s, teamName),
|
||||||
|
fetchTeamAgentRuntime: s.fetchTeamAgentRuntime,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const effectiveIsTeamAlive = isTeamAlive ?? storeIsTeamAlive;
|
||||||
|
const effectiveIsTeamProvisioning = isTeamProvisioning ?? storeIsTeamProvisioning;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
const shouldWatch =
|
||||||
|
effectiveIsTeamProvisioning ||
|
||||||
|
effectiveIsTeamAlive === 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);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
effectiveIsTeamAlive,
|
||||||
|
effectiveIsTeamProvisioning,
|
||||||
|
enabled,
|
||||||
|
fetchTeamAgentRuntime,
|
||||||
|
leadActivity,
|
||||||
|
teamName,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
@ -3070,6 +3070,33 @@ export interface TeamSlice {
|
||||||
|
|
||||||
// --- Per-team launch params persistence ---
|
// --- Per-team launch params persistence ---
|
||||||
const LAUNCH_PARAMS_PREFIX = 'team:launchParams:';
|
const LAUNCH_PARAMS_PREFIX = 'team:launchParams:';
|
||||||
|
const MESSAGES_PANEL_MODE_STORAGE_KEY = 'team:messagesPanelMode';
|
||||||
|
const DEFAULT_MESSAGES_PANEL_MODE: TeamMessagesPanelMode = 'sidebar';
|
||||||
|
const VALID_MESSAGES_PANEL_MODES: ReadonlySet<TeamMessagesPanelMode> = new Set([
|
||||||
|
'sidebar',
|
||||||
|
'inline',
|
||||||
|
'bottom-sheet',
|
||||||
|
'floating-composer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function loadPersistedMessagesPanelMode(): TeamMessagesPanelMode {
|
||||||
|
try {
|
||||||
|
const persisted = localStorage.getItem(MESSAGES_PANEL_MODE_STORAGE_KEY);
|
||||||
|
return VALID_MESSAGES_PANEL_MODES.has(persisted as TeamMessagesPanelMode)
|
||||||
|
? (persisted as TeamMessagesPanelMode)
|
||||||
|
: DEFAULT_MESSAGES_PANEL_MODE;
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_MESSAGES_PANEL_MODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePersistedMessagesPanelMode(mode: TeamMessagesPanelMode): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(MESSAGES_PANEL_MODE_STORAGE_KEY, mode);
|
||||||
|
} catch {
|
||||||
|
// ignore - best-effort UI preference persistence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentProvisioningProgressForTeam(
|
export function getCurrentProvisioningProgressForTeam(
|
||||||
state: Pick<TeamSlice, 'currentProvisioningRunIdByTeam' | 'provisioningRuns'>,
|
state: Pick<TeamSlice, 'currentProvisioningRunIdByTeam' | 'provisioningRuns'>,
|
||||||
|
|
@ -3440,10 +3467,13 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
toolApprovalSettings: loadToolApprovalSettings(),
|
toolApprovalSettings: loadToolApprovalSettings(),
|
||||||
|
|
||||||
// Messages panel UI state
|
// Messages panel UI state
|
||||||
messagesPanelMode: 'sidebar' as const,
|
messagesPanelMode: loadPersistedMessagesPanelMode(),
|
||||||
messagesPanelWidth: 340,
|
messagesPanelWidth: 340,
|
||||||
sidebarLogsHeight: 213,
|
sidebarLogsHeight: 213,
|
||||||
setMessagesPanelMode: (mode: TeamMessagesPanelMode) => set({ messagesPanelMode: mode }),
|
setMessagesPanelMode: (mode: TeamMessagesPanelMode) => {
|
||||||
|
savePersistedMessagesPanelMode(mode);
|
||||||
|
set({ messagesPanelMode: mode });
|
||||||
|
},
|
||||||
setMessagesPanelWidth: (width: number) => set({ messagesPanelWidth: width }),
|
setMessagesPanelWidth: (width: number) => set({ messagesPanelWidth: width }),
|
||||||
setSidebarLogsHeight: (height: number) => set({ sidebarLogsHeight: height }),
|
setSidebarLogsHeight: (height: number) => set({ sidebarLogsHeight: height }),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,13 @@ describe('GraphActivityHud', () => {
|
||||||
button.textContent?.includes('+1 more')
|
button.textContent?.includes('+1 more')
|
||||||
);
|
);
|
||||||
expect(moreButton).not.toBeUndefined();
|
expect(moreButton).not.toBeUndefined();
|
||||||
|
expect(moreButton?.className).toContain('pointer-events-auto');
|
||||||
|
|
||||||
|
const shell = host.querySelector('.z-10');
|
||||||
|
expect(shell?.className).toContain('pointer-events-none');
|
||||||
|
expect(host.querySelector('[data-activity-entry-id="item-1"]')?.className).toContain(
|
||||||
|
'pointer-events-auto'
|
||||||
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
moreButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
moreButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TeamGraphAdapter,
|
TeamGraphAdapter,
|
||||||
type TeamGraphData,
|
type TeamGraphData,
|
||||||
} from '@features/agent-graph/renderer/adapters/TeamGraphAdapter';
|
} from '@features/agent-graph/renderer/adapters/TeamGraphAdapter';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { InboxMessage, TeamTaskWithKanban } from '@shared/types/team';
|
|
||||||
import type { GraphDataPort } from '@claude-teams/agent-graph';
|
import type { GraphDataPort } from '@claude-teams/agent-graph';
|
||||||
|
import type {
|
||||||
|
InboxMessage,
|
||||||
|
MemberSpawnStatusEntry,
|
||||||
|
TeamAgentRuntimeEntry,
|
||||||
|
TeamTaskWithKanban,
|
||||||
|
} from '@shared/types/team';
|
||||||
|
|
||||||
function createBaseTeamData(
|
function createBaseTeamData(
|
||||||
overrides?: Partial<TeamGraphData> & {
|
overrides?: Partial<TeamGraphData> & {
|
||||||
|
|
@ -62,6 +66,23 @@ function findNode(graph: GraphDataPort, nodeId: string) {
|
||||||
return graph.nodes.find((node) => node.id === nodeId);
|
return graph.nodes.find((node) => node.id === nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createLiveRuntimeEntry(
|
||||||
|
memberName: string,
|
||||||
|
overrides: Partial<TeamAgentRuntimeEntry> = {}
|
||||||
|
): TeamAgentRuntimeEntry {
|
||||||
|
return {
|
||||||
|
memberName,
|
||||||
|
alive: true,
|
||||||
|
restartable: true,
|
||||||
|
providerId: 'codex',
|
||||||
|
providerBackendId: 'codex-native',
|
||||||
|
livenessKind: 'runtime_process',
|
||||||
|
pid: 12345,
|
||||||
|
updatedAt: '2026-03-28T19:00:00.000Z',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function adaptWithActiveTaskLogActivity(
|
function adaptWithActiveTaskLogActivity(
|
||||||
adapter: TeamGraphAdapter,
|
adapter: TeamGraphAdapter,
|
||||||
teamData: TeamGraphData,
|
teamData: TeamGraphData,
|
||||||
|
|
@ -185,6 +206,62 @@ describe('TeamGraphAdapter particles', () => {
|
||||||
expect(graph.layout?.mode).toBe('grid-under-lead');
|
expect(graph.layout?.mode).toBe('grid-under-lead');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses runtime entries when deriving member launch status', () => {
|
||||||
|
const adapter = TeamGraphAdapter.create();
|
||||||
|
const spawnStatuses: Record<string, MemberSpawnStatusEntry> = {
|
||||||
|
alice: {
|
||||||
|
status: 'online',
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
livenessKind: 'runtime_process',
|
||||||
|
updatedAt: '2026-03-28T19:00:00.000Z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const teamData = createBaseTeamData({
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
name: 'team-lead',
|
||||||
|
status: 'active',
|
||||||
|
currentTaskId: null,
|
||||||
|
taskCount: 0,
|
||||||
|
lastActiveAt: null,
|
||||||
|
messageCount: 0,
|
||||||
|
agentType: 'team-lead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'alice',
|
||||||
|
status: 'active',
|
||||||
|
currentTaskId: null,
|
||||||
|
taskCount: 1,
|
||||||
|
lastActiveAt: null,
|
||||||
|
messageCount: 0,
|
||||||
|
providerId: 'codex',
|
||||||
|
providerBackendId: 'codex-native',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const withoutRuntime = adapter.adapt(teamData, 'my-team', spawnStatuses);
|
||||||
|
expect(findNode(withoutRuntime, 'member:my-team:alice')?.launchStatusLabel).toBe(
|
||||||
|
'stale runtime'
|
||||||
|
);
|
||||||
|
|
||||||
|
const withRuntime = adapter.adapt(
|
||||||
|
{
|
||||||
|
...teamData,
|
||||||
|
runtimeEntriesByMember: {
|
||||||
|
alice: createLiveRuntimeEntry('alice'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'my-team',
|
||||||
|
spawnStatuses
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(findNode(withRuntime, 'member:my-team:alice')?.launchStatusLabel).toBeUndefined();
|
||||||
|
expect(findNode(withRuntime, 'member:my-team:alice')?.launchVisualState).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('applies saved grid owner order only in grid-under-lead mode', () => {
|
it('applies saved grid owner order only in grid-under-lead mode', () => {
|
||||||
const adapter = TeamGraphAdapter.create();
|
const adapter = TeamGraphAdapter.create();
|
||||||
const teamData = createBaseTeamData({
|
const teamData = createBaseTeamData({
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import {
|
||||||
getActiveTeamPendingReplyWaits,
|
getActiveTeamPendingReplyWaits,
|
||||||
hasActiveTeamPendingReplyWait,
|
hasActiveTeamPendingReplyWait,
|
||||||
getCurrentProvisioningProgressForTeam,
|
getCurrentProvisioningProgressForTeam,
|
||||||
|
loadPersistedMessagesPanelMode,
|
||||||
|
savePersistedMessagesPanelMode,
|
||||||
selectMemberMessagesForTeamMember,
|
selectMemberMessagesForTeamMember,
|
||||||
selectResolvedMemberForTeamName,
|
selectResolvedMemberForTeamName,
|
||||||
selectResolvedMembersForTeamName,
|
selectResolvedMembersForTeamName,
|
||||||
|
|
@ -344,6 +346,7 @@ describe('teamSlice actions', () => {
|
||||||
});
|
});
|
||||||
hoisted.restartMember.mockResolvedValue(undefined);
|
hoisted.restartMember.mockResolvedValue(undefined);
|
||||||
hoisted.skipMemberForLaunch.mockResolvedValue(undefined);
|
hoisted.skipMemberForLaunch.mockResolvedValue(undefined);
|
||||||
|
window.localStorage.removeItem('team:messagesPanelMode');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
@ -351,6 +354,31 @@ describe('teamSlice actions', () => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('restores the selected messages panel mode from localStorage', () => {
|
||||||
|
window.localStorage.setItem('team:messagesPanelMode', 'bottom-sheet');
|
||||||
|
|
||||||
|
const store = createSliceStore();
|
||||||
|
|
||||||
|
expect(store.getState().messagesPanelMode).toBe('bottom-sheet');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists messages panel mode changes and ignores invalid stored values', () => {
|
||||||
|
const store = createSliceStore();
|
||||||
|
|
||||||
|
store.getState().setMessagesPanelMode('floating-composer');
|
||||||
|
|
||||||
|
expect(window.localStorage.getItem('team:messagesPanelMode')).toBe('floating-composer');
|
||||||
|
expect(loadPersistedMessagesPanelMode()).toBe('floating-composer');
|
||||||
|
|
||||||
|
window.localStorage.setItem('team:messagesPanelMode', 'bad-mode');
|
||||||
|
|
||||||
|
expect(loadPersistedMessagesPanelMode()).toBe('sidebar');
|
||||||
|
|
||||||
|
savePersistedMessagesPanelMode('inline');
|
||||||
|
|
||||||
|
expect(window.localStorage.getItem('team:messagesPanelMode')).toBe('inline');
|
||||||
|
});
|
||||||
|
|
||||||
it('records terminal provisioning fanout diagnostics without changing visible graph hydrate behavior', () => {
|
it('records terminal provisioning fanout diagnostics without changing visible graph hydrate behavior', () => {
|
||||||
const store = createSliceStore();
|
const store = createSliceStore();
|
||||||
const fetchTeams = vi.fn(async () => undefined);
|
const fetchTeams = vi.fn(async () => undefined);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue