perf(team): gate hidden team screen effects

This commit is contained in:
777genius 2026-05-05 15:41:47 +03:00
parent 49901b2553
commit 364c5c6471
2 changed files with 36 additions and 19 deletions

View file

@ -52,7 +52,11 @@ export const PaneContent = ({ pane, isPaneFocused }: PaneContentProps): React.JS
{tab.type === 'teams' && <TeamListView />}
{tab.type === 'team' && (
<TabUIProvider tabId={tab.id}>
<TeamDetailView teamName={tab.teamName ?? ''} isPaneFocused={isPaneFocused} />
<TeamDetailView
teamName={tab.teamName ?? ''}
isActive={isActive}
isPaneFocused={isPaneFocused}
/>
</TabUIProvider>
)}
{tab.type === 'session' && (

View file

@ -157,6 +157,7 @@ import type { ContextUsageLike } from '@shared/utils/contextMetrics';
interface TeamDetailViewProps {
teamName: string;
isActive?: boolean;
isPaneFocused?: boolean;
}
@ -377,10 +378,12 @@ const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({
teamName,
isTeamProvisioning,
isTeamAlive,
isThisTabActive,
}: {
teamName: string;
isTeamProvisioning: boolean;
isTeamAlive?: boolean;
isThisTabActive: boolean;
}): null {
const { leadActivity, memberSpawnStatuses, memberSpawnSnapshot, fetchMemberSpawnStatuses } =
useStore(
@ -393,6 +396,8 @@ const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({
);
useEffect(() => {
if (!isThisTabActive) return;
const hasUnresolvedSpawn = hasUnresolvedMemberSpawnStatus(
memberSpawnStatuses,
memberSpawnSnapshot
@ -420,6 +425,7 @@ const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({
fetchMemberSpawnStatuses,
isTeamAlive,
isTeamProvisioning,
isThisTabActive,
leadActivity,
memberSpawnSnapshot,
memberSpawnStatuses,
@ -888,6 +894,7 @@ const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge(
export const TeamDetailView = memo(function TeamDetailView({
teamName,
isActive = true,
isPaneFocused = false,
}: TeamDetailViewProps): React.JSX.Element {
const { isLight } = useTheme();
@ -1279,8 +1286,7 @@ export const TeamDetailView = memo(function TeamDetailView({
);
const tabId = useTabIdOptional();
const activeTabId = useStore((s) => s.activeTabId);
const isThisTabActive = tabId ? activeTabId === tabId : false;
const isThisTabActive = isActive;
const wasInteractiveRef = useRef(false);
// Messages panel resize
@ -1324,33 +1330,32 @@ export const TeamDetailView = memo(function TeamDetailView({
useEffect(() => {
const wasProvisioning = wasProvisioningRef.current;
wasProvisioningRef.current = isTeamProvisioning;
if (!isThisTabActive) return;
if (!wasProvisioning && isTeamProvisioning) {
provisioningBannerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, [isTeamProvisioning]);
}, [isTeamProvisioning, isThisTabActive]);
const [kanbanSearch, setKanbanSearch] = useState('');
// Open editor overlay when a file reveal is requested (e.g. from chip click)
const pendingRevealFile = useStore((s) => s.editorPendingRevealFile);
useEffect(() => {
if (!isThisTabActive) return;
if (pendingRevealFile && data?.config.projectPath) {
setEditorOpen(true);
}
}, [pendingRevealFile, data?.config.projectPath]);
}, [isThisTabActive, pendingRevealFile, data?.config.projectPath]);
useEffect(() => {
if (!teamName) {
if (!isThisTabActive || !teamName) {
return;
}
void selectTeam(teamName);
void fetchDeletedTasks(teamName);
}, [teamName, selectTeam, fetchDeletedTasks]);
}, [isThisTabActive, teamName, selectTeam, fetchDeletedTasks]);
// Recovery: after HMR, all mounted TeamDetailView effects re-run simultaneously.
// With CSS display-toggle (all tabs stay mounted), the last selectTeam() call wins
// and other tabs get stuck with mismatched data (permanent skeleton).
// Re-trigger selectTeam when this tab becomes active and store data is stale.
// Re-trigger selectTeam when this visible tab becomes active and store data is stale.
const storedTeamName = data?.teamName;
useEffect(() => {
if (!isThisTabActive || !teamName || loading) return;
@ -1387,7 +1392,7 @@ export const TeamDetailView = memo(function TeamDetailView({
// Fetch active teams when launch dialog opens (for conflict warning)
useEffect(() => {
if (!launchDialogOpen) return;
if (!isThisTabActive || !launchDialogOpen) return;
let cancelled = false;
const teamsSnapshot = useStore.getState().teams;
void (async () => {
@ -1410,7 +1415,7 @@ export const TeamDetailView = memo(function TeamDetailView({
return () => {
cancelled = true;
};
}, [launchDialogOpen]);
}, [isThisTabActive, launchDialogOpen]);
useEffect(() => {
if (kanbanFilterQuery) {
@ -1456,7 +1461,7 @@ export const TeamDetailView = memo(function TeamDetailView({
]);
useEffect(() => {
if (!projectId) return;
if (!isThisTabActive || !projectId) return;
let cancelled = false;
setSessionsLoading(true);
@ -1485,7 +1490,7 @@ export const TeamDetailView = memo(function TeamDetailView({
return () => {
cancelled = true;
};
}, [data?.config.leadSessionId, projectId, sessionHistoryKey]);
}, [data?.config.leadSessionId, isThisTabActive, projectId, sessionHistoryKey]);
// Live git branch tracking for the lead project and member worktrees
const teamProjectPath = data?.config.projectPath?.trim() ?? null;
@ -1510,7 +1515,11 @@ export const TeamDetailView = memo(function TeamDetailView({
return Array.from(uniquePaths.values());
}, [members, leadProjectPath]);
useBranchSync(branchSyncPaths, { live: true });
const activeBranchSyncPaths = useMemo(
() => (isThisTabActive ? branchSyncPaths : []),
[branchSyncPaths, isThisTabActive]
);
useBranchSync(activeBranchSyncPaths, { live: isThisTabActive });
const trackedBranches = useStore(
useShallow((s) =>
Object.fromEntries(
@ -1830,6 +1839,7 @@ export const TeamDetailView = memo(function TeamDetailView({
// Pick up pending review request from GlobalTaskDetailDialog
useEffect(() => {
if (!isThisTabActive) return;
if (!pendingReviewRequest) return;
setReviewDialogState({
open: true,
@ -1842,11 +1852,12 @@ export const TeamDetailView = memo(function TeamDetailView({
selectReviewFile(pendingReviewRequest.filePath);
}
setPendingReviewRequest(null);
}, [pendingReviewRequest, selectReviewFile, setPendingReviewRequest]);
}, [isThisTabActive, pendingReviewRequest, selectReviewFile, setPendingReviewRequest]);
const pendingTeamSectionFocus = useStore((s) => s.pendingTeamSectionFocus);
const clearTeamSectionFocus = useStore((s) => s.clearTeamSectionFocus);
useEffect(() => {
if (!isThisTabActive) return;
if (pendingTeamSectionFocus?.teamName !== teamName) return;
const sectionId =
@ -1868,11 +1879,12 @@ export const TeamDetailView = memo(function TeamDetailView({
if (!section) return;
section.dispatchEvent(new CustomEvent('team-section-navigate'));
clearTeamSectionFocus();
}, [pendingTeamSectionFocus, clearTeamSectionFocus, teamName, data]);
}, [pendingTeamSectionFocus, clearTeamSectionFocus, isThisTabActive, teamName, data]);
// Pick up pending member profile request from MemberHoverCard
const pendingMemberProfile = useStore((s) => s.pendingMemberProfile);
useEffect(() => {
if (!isThisTabActive) return;
if (!pendingMemberProfile || !data) return;
if (pendingMemberProfile.teamName && pendingMemberProfile.teamName !== teamName) return;
@ -1889,7 +1901,7 @@ export const TeamDetailView = memo(function TeamDetailView({
});
}
useStore.getState().closeMemberProfile();
}, [pendingMemberProfile, membersWithLiveBranches, teamName, data]);
}, [isThisTabActive, pendingMemberProfile, membersWithLiveBranches, teamName, data]);
const handleDeleteTask = useCallback(
(taskId: string) => {
@ -2058,6 +2070,7 @@ export const TeamDetailView = memo(function TeamDetailView({
teamName={teamName}
isTeamProvisioning={isTeamProvisioning}
isTeamAlive={data?.isAlive}
isThisTabActive={isThisTabActive}
/>
);
const teamAgentRuntimeWatcher = (