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 === 'teams' && <TeamListView />}
{tab.type === 'team' && ( {tab.type === 'team' && (
<TabUIProvider tabId={tab.id}> <TabUIProvider tabId={tab.id}>
<TeamDetailView teamName={tab.teamName ?? ''} isPaneFocused={isPaneFocused} /> <TeamDetailView
teamName={tab.teamName ?? ''}
isActive={isActive}
isPaneFocused={isPaneFocused}
/>
</TabUIProvider> </TabUIProvider>
)} )}
{tab.type === 'session' && ( {tab.type === 'session' && (

View file

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