From 9dd1572763aea2d985603eea1e8cf33e10dc414d Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 19 May 2026 11:17:17 +0300 Subject: [PATCH] fix(graph): refresh runtime state in graph views --- .gitignore | 1 + AGENTS.md | 7 + eslint.fast.config.js | 256 ++++++++++++++++++ package.json | 2 + runtime.lock.json | 12 +- .../renderer/adapters/TeamGraphAdapter.ts | 8 + .../hooks/useGraphMemberPopoverContext.ts | 4 + .../renderer/hooks/useTeamGraphAdapter.ts | 11 +- .../renderer/ui/GraphActivityHud.tsx | 7 +- .../renderer/ui/GraphNodePopover.tsx | 4 + .../components/team/TeamDetailView.tsx | 36 +-- .../team/useTeamAgentRuntimeWatcher.ts | 59 ++++ src/renderer/store/slices/teamSlice.ts | 34 ++- .../agent-graph/GraphActivityHud.test.ts | 7 + .../agent-graph/TeamGraphAdapter.test.ts | 83 +++++- test/renderer/store/teamSlice.test.ts | 28 ++ 16 files changed, 513 insertions(+), 46 deletions(-) create mode 100644 eslint.fast.config.js create mode 100644 src/renderer/components/team/useTeamAgentRuntimeWatcher.ts diff --git a/.gitignore b/.gitignore index 480a728f..87a82200 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ temp/ eslint-fix/ .eslintcache +.eslintcache-fast remotion/* .home/ diff --git a/AGENTS.md b/AGENTS.md index 836b2b5b..6667975b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. - 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 -- ` 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: - Default home for medium and large features: `src/features//` diff --git a/eslint.fast.config.js b/eslint.fast.config.js new file mode 100644 index 00000000..29c3c9b0 --- /dev/null +++ b/eslint.fast.config.js @@ -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.' }, + ], + }, + ], + }, + }, +]); diff --git a/package.json b/package.json index dbc5330d..4f123949 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,8 @@ "typecheck": "tsc --noEmit", "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: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:fix": "eslint src/ --fix --cache --cache-location .eslintcache --cache-strategy content", "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"", diff --git a/runtime.lock.json b/runtime.lock.json index 492a0d16..9a433722 100644 --- a/runtime.lock.json +++ b/runtime.lock.json @@ -1,27 +1,27 @@ { - "version": "0.0.39", - "sourceRef": "v0.0.39", + "version": "0.0.40", + "sourceRef": "v0.0.40", "sourceRepository": "777genius/agent_teams_orchestrator", "releaseRepository": "777genius/agent-teams-ai", "releaseTag": "v2.0.0", "assets": { "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", "binaryName": "claude-multimodel" }, "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", "binaryName": "claude-multimodel" }, "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", "binaryName": "claude-multimodel" }, "win32-x64": { - "file": "agent-teams-runtime-win32-x64-v0.0.39.zip", + "file": "agent-teams-runtime-win32-x64-v0.0.40.zip", "archiveKind": "zip", "binaryName": "claude-multimodel.exe" } diff --git a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts index fb49907b..68de7115 100644 --- a/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts +++ b/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts @@ -67,6 +67,7 @@ import type { MemberSpawnStatusEntry, MemberSpawnStatusesSnapshot, ResolvedTeamMember, + TeamAgentRuntimeEntry, TeamProcess, TeamProvisioningProgress, TeamViewSnapshot, @@ -76,6 +77,7 @@ import type { LeadContextUsage } from '@shared/types/team'; export interface TeamGraphData extends TeamViewSnapshot { members: ResolvedTeamMember[]; messageFeed: InboxMessage[]; + runtimeEntriesByMember?: Record; } function toGraphLaunchVisualState( @@ -438,6 +440,7 @@ export class TeamGraphAdapter { ): void { const percent = leadContext?.contextUsedPercent; const leadMember = data.members.find((member) => member.name === leadName); + const runtimeEntry = data.runtimeEntriesByMember?.[leadName]; const isTeamVisualOnline = data.isAlive || isTeamProvisioning; const activeTool = TeamGraphAdapter.#selectVisibleTool( activeTools?.[leadName], @@ -454,6 +457,7 @@ export class TeamGraphAdapter { spawnLivenessSource: undefined, spawnRuntimeAlive: undefined, spawnBootstrapStalled: undefined, + runtimeEntry, runtimeAdvisory: leadMember.runtimeAdvisory, isLaunchSettling: false, isTeamAlive: data.isAlive, @@ -545,6 +549,7 @@ export class TeamGraphAdapter { const memberId = memberNodeIdByAlias.get(member.name) ?? buildGraphMemberNodeIdForMember(teamName, member); const spawn = spawnStatuses?.[member.name]; + const runtimeEntry = data.runtimeEntriesByMember?.[member.name]; const activeTool = TeamGraphAdapter.#selectVisibleTool( activeTools?.[member.name], finishedVisible?.[member.name] @@ -576,6 +581,9 @@ export class TeamGraphAdapter { spawnAgentToolAccepted: spawn?.agentToolAccepted, spawnHardFailure: spawn?.hardFailure, spawnLivenessKind: spawn?.livenessKind, + spawnFirstSpawnAcceptedAt: spawn?.firstSpawnAcceptedAt, + spawnUpdatedAt: spawn?.updatedAt, + runtimeEntry, runtimeAdvisory: member.runtimeAdvisory, isLaunchSettling, isTeamAlive: data.isAlive, diff --git a/src/features/agent-graph/renderer/hooks/useGraphMemberPopoverContext.ts b/src/features/agent-graph/renderer/hooks/useGraphMemberPopoverContext.ts index 11ea2c25..692ab93f 100644 --- a/src/features/agent-graph/renderer/hooks/useGraphMemberPopoverContext.ts +++ b/src/features/agent-graph/renderer/hooks/useGraphMemberPopoverContext.ts @@ -17,6 +17,7 @@ interface GraphMemberPopoverContext { | null; teamMembers: ReturnType; spawnEntry: AppState['memberSpawnStatusesByTeam'][string][string] | undefined; + runtimeEntry: AppState['teamAgentRuntimeByTeam'][string]['members'][string] | undefined; leadActivity: AppState['leadActivityByTeam'][string] | undefined; progress: ReturnType | null; memberSpawnSnapshot: AppState['memberSpawnSnapshotsByTeam'][string] | undefined; @@ -41,6 +42,9 @@ function selectGraphMemberPopoverContext( : null, teamMembers, spawnEntry: teamName ? state.memberSpawnStatusesByTeam[teamName]?.[memberName] : undefined, + runtimeEntry: teamName + ? state.teamAgentRuntimeByTeam[teamName]?.members[memberName] + : undefined, leadActivity: teamName ? state.leadActivityByTeam[teamName] : undefined, progress: teamName ? getCurrentProvisioningProgressForTeam(state, teamName) : null, memberSpawnSnapshot: teamName ? state.memberSpawnSnapshotsByTeam[teamName] : undefined, diff --git a/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts b/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts index 0d722411..5299417f 100644 --- a/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts +++ b/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts @@ -5,6 +5,7 @@ import { useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react'; +import { useTeamAgentRuntimeWatcher } from '@renderer/components/team/useTeamAgentRuntimeWatcher'; import { getSnapshot, subscribe } from '@renderer/services/commentReadStorage'; import { useStore } from '@renderer/store'; import { @@ -73,6 +74,7 @@ export function useTeamGraphAdapter( members, messages, spawnStatuses, + runtimeSnapshot, leadActivity, leadContext, pendingApprovals, @@ -93,6 +95,7 @@ export function useTeamGraphAdapter( members: isActive ? selectResolvedMembersForTeamName(s, teamName) : EMPTY_MEMBERS, messages: isActive ? selectTeamMessages(s, teamName) : EMPTY_MESSAGES, spawnStatuses: isActive && teamName ? s.memberSpawnStatusesByTeam[teamName] : undefined, + runtimeSnapshot: isActive && teamName ? s.teamAgentRuntimeByTeam[teamName] : undefined, leadActivity: isActive && teamName ? s.leadActivityByTeam[teamName] : undefined, leadContext: isActive && teamName ? s.leadContextByTeam[teamName] : undefined, pendingApprovals: isActive ? s.pendingApprovals : EMPTY_PENDING_APPROVALS, @@ -113,6 +116,11 @@ export function useTeamGraphAdapter( })) ); + useTeamAgentRuntimeWatcher({ + teamName, + enabled: isActive, + }); + const pendingApprovalAgents = useMemo(() => { if (!isActive) { return EMPTY_PENDING_APPROVAL_AGENTS; @@ -134,8 +142,9 @@ export function useTeamGraphAdapter( ...teamSnapshot, members, messageFeed: messages, + runtimeEntriesByMember: runtimeSnapshot?.members, }; - }, [members, messages, teamSnapshot]); + }, [members, messages, runtimeSnapshot?.members, teamSnapshot]); const commentReadState = useSyncExternalStore( isActive ? subscribe : subscribeNoop, diff --git a/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx b/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx index 21863e37..06a29b1f 100644 --- a/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx +++ b/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx @@ -29,6 +29,7 @@ const ACTIVITY_SHELL_HEIGHT = ACTIVITY_LANE.maxVisibleItems * ACTIVITY_LANE.rowHeight + ACTIVITY_LANE.overflowHeight; const NEW_ACTIVITY_HIGHLIGHT_MS = 1_000; +const INTERACTIVE_ACTIVITY_CONTROL_CLASS = 'pointer-events-auto'; interface GraphActivityHudProps { teamName: string; @@ -456,7 +457,7 @@ export const GraphActivityHud = ({ key={entry.graphItem.id} data-activity-entry-id={entry.graphItem.id} 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 ? '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', @@ -536,7 +537,7 @@ export const GraphActivityHud = ({ ref={(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={{ width: `${laneWidth}px`, maxWidth: `${laneWidth}px`, @@ -561,7 +562,7 @@ export const GraphActivityHud = ({ {lane.overflowCount > 0 ? (