diff --git a/src/renderer/components/layout/PaneContent.tsx b/src/renderer/components/layout/PaneContent.tsx
index e65e0a9e..b4d96187 100644
--- a/src/renderer/components/layout/PaneContent.tsx
+++ b/src/renderer/components/layout/PaneContent.tsx
@@ -52,7 +52,11 @@ export const PaneContent = ({ pane, isPaneFocused }: PaneContentProps): React.JS
{tab.type === 'teams' && }
{tab.type === 'team' && (
-
+
)}
{tab.type === 'session' && (
diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx
index 2c04dd15..4804eb03 100644
--- a/src/renderer/components/team/TeamDetailView.tsx
+++ b/src/renderer/components/team/TeamDetailView.tsx
@@ -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 = (