import {
  lazy,
  memo,
  Suspense,
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from 'react';

import { api } from '@renderer/api';
import { SessionContextPanel } from '@renderer/components/chat/SessionContextPanel/index';
import { confirm } from '@renderer/components/common/ConfirmDialog';
import { Button } from '@renderer/components/ui/button';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@renderer/components/ui/dialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTabIdOptional } from '@renderer/contexts/useTabUIContext';
import { useBranchSync } from '@renderer/hooks/useBranchSync';
import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils';
import { useStore } from '@renderer/store';
import {
  getCurrentProvisioningProgressForTeam,
  isTeamProvisioningActive,
  selectResolvedMemberForTeamName,
  selectResolvedMembersForTeamName,
  selectTeamMemberSnapshotsForName,
} from '@renderer/store/slices/teamSlice';
import { createChipFromSelection } from '@renderer/utils/chipUtils';
import { sumContextInjectionTokens } from '@renderer/utils/contextMath';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import {
  hasUnresolvedMemberSpawnStatus,
  MEMBER_SPAWN_STATUS_REFRESH_MS,
} from '@renderer/utils/memberSpawnStatusPolling';
import { formatProjectPath } from '@renderer/utils/pathDisplay';
import { buildTaskCountsByOwner, normalizePath } from '@renderer/utils/pathNormalize';
import { nameColorSet } from '@renderer/utils/projectColor';
import { resolveProjectIdByPath } from '@renderer/utils/projectLookup';
import { getCronDescription } from '@renderer/utils/scheduleFormatters';
import {
  buildTaskChangeRequestOptions,
  type TaskChangeRequestOptions,
} from '@renderer/utils/taskChangeRequest';

import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import { deriveContextMetrics } from '@shared/utils/contextMetrics';
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
  AlertTriangle,
  Columns3,
  FolderOpen,
  GitBranch,
  History,
  Pencil,
  Play,
  Plus,
  Terminal,
  Trash2,
  Users,
} from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';

import { CreateTaskDialog } from './dialogs/CreateTaskDialog';
import { EditTeamDialog } from './dialogs/EditTeamDialog';
import { LaunchTeamDialog, type TeamLaunchDialogMode } from './dialogs/LaunchTeamDialog';
import { ReviewDialog } from './dialogs/ReviewDialog';
import { SendMessageDialog } from './dialogs/SendMessageDialog';
import { TaskDetailDialog } from './dialogs/TaskDetailDialog';
import { executeTeamRelaunch } from './dialogs/teamRelaunchFlow';
import { KanbanBoard } from './kanban/KanbanBoard';
import { UNASSIGNED_OWNER } from './kanban/KanbanFilterPopover';
import { KanbanSearchInput } from './kanban/KanbanSearchInput';
import { TrashDialog } from './kanban/TrashDialog';
import { MemberDetailDialog } from './members/MemberDetailDialog';
import { type MemberActivityFilter, type MemberDetailTab } from './members/memberDetailTypes';

import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode';
import type { ComponentProps } from 'react';

const ProjectEditorOverlay = lazy(() =>
  import('./editor/ProjectEditorOverlay').then((m) => ({ default: m.ProjectEditorOverlay }))
);
import { MemberList } from './members/MemberList';
import { MessagesPanel } from './messages/MessagesPanel';
import { ChangeReviewDialog } from './review/ChangeReviewDialog';
import {
  getTeamPendingRepliesState,
  setTeamPendingRepliesState,
} from './sidebar/teamSidebarUiState';
import { CollapsibleTeamSection } from './CollapsibleTeamSection';
import { ProcessesSection } from './ProcessesSection';
import { getLaunchJoinMilestonesFromMembers, getLaunchJoinState } from './provisioningSteps';
import { TeamProvisioningBanner } from './TeamProvisioningBanner';
import {
  isLeadSessionMissing,
  shouldSuppressMissingLeadSessionFetch,
} from './teamSessionFetchGuards';

import type { KanbanFilterState } from './kanban/KanbanFilterPopover';
import type { KanbanSortState } from './kanban/KanbanSortPopover';
import type { ContextInjection } from '@renderer/types/contextInjection';
import type { Session } from '@renderer/types/data';
import type { InlineChip } from '@renderer/types/inlineChip';
import type {
  EffortLevel,
  GlobalProvider,
  MemberSpawnStatusEntry,
  ResolvedTeamMember,
  TaskRef,
  TeamAgentRuntimeEntry,
  TeamCreateRequest,
  TeamFastMode,
  TeamLaunchRequest,
  TeamProviderId,
  Schedule,
  ScheduleRunStatus,
  TeamTaskWithKanban,
  TeamViewSnapshot,
} from '@shared/types';
import type { EditorSelectionAction } from '@shared/types/editor';
import type { ContextUsageLike } from '@shared/utils/contextMetrics';

interface TeamDetailViewProps {
  teamName: string;
  isPaneFocused?: boolean;
}

interface CreateTaskDialogState {
  open: boolean;
  defaultSubject: string;
  defaultDescription: string;
  defaultOwner: string;
  defaultStartImmediately?: boolean;
  defaultChip?: InlineChip;
}

const TEAM_PENDING_REPLY_REFRESH_DELAY_MS = 10_000;
const SCHEDULE_TASK_ID_PREFIX = 'schedule:';
const SCHEDULE_IN_PROGRESS_STATUSES = new Set<ScheduleRunStatus>([
  'pending',
  'warming_up',
  'warm',
  'running',
]);

function isScheduleBoardTaskId(taskId: string): boolean {
  return taskId.startsWith(SCHEDULE_TASK_ID_PREFIX);
}

function getScheduleIdFromBoardTaskId(taskId: string): string | null {
  return isScheduleBoardTaskId(taskId) ? taskId.slice(SCHEDULE_TASK_ID_PREFIX.length) : null;
}

function areResolvedMembersEqual(
  prev: readonly ResolvedTeamMember[],
  next: readonly ResolvedTeamMember[]
): boolean {
  if (prev === next) return true;
  if (prev.length !== next.length) return false;

  for (let i = 0; i < prev.length; i++) {
    const prevMember = prev[i];
    const nextMember = next[i];
    if (
      prevMember.name !== nextMember.name ||
      prevMember.status !== nextMember.status ||
      prevMember.currentTaskId !== nextMember.currentTaskId ||
      prevMember.color !== nextMember.color ||
      prevMember.agentType !== nextMember.agentType ||
      prevMember.role !== nextMember.role ||
      prevMember.workflow !== nextMember.workflow ||
      prevMember.providerId !== nextMember.providerId ||
      prevMember.model !== nextMember.model ||
      prevMember.effort !== nextMember.effort ||
      prevMember.cwd !== nextMember.cwd ||
      prevMember.gitBranch !== nextMember.gitBranch ||
      prevMember.removedAt !== nextMember.removedAt ||
      prevMember.runtimeAdvisory?.kind !== nextMember.runtimeAdvisory?.kind ||
      prevMember.runtimeAdvisory?.observedAt !== nextMember.runtimeAdvisory?.observedAt ||
      prevMember.runtimeAdvisory?.retryUntil !== nextMember.runtimeAdvisory?.retryUntil ||
      prevMember.runtimeAdvisory?.retryDelayMs !== nextMember.runtimeAdvisory?.retryDelayMs ||
      prevMember.runtimeAdvisory?.reasonCode !== nextMember.runtimeAdvisory?.reasonCode ||
      prevMember.runtimeAdvisory?.message !== nextMember.runtimeAdvisory?.message
    ) {
      return false;
    }
  }

  return true;
}

function useStableActiveMembers(
  members: readonly ResolvedTeamMember[] | undefined
): ResolvedTeamMember[] {
  const filteredMembers = useMemo(
    () => (members ?? []).filter((member) => !member.removedAt),
    [members]
  );
  const stableMembersRef = useRef(filteredMembers);

  if (!areResolvedMembersEqual(stableMembersRef.current, filteredMembers)) {
    stableMembersRef.current = filteredMembers;
  }

  return stableMembersRef.current;
}

interface TimeWindow {
  start: number;
  end: number;
}

function filterKanbanTasks(tasks: TeamTaskWithKanban[], query: string): TeamTaskWithKanban[] {
  if (query.startsWith('#')) {
    const id = query.slice(1);
    return tasks.filter((t) => t.id === id || t.displayId === id);
  }
  const lower = query.toLowerCase();
  return tasks.filter(
    (t) =>
      t.id.toLowerCase().includes(lower) ||
      (t.displayId?.toLowerCase().includes(lower) ?? false) ||
      t.subject.toLowerCase().includes(lower) ||
      (t.owner?.toLowerCase().includes(lower) ?? false)
  );
}

const TeamOfflineStatusBanner = memo(function TeamOfflineStatusBanner({
  teamName,
  onLaunch,
}: {
  teamName: string;
  onLaunch: () => void;
}): React.JSX.Element {
  const message = '团队离线中';

  return (
    <div
      className="mb-3 flex items-center justify-between gap-3 rounded-md border px-3 py-2"
      style={{
        backgroundColor: 'var(--warning-bg)',
        borderColor: 'var(--warning-border)',
        color: 'var(--warning-text)',
      }}
    >
      <span className="flex items-center gap-1.5 text-xs">
        <AlertTriangle size={14} className="shrink-0" />
        {message}
      </span>
      <Button
        variant="ghost"
        size="sm"
        className="h-7 shrink-0 gap-1 px-2 text-xs text-[var(--step-done-text)] hover:bg-[var(--step-done-bg)]"
        onClick={onLaunch}
      >
        <Play size={12} />
        启动
      </Button>
    </div>
  );
});

type TeamMessagesPanelBridgeProps = Omit<
  ComponentProps<typeof MessagesPanel>,
  'leadActivity' | 'leadContextUpdatedAt'
>;
type SharedTeamMessagesPanelProps = Omit<TeamMessagesPanelBridgeProps, 'position'>;
type TeamMemberListBridgeProps = Omit<
  ComponentProps<typeof MemberList>,
  'leadActivity' | 'memberSpawnStatuses'
> & {
  teamName: string;
};
type TeamMemberDetailDialogBridgeProps = Omit<
  ComponentProps<typeof MemberDetailDialog>,
  'leadActivity' | 'spawnEntry' | 'runtimeEntry'
>;
interface LeadContextWatcherProps {
  teamName: string;
  tabId: string | null;
  projectId: string | null;
  leadSessionId: string | null;
  sessionHistoryKey: string;
  isThisTabActive: boolean;
  isTeamAlive?: boolean;
  sessions: readonly Session[];
  sessionsLoading: boolean;
}
interface LeadContextBridgeProps {
  teamName: string;
  tabId: string | null;
  projectId: string | null;
  leadSessionId: string | null;
  fallbackProjectRoot?: string;
}

function buildMemberSpawnStatusMap(
  memberSpawnStatuses: Record<string, MemberSpawnStatusEntry> | undefined
): Map<string, MemberSpawnStatusEntry> | undefined {
  if (!memberSpawnStatuses) {
    return undefined;
  }

  const map = new Map<string, MemberSpawnStatusEntry>(Object.entries(memberSpawnStatuses));
  return map.size > 0 ? map : undefined;
}

function buildTeamAgentRuntimeMap(
  runtimeSnapshot: Record<string, TeamAgentRuntimeEntry> | undefined
): Map<string, TeamAgentRuntimeEntry> | undefined {
  if (!runtimeSnapshot) {
    return undefined;
  }

  const map = new Map<string, TeamAgentRuntimeEntry>(Object.entries(runtimeSnapshot));
  return map.size > 0 ? map : undefined;
}

const TeamSpawnStatusWatcher = memo(function TeamSpawnStatusWatcher({
  teamName,
  isTeamProvisioning,
  isTeamAlive,
}: {
  teamName: string;
  isTeamProvisioning: boolean;
  isTeamAlive?: boolean;
}): null {
  const { leadActivity, memberSpawnStatuses, memberSpawnSnapshot, fetchMemberSpawnStatuses } =
    useStore(
      useShallow((s) => ({
        leadActivity: s.leadActivityByTeam[teamName],
        memberSpawnStatuses: s.memberSpawnStatusesByTeam[teamName],
        memberSpawnSnapshot: s.memberSpawnSnapshotsByTeam[teamName],
        fetchMemberSpawnStatuses: s.fetchMemberSpawnStatuses,
      }))
    );

  useEffect(() => {
    const hasUnresolvedSpawn = hasUnresolvedMemberSpawnStatus(
      memberSpawnStatuses,
      memberSpawnSnapshot
    );
    const shouldFetchSpawnStatuses =
      isTeamProvisioning ||
      hasUnresolvedSpawn ||
      (memberSpawnStatuses == null &&
        (isTeamAlive === true || leadActivity === 'active' || leadActivity === 'idle'));
    if (shouldFetchSpawnStatuses) {
      void fetchMemberSpawnStatuses(teamName);
    }

    if (!isTeamProvisioning && !hasUnresolvedSpawn) {
      return;
    }

    const interval = window.setInterval(() => {
      void fetchMemberSpawnStatuses(teamName);
    }, MEMBER_SPAWN_STATUS_REFRESH_MS);
    return () => {
      window.clearInterval(interval);
    };
  }, [
    fetchMemberSpawnStatuses,
    isTeamAlive,
    isTeamProvisioning,
    leadActivity,
    memberSpawnSnapshot,
    memberSpawnStatuses,
    teamName,
  ]);

  return null;
});

const TEAM_AGENT_RUNTIME_REFRESH_MS = 15_000;

const TeamAgentRuntimeWatcher = memo(function TeamAgentRuntimeWatcher({
  teamName,
  isTeamProvisioning,
  isTeamAlive,
  isThisTabActive,
}: {
  teamName: string;
  isTeamProvisioning: boolean;
  isTeamAlive?: boolean;
  isThisTabActive: boolean;
}): null {
  const { leadActivity, fetchTeamAgentRuntime } = useStore(
    useShallow((s) => ({
      leadActivity: s.leadActivityByTeam[teamName],
      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,
    isTeamProvisioning,
    isThisTabActive,
    leadActivity,
    teamName,
  ]);

  return null;
});

const LeadContextWatcher = memo(function LeadContextWatcher({
  teamName,
  tabId,
  projectId,
  leadSessionId,
  sessionHistoryKey,
  isThisTabActive,
  isTeamAlive,
  sessions,
  sessionsLoading,
}: LeadContextWatcherProps): null {
  const fetchSessionDetail = useStore((s) => s.fetchSessionDetail);
  const missingLeadSessionFetchKeyRef = useRef<string | null>(null);
  const missingLeadSessionFetchKey = useMemo(
    () => `${teamName}:${projectId ?? ''}:${leadSessionId ?? ''}:${sessionHistoryKey}`,
    [teamName, projectId, leadSessionId, sessionHistoryKey]
  );

  useEffect(() => {
    missingLeadSessionFetchKeyRef.current = null;
  }, [missingLeadSessionFetchKey]);

  useEffect(() => {
    if (!isThisTabActive) return;
    if (!tabId || !projectId || !leadSessionId) return;

    const leadSessionMissing = isLeadSessionMissing({
      leadSessionId,
      projectId,
      sessionsLoading,
      knownSessions: sessions,
    });
    if (leadSessionMissing) {
      missingLeadSessionFetchKeyRef.current = missingLeadSessionFetchKey;
      return;
    }

    const fetchLeadSessionDetail = () => {
      const suppressRepeatedFetch = shouldSuppressMissingLeadSessionFetch({
        leadSessionId,
        projectId,
        sessionsLoading,
        knownSessions: sessions,
        suppressionKey: missingLeadSessionFetchKeyRef.current,
        currentKey: missingLeadSessionFetchKey,
      });
      if (suppressRepeatedFetch) {
        return;
      }
      void fetchSessionDetail(projectId, leadSessionId, tabId, { silent: true });
    };

    fetchLeadSessionDetail();

    if (!isTeamAlive) return;

    const id = window.setInterval(() => {
      fetchLeadSessionDetail();
    }, 10_000);
    return () => window.clearInterval(id);
  }, [
    fetchSessionDetail,
    isTeamAlive,
    isThisTabActive,
    leadSessionId,
    missingLeadSessionFetchKey,
    projectId,
    sessions,
    sessionsLoading,
    tabId,
  ]);

  return null;
});

const LeadContextBridge = memo(function LeadContextBridge({
  teamName,
  tabId,
  projectId,
  leadSessionId,
  fallbackProjectRoot,
}: LeadContextBridgeProps): React.JSX.Element | null {
  const {
    leadTabData,
    leadContextSnapshot,
    isContextPanelVisible,
    selectedContextPhase,
    setContextPanelVisibleForTab,
    setSelectedContextPhaseForTab,
    fetchSessionDetail,
  } = useStore(
    useShallow((s) => ({
      leadTabData: tabId ? (s.tabSessionData[tabId] ?? null) : null,
      leadContextSnapshot: s.leadContextByTeam[teamName] ?? null,
      isContextPanelVisible: tabId ? (s.tabUIStates.get(tabId)?.showContextPanel ?? false) : false,
      selectedContextPhase: tabId ? (s.tabUIStates.get(tabId)?.selectedContextPhase ?? null) : null,
      setContextPanelVisibleForTab: s.setContextPanelVisibleForTab,
      setSelectedContextPhaseForTab: s.setSelectedContextPhaseForTab,
      fetchSessionDetail: s.fetchSessionDetail,
    }))
  );
  const [isContextButtonHovered, setIsContextButtonHovered] = useState(false);

  const setContextPanelVisible = useCallback(
    (visible: boolean) => {
      if (!tabId) return;
      setContextPanelVisibleForTab(tabId, visible);
    },
    [setContextPanelVisibleForTab, tabId]
  );
  const setSelectedContextPhase = useCallback(
    (phase: number | null) => {
      if (!tabId) return;
      setSelectedContextPhaseForTab(tabId, phase);
    },
    [setSelectedContextPhaseForTab, tabId]
  );

  const leadSessionDetail = leadTabData?.sessionDetail ?? null;
  const leadConversation = leadTabData?.conversation ?? null;
  const leadSessionContextStats = leadTabData?.sessionContextStats ?? null;
  const leadSessionPhaseInfo = leadTabData?.sessionPhaseInfo ?? null;
  const leadSessionLoading = leadTabData?.sessionDetailLoading ?? false;
  const leadSessionLoaded = Boolean(
    leadSessionId && leadSessionDetail?.session?.id === leadSessionId
  );
  const leadSubagentCostUsd = useMemo(() => {
    const processes = leadSessionDetail?.processes;
    if (!processes || processes.length === 0) return undefined;
    const total = processes.reduce((sum, p) => sum + (p.metrics.costUsd ?? 0), 0);
    return total > 0 ? total : undefined;
  }, [leadSessionDetail?.processes]);
  const { allContextInjections, lastAssistantUsage, lastAssistantModelName } = useMemo(() => {
    if (!leadSessionLoaded || !leadSessionContextStats || !leadConversation?.items.length) {
      return {
        allContextInjections: [] as ContextInjection[],
        lastAssistantUsage: null as ContextUsageLike | null,
        lastAssistantModelName: undefined as string | undefined,
      };
    }

    const effectivePhase = selectedContextPhase;

    let targetAiGroupId: string | undefined;
    if (effectivePhase !== null && leadSessionPhaseInfo) {
      const phase = leadSessionPhaseInfo.phases.find((p) => p.phaseNumber === effectivePhase);
      if (phase) {
        targetAiGroupId = phase.lastAIGroupId;
      }
    }

    if (!targetAiGroupId) {
      const lastAiItem = [...leadConversation.items].reverse().find((item) => item.type === 'ai');
      if (lastAiItem?.type !== 'ai') {
        return {
          allContextInjections: [] as ContextInjection[],
          lastAssistantUsage: null,
          lastAssistantModelName: undefined,
        };
      }
      targetAiGroupId = lastAiItem.group.id;
    }

    const stats = leadSessionContextStats.get(targetAiGroupId);
    const injections = stats?.accumulatedInjections ?? [];

    let lastUsage: ContextUsageLike | null = null;
    let lastModelName: string | undefined;
    const targetItem = leadConversation.items.find(
      (item) => item.type === 'ai' && item.group.id === targetAiGroupId
    );
    if (targetItem?.type === 'ai') {
      const responses = targetItem.group.responses || [];
      for (let i = responses.length - 1; i >= 0; i--) {
        const msg = responses[i];
        if (msg.type === 'assistant' && msg.usage) {
          lastUsage = msg.usage;
          lastModelName = msg.model;
          break;
        }
      }
    }

    return {
      allContextInjections: injections,
      lastAssistantUsage: lastUsage,
      lastAssistantModelName: lastModelName,
    };
  }, [
    leadConversation,
    leadSessionContextStats,
    leadSessionLoaded,
    leadSessionPhaseInfo,
    selectedContextPhase,
  ]);
  const visibleContextTokens = useMemo(
    () => sumContextInjectionTokens(allContextInjections),
    [allContextInjections]
  );
  const contextMetrics = useMemo(
    () =>
      deriveContextMetrics({
        usage: lastAssistantUsage,
        modelName: lastAssistantModelName,
        contextWindowTokens: leadContextSnapshot?.contextWindowTokens ?? null,
        visibleContextTokens,
      }),
    [
      lastAssistantModelName,
      lastAssistantUsage,
      leadContextSnapshot?.contextWindowTokens,
      visibleContextTokens,
    ]
  );
  const contextUsedPercentLabel = useMemo(() => {
    const percent =
      contextMetrics.contextUsedPercentOfContextWindow ?? leadContextSnapshot?.contextUsedPercent;
    return percent === null || percent === undefined ? null : `${percent.toFixed(1)}%`;
  }, [contextMetrics.contextUsedPercentOfContextWindow, leadContextSnapshot?.contextUsedPercent]);

  if (!leadSessionId) {
    return null;
  }

  return (
    <>
      {isContextPanelVisible && (
        <div className="w-80 shrink-0">
          {leadSessionLoaded ? (
            <SessionContextPanel
              injections={allContextInjections}
              onClose={() => setContextPanelVisible(false)}
              projectRoot={leadSessionDetail?.session?.projectPath ?? fallbackProjectRoot}
              contextMetrics={contextMetrics}
              sessionMetrics={leadSessionDetail?.metrics}
              subagentCostUsd={leadSubagentCostUsd}
              phaseInfo={leadSessionPhaseInfo ?? undefined}
              selectedPhase={selectedContextPhase}
              onPhaseChange={setSelectedContextPhase}
              side="left"
            />
          ) : (
            <div
              className="flex h-full flex-col border-0 bg-[var(--color-surface)]"
              style={{ backgroundColor: 'var(--color-surface)' }}
            >
              <div className="flex items-center justify-between border-b border-[var(--color-border)] px-3 py-2">
                <div className="min-w-0">
                  <p className="text-sm font-medium text-[var(--color-text)]">上下文</p>
                  <p className="text-[10px] text-[var(--color-text-muted)]">
                    {leadSessionLoading ? '加载中…' : '暂无会话'}
                  </p>
                </div>
                <button
                  type="button"
                  className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
                  onClick={() => setContextPanelVisible(false)}
                  aria-label={`关闭 ${teamName} 上下文面板`}
                >
                  ×
                </button>
              </div>
              <div className="flex flex-1 items-center justify-center p-4">
                <p className="text-xs text-[var(--color-text-muted)]">
                  {leadSessionLoading ? '正在加载上下文…' : '打开团队负责人会话后可查看上下文。'}
                </p>
              </div>
            </div>
          )}
        </div>
      )}

      <div
        className="pointer-events-none fixed bottom-4 z-20"
        style={{ left: isContextPanelVisible ? 'calc(20rem + 1rem)' : '1rem' }}
      >
        <button
          onClick={() => {
            const next = !isContextPanelVisible;
            setContextPanelVisible(next);
            if (tabId && projectId) {
              void fetchSessionDetail(projectId, leadSessionId, tabId, { silent: true });
            }
          }}
          onMouseEnter={() => setIsContextButtonHovered(true)}
          onMouseLeave={() => setIsContextButtonHovered(false)}
          className="pointer-events-auto flex w-fit items-center gap-1 rounded-md px-2.5 py-1.5 text-xs shadow-lg backdrop-blur-md transition-colors"
          style={{
            backgroundColor: isContextPanelVisible
              ? 'var(--context-btn-active-bg)'
              : isContextButtonHovered
                ? 'var(--context-btn-bg-hover)'
                : 'var(--context-btn-bg)',
            color: isContextPanelVisible
              ? 'var(--context-btn-active-text)'
              : 'var(--color-text-secondary)',
          }}
          title={
            leadSessionLoaded
              ? `会话：${leadSessionId}`
              : leadSessionLoading
                ? '正在加载上下文…'
                : leadSessionId
          }
        >
          {contextUsedPercentLabel ?? '上下文'}
        </button>
      </div>
    </>
  );
});

const TeamMemberListBridge = memo(function TeamMemberListBridge({
  teamName,
  ...props
}: TeamMemberListBridgeProps): React.JSX.Element {
  const { leadActivity, progress, memberSpawnStatuses, memberSpawnSnapshot, runtimeSnapshot } =
    useStore(
      useShallow((s) => ({
        leadActivity: s.leadActivityByTeam[teamName],
        progress: getCurrentProvisioningProgressForTeam(s, teamName),
        memberSpawnStatuses: s.memberSpawnStatusesByTeam[teamName],
        memberSpawnSnapshot: s.memberSpawnSnapshotsByTeam[teamName],
        runtimeSnapshot: s.teamAgentRuntimeByTeam[teamName],
      }))
    );
  const memberSpawnStatusMap = useMemo(
    () => buildMemberSpawnStatusMap(memberSpawnStatuses),
    [memberSpawnStatuses]
  );
  const memberRuntimeMap = useMemo(
    () => buildTeamAgentRuntimeMap(runtimeSnapshot?.members),
    [runtimeSnapshot?.members]
  );
  const runtimeRunId = runtimeSnapshot?.runId ?? memberSpawnSnapshot?.runId ?? progress?.runId;
  const isLaunchSettling = useMemo(() => {
    if (progress?.state !== 'ready') {
      return false;
    }
    return getLaunchJoinState(
      getLaunchJoinMilestonesFromMembers({
        members: props.members,
        memberSpawnStatuses,
        memberSpawnSnapshot,
      })
    ).hasMembersStillJoining;
  }, [memberSpawnSnapshot, memberSpawnStatuses, progress?.state, props.members]);

  return (
    <MemberList
      {...props}
      leadActivity={leadActivity}
      memberSpawnStatuses={memberSpawnStatusMap}
      memberRuntimeEntries={memberRuntimeMap}
      runtimeRunId={runtimeRunId}
      isLaunchSettling={isLaunchSettling}
    />
  );
});

const TeamMessagesPanelBridge = memo(function TeamMessagesPanelBridge({
  teamName,
  ...props
}: TeamMessagesPanelBridgeProps): React.JSX.Element {
  const { leadActivity, leadContextUpdatedAt } = useStore(
    useShallow((s) => ({
      leadActivity: s.leadActivityByTeam[teamName],
      leadContextUpdatedAt: s.leadContextByTeam[teamName]?.updatedAt,
    }))
  );

  return (
    <MessagesPanel
      {...props}
      teamName={teamName}
      leadActivity={leadActivity}
      leadContextUpdatedAt={leadContextUpdatedAt}
    />
  );
});

const TeamMemberDetailDialogBridge = memo(function TeamMemberDetailDialogBridge({
  teamName,
  member,
  ...props
}: TeamMemberDetailDialogBridgeProps): React.JSX.Element | null {
  const {
    leadActivity,
    liveMember,
    progress,
    launchMembers,
    memberSpawnStatuses,
    memberSpawnSnapshot,
    spawnEntry,
    runtimeRunId,
    runtimeEntry,
  } = useStore(
    useShallow((s) => ({
      leadActivity: s.leadActivityByTeam[teamName],
      liveMember: member ? selectResolvedMemberForTeamName(s, teamName, member.name) : null,
      progress: getCurrentProvisioningProgressForTeam(s, teamName),
      launchMembers: selectTeamMemberSnapshotsForName(s, teamName),
      memberSpawnStatuses: s.memberSpawnStatusesByTeam[teamName],
      memberSpawnSnapshot: s.memberSpawnSnapshotsByTeam[teamName],
      spawnEntry: member ? s.memberSpawnStatusesByTeam[teamName]?.[member.name] : undefined,
      runtimeRunId:
        s.teamAgentRuntimeByTeam[teamName]?.runId ??
        s.memberSpawnSnapshotsByTeam[teamName]?.runId ??
        getCurrentProvisioningProgressForTeam(s, teamName)?.runId,
      runtimeEntry: member ? s.teamAgentRuntimeByTeam[teamName]?.members[member.name] : undefined,
    }))
  );
  const isLaunchSettling = useMemo(() => {
    if (progress?.state !== 'ready') {
      return false;
    }
    return getLaunchJoinState(
      getLaunchJoinMilestonesFromMembers({
        members: launchMembers,
        memberSpawnStatuses,
        memberSpawnSnapshot,
      })
    ).hasMembersStillJoining;
  }, [launchMembers, memberSpawnSnapshot, memberSpawnStatuses, progress?.state]);

  return (
    <MemberDetailDialog
      {...props}
      teamName={teamName}
      member={liveMember ?? member}
      isLaunchSettling={isLaunchSettling}
      leadActivity={leadActivity}
      spawnEntry={spawnEntry}
      runtimeEntry={runtimeEntry}
      runtimeRunId={runtimeRunId}
    />
  );
});

export const TeamDetailView = ({
  teamName,
  isPaneFocused = false,
}: TeamDetailViewProps): React.JSX.Element => {
  const { isLight } = useTheme();
  const [requestChangesTaskId, setRequestChangesTaskId] = useState<string | null>(null);
  const [selectedTask, setSelectedTask] = useState<TeamTaskWithKanban | null>(null);
  const [selectedMember, setSelectedMember] = useState<ResolvedTeamMember | null>(null);
  const [selectedMemberView, setSelectedMemberView] = useState<{
    initialTab?: MemberDetailTab;
    initialActivityFilter?: MemberActivityFilter;
  } | null>(null);
  const [pendingRepliesByMember, setPendingRepliesByMember] = useState<Record<string, number>>(() =>
    getTeamPendingRepliesState(teamName)
  );
  const [createTaskDialog, setCreateTaskDialog] = useState<CreateTaskDialogState>({
    open: false,
    defaultSubject: '',
    defaultDescription: '',
    defaultOwner: '',
  });
  const [creatingTask, setCreatingTask] = useState(false);
  const [removeMemberConfirm, setRemoveMemberConfirm] = useState<string | null>(null);
  const [updatingRoleLoading, setUpdatingRoleLoading] = useState(false);
  const [editDialogOpen, setEditDialogOpen] = useState(false);
  const [savedLaunchRequest, setSavedLaunchRequest] = useState<TeamLaunchRequest | null>(null);
  useEffect(() => {
    if (!editDialogOpen || !teamName) return;
    let cancelled = false;
    void (async () => {
      try {
        const saved = await api.teams.getSavedRequest(teamName);
        if (!cancelled) setSavedLaunchRequest(saved ?? null);
      } catch {
        if (!cancelled) setSavedLaunchRequest(null);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [editDialogOpen, teamName]);
  const [launchDialogState, setLaunchDialogState] = useState<{
    open: boolean;
    mode: TeamLaunchDialogMode;
  }>({
    open: false,
    mode: 'launch',
  });
  const [editorOpen, setEditorOpen] = useState(false);
  const contentRef = useRef<HTMLDivElement>(null);
  const provisioningBannerRef = useRef<HTMLDivElement>(null);
  const wasProvisioningRef = useRef(false);

  // Set inert on background content when editor overlay is open (a11y focus trap)
  useEffect(() => {
    const el = contentRef.current;
    if (!el) return;
    if (editorOpen) {
      el.setAttribute('inert', '');
    } else {
      el.removeAttribute('inert');
    }
  }, [editorOpen]);

  // Listen for graph tab actions (open task, send message)
  useEffect(() => {
    const onOpenTask = (e: Event) => {
      const { teamName: tn, taskId } = (e as CustomEvent).detail ?? {};
      if (tn !== teamName || !data) return;
      const task = data.tasks.find((t: { id: string }) => t.id === taskId);
      if (task) setSelectedTask(task);
    };
    const onSendMsg = (e: Event) => {
      const { teamName: tn, memberName } = (e as CustomEvent).detail ?? {};
      if (tn !== teamName) return;
      setSendDialogRecipient(memberName);
      setSendDialogDefaultText(undefined);
      setSendDialogDefaultChip(undefined);
      setSendDialogOpen(true);
    };
    const onOpenProfile = (e: Event) => {
      const {
        teamName: tn,
        memberName,
        initialTab,
        initialActivityFilter,
      } = (e as CustomEvent).detail ?? {};
      if (tn !== teamName || !data) return;
      const member = members.find((m: { name: string }) => m.name === memberName);
      if (member) {
        setSelectedMember(member);
        setSelectedMemberView({
          initialTab,
          initialActivityFilter,
        });
      }
    };
    const onCreateTask = (e: Event) => {
      const { teamName: tn, owner } = (e as CustomEvent).detail ?? {};
      if (tn !== teamName) return;
      openCreateTaskDialog('', '', owner ?? '');
    };
    window.addEventListener('graph:open-task', onOpenTask);
    window.addEventListener('graph:send-message', onSendMsg);
    window.addEventListener('graph:open-profile', onOpenProfile);
    window.addEventListener('graph:create-task', onCreateTask);

    // Task action events from graph
    const taskAction = (handler: (taskId: string) => void) => (e: Event) => {
      const { teamName: tn, taskId } = (e as CustomEvent).detail ?? {};
      if (tn !== teamName || !taskId) return;
      handler(taskId);
    };
    const onStartTask = taskAction((taskId) => {
      void (async () => {
        try {
          const result = await startTaskByUser(teamName, taskId);
          if (data?.isAlive) {
            const task = data.tasks.find((t: { id: string }) => t.id === taskId);
            try {
              if (result.notifiedOwner && task?.owner) {
                await api.teams.processSend(
                  teamName,
                  `Task ${formatTaskDisplayLabel(task)} "${task.subject}" has started. Please begin working on it.`
                );
              }
            } catch {
              /* best-effort */
            }
          }
        } catch {
          /* error via store */
        }
      })();
    });
    const onCompleteTask = taskAction((taskId) => {
      void (async () => {
        try {
          await updateTaskStatus(teamName, taskId, 'completed');
        } catch {
          /* */
        }
      })();
    });
    const onApproveTask = taskAction((taskId) => {
      void (async () => {
        try {
          await updateKanban(teamName, taskId, { op: 'set_column', column: 'approved' });
        } catch {
          /* */
        }
      })();
    });
    const onRequestReviewTask = taskAction((taskId) => {
      void (async () => {
        try {
          await requestReview(teamName, taskId);
        } catch {
          /* */
        }
      })();
    });
    const onRequestChangesTask = taskAction((taskId) => {
      setRequestChangesTaskId(taskId);
    });
    const onCancelTask = taskAction((taskId) => {
      void (async () => {
        try {
          await updateTaskStatus(teamName, taskId, 'pending');
        } catch {
          /* */
        }
      })();
    });
    const onMoveBackToDoneTask = taskAction((taskId) => {
      void (async () => {
        try {
          await updateKanban(teamName, taskId, { op: 'remove' });
          await updateTaskStatus(teamName, taskId, 'completed');
        } catch {
          /* */
        }
      })();
    });
    const onDeleteTaskGraph = taskAction((taskId) => handleDeleteTask(taskId));

    window.addEventListener('graph:start-task', onStartTask);
    window.addEventListener('graph:complete-task', onCompleteTask);
    window.addEventListener('graph:approve-task', onApproveTask);
    window.addEventListener('graph:request-review', onRequestReviewTask);
    window.addEventListener('graph:request-changes', onRequestChangesTask);
    window.addEventListener('graph:cancel-task', onCancelTask);
    window.addEventListener('graph:move-back-to-done', onMoveBackToDoneTask);
    window.addEventListener('graph:delete-task', onDeleteTaskGraph);
    return () => {
      window.removeEventListener('graph:open-task', onOpenTask);
      window.removeEventListener('graph:send-message', onSendMsg);
      window.removeEventListener('graph:open-profile', onOpenProfile);
      window.removeEventListener('graph:create-task', onCreateTask);
      window.removeEventListener('graph:start-task', onStartTask);
      window.removeEventListener('graph:complete-task', onCompleteTask);
      window.removeEventListener('graph:approve-task', onApproveTask);
      window.removeEventListener('graph:request-review', onRequestReviewTask);
      window.removeEventListener('graph:request-changes', onRequestChangesTask);
      window.removeEventListener('graph:cancel-task', onCancelTask);
      window.removeEventListener('graph:move-back-to-done', onMoveBackToDoneTask);
      window.removeEventListener('graph:delete-task', onDeleteTaskGraph);
    };
  });

  const [sendDialogOpen, setSendDialogOpen] = useState(false);
  const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
  const [trashOpen, setTrashOpen] = useState(false);
  const [sendDialogRecipient, setSendDialogRecipient] = useState<string | undefined>(undefined);
  const [sendDialogDefaultText, setSendDialogDefaultText] = useState<string | undefined>(undefined);
  const [sendDialogDefaultChip, setSendDialogDefaultChip] = useState<InlineChip | undefined>(
    undefined
  );
  const [replyQuote, setReplyQuote] = useState<{ from: string; text: string } | undefined>(
    undefined
  );
  const [reviewDialogState, setReviewDialogState] = useState<{
    open: boolean;
    mode: 'agent' | 'task';
    memberName?: string;
    taskId?: string;
    initialFilePath?: string;
    taskChangeRequestOptions?: TaskChangeRequestOptions;
  }>({ open: false, mode: 'task' });

  // Active teams for conflict warning in LaunchTeamDialog
  const [activeTeamsForLaunch, setActiveTeamsForLaunch] = useState<
    { teamName: string; displayName: string; projectPath: string }[]
  >([]);
  const launchDialogOpen = launchDialogState.open;

  // Session loading and filtering state
  const [sessions, setSessions] = useState<Session[]>([]);
  const [sessionsLoading, setSessionsLoading] = useState(false);
  const [sessionsError, setSessionsError] = useState<string | null>(null);
  const [kanbanFilter, setKanbanFilter] = useState<KanbanFilterState>({
    sessionId: null,
    selectedOwners: new Set(),
    columns: new Set(),
  });
  const [kanbanSort, setKanbanSort] = useState<KanbanSortState>({ field: 'updatedAt' });

  const {
    data,
    members,
    loading,
    error,
    projects,
    repositoryGroups,
    initTabUIState,
    selectTeam,
    updateKanban,
    updateKanbanColumnOrder,
    updateTaskStatus,
    updateTaskOwner,
    sendTeamMessage,
    requestReview,
    createTeamTask,
    startTaskByUser,
    deleteTeam,
    openTeamsTab,
    closeTab,
    sendingMessage,
    sendMessageError,
    sendMessageWarning,
    sendMessageDebugDetails,
    lastSendMessageResult,
    reviewActionError,
    restartMember,
    skipMemberForLaunch,
    removeMember,
    updateMemberRole,
    launchTeam,
    provisioningError,
    clearProvisioningError,
    isTeamProvisioning,
    refreshTeamData,
    refreshTeamMessagesHead,
    refreshMemberActivityMeta,
    syncTeamPendingReplyRefresh,
    kanbanFilterQuery,
    clearKanbanFilter,
    softDeleteTask,
    restoreTask,
    fetchDeletedTasks,
    deletedTasks,
    launchParams,
    selectReviewFile,
    pendingReviewRequest,
    setPendingReviewRequest,
    teams,
    schedules,
    scheduleRuns,
    scheduleRunsLoading,
    fetchSchedules,
    fetchRunHistory,
    triggerScheduleNow,
    removeSchedule,
    fetchTeams,
  } = useStore(
    useShallow((s) => ({
      projects: s.projects,
      repositoryGroups: s.repositoryGroups,
      initTabUIState: s.initTabUIState,
      selectTeam: s.selectTeam,
      updateKanban: s.updateKanban,
      updateKanbanColumnOrder: s.updateKanbanColumnOrder,
      updateTaskStatus: s.updateTaskStatus,
      updateTaskOwner: s.updateTaskOwner,
      sendTeamMessage: s.sendTeamMessage,
      requestReview: s.requestReview,
      createTeamTask: s.createTeamTask,
      startTaskByUser: s.startTaskByUser,
      deleteTeam: s.deleteTeam,
      openTeamsTab: s.openTeamsTab,
      closeTab: s.closeTab,
      sendingMessage: s.sendingMessage,
      sendMessageError: s.sendMessageError,
      sendMessageWarning: s.sendMessageWarning,
      sendMessageDebugDetails: s.sendMessageDebugDetails,
      lastSendMessageResult: s.lastSendMessageResult,
      reviewActionError: s.reviewActionError,
      restartMember: s.restartMember,
      skipMemberForLaunch: s.skipMemberForLaunch,
      removeMember: s.removeMember,
      updateMemberRole: s.updateMemberRole,
      launchTeam: s.launchTeam,
      provisioningError: teamName ? (s.provisioningErrorByTeam[teamName] ?? null) : null,
      clearProvisioningError: s.clearProvisioningError,
      isTeamProvisioning: teamName ? isTeamProvisioningActive(s, teamName) : false,
      data: s.selectedTeamName === teamName ? s.selectedTeamData : null,
      members: selectResolvedMembersForTeamName(s, teamName),
      loading: s.selectedTeamName === teamName ? s.selectedTeamLoading : false,
      error: s.selectedTeamName === teamName ? s.selectedTeamError : null,
      refreshTeamData: s.refreshTeamData,
      refreshTeamMessagesHead: s.refreshTeamMessagesHead,
      refreshMemberActivityMeta: s.refreshMemberActivityMeta,
      syncTeamPendingReplyRefresh: s.syncTeamPendingReplyRefresh,
      kanbanFilterQuery: s.kanbanFilterQuery,
      clearKanbanFilter: s.clearKanbanFilter,
      softDeleteTask: s.softDeleteTask,
      restoreTask: s.restoreTask,
      fetchDeletedTasks: s.fetchDeletedTasks,
      deletedTasks: s.deletedTasks,
      launchParams: teamName ? s.launchParamsByTeam[teamName] : undefined,
      selectReviewFile: s.selectReviewFile,
      pendingReviewRequest: s.pendingReviewRequest,
      setPendingReviewRequest: s.setPendingReviewRequest,
      teams: s.teams,
      schedules: s.schedules,
      scheduleRuns: s.scheduleRuns,
      scheduleRunsLoading: s.scheduleRunsLoading,
      fetchSchedules: s.fetchSchedules,
      fetchRunHistory: s.fetchRunHistory,
      triggerScheduleNow: s.triggerNow,
      removeSchedule: s.deleteSchedule,
      fetchTeams: s.fetchTeams,
    }))
  );

  const tabId = useTabIdOptional();
  const activeTabId = useStore((s) => s.activeTabId);
  const isThisTabActive = tabId ? activeTabId === tabId : false;
  const wasInteractiveRef = useRef(false);

  const keepMessagesInline = useCallback((_mode: TeamMessagesPanelMode) => {}, []);

  useEffect(() => {
    if (tabId) {
      initTabUIState(tabId);
    }
  }, [tabId, initTabUIState]);

  useEffect(() => {
    setPendingRepliesByMember(getTeamPendingRepliesState(teamName));
  }, [teamName]);

  useEffect(() => {
    setTeamPendingRepliesState(teamName, pendingRepliesByMember);
  }, [pendingRepliesByMember, teamName]);

  useEffect(() => {
    const wasProvisioning = wasProvisioningRef.current;
    wasProvisioningRef.current = isTeamProvisioning;
    if (!wasProvisioning && isTeamProvisioning) {
      provisioningBannerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  }, [isTeamProvisioning]);

  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 (pendingRevealFile && data?.config.projectPath) {
      setEditorOpen(true);
    }
  }, [pendingRevealFile, data?.config.projectPath]);

  useEffect(() => {
    if (!teamName) {
      return;
    }
    void selectTeam(teamName);
    void fetchDeletedTasks(teamName);
  }, [teamName, selectTeam, fetchDeletedTasks]);

  useEffect(() => {
    void fetchSchedules();
  }, [fetchSchedules]);

  const teamSchedules = useMemo(
    () => schedules.filter((schedule) => schedule.teamName === teamName),
    [schedules, teamName]
  );

  useEffect(() => {
    for (const schedule of teamSchedules) {
      if (scheduleRuns[schedule.id] || scheduleRunsLoading[schedule.id]) {
        continue;
      }
      void fetchRunHistory(schedule.id);
    }
  }, [fetchRunHistory, scheduleRuns, scheduleRunsLoading, teamSchedules]);

  const scheduleKanbanTasks = useMemo<TeamTaskWithKanban[]>(() => {
    return teamSchedules.map((schedule: Schedule) => {
      const latestRun = (scheduleRuns[schedule.id] ?? [])[0];
      const isInProgress = latestRun ? SCHEDULE_IN_PROGRESS_STATUSES.has(latestRun.status) : false;
      const status: TeamTaskWithKanban['status'] = isInProgress ? 'in_progress' : 'pending';
      const title = schedule.label?.trim() || getCronDescription(schedule.cronExpression);
      const latestRunText = latestRun
        ? `最近运行：${latestRun.status}${latestRun.startedAt ? ` (${new Date(latestRun.startedAt).toLocaleString('zh-CN')})` : ''}`
        : null;
      const nextRunText = schedule.nextRunAt
        ? `下次执行：${new Date(schedule.nextRunAt).toLocaleString('zh-CN')}`
        : null;

      return {
        id: `${SCHEDULE_TASK_ID_PREFIX}${schedule.id}`,
        displayId: `SCH-${schedule.id.slice(0, 6)}`,
        subject: `[定时] ${title}`,
        description: [
          `Cron: ${schedule.cronExpression}`,
          `时区: ${schedule.timezone}`,
          nextRunText,
          latestRunText,
        ]
          .filter((item): item is string => Boolean(item))
          .join('\n'),
        status,
        createdAt: schedule.createdAt,
        updatedAt: latestRun?.startedAt ?? schedule.updatedAt,
      };
    });
  }, [scheduleRuns, teamSchedules]);

  // 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.
  const storedTeamName = data?.teamName;
  useEffect(() => {
    if (!isThisTabActive || !teamName || loading) return;
    if (storedTeamName != null && storedTeamName !== teamName) {
      void selectTeam(teamName);
    }
  }, [isThisTabActive, teamName, storedTeamName, loading, selectTeam]);

  useEffect(() => {
    const isInteractive = isThisTabActive && isPaneFocused;
    const justBecameInteractive = isInteractive && !wasInteractiveRef.current;
    wasInteractiveRef.current = isInteractive;
    if (!justBecameInteractive || !teamName) {
      return;
    }

    void (async () => {
      try {
        const headResult = await refreshTeamMessagesHead(teamName);
        if (headResult.feedChanged) {
          await refreshMemberActivityMeta(teamName);
        }
      } catch {
        // Best-effort refresh on tab focus.
      }
    })();
  }, [
    isPaneFocused,
    isThisTabActive,
    refreshMemberActivityMeta,
    refreshTeamMessagesHead,
    teamName,
  ]);

  // Fetch active teams when launch dialog opens (for conflict warning)
  useEffect(() => {
    if (!launchDialogOpen) return;
    let cancelled = false;
    const teamsSnapshot = useStore.getState().teams;
    void (async () => {
      try {
        const aliveList = await api.teams.aliveList();
        if (cancelled) return;
        const aliveSet = new Set(aliveList);
        const refs = teamsSnapshot
          .filter((t) => aliveSet.has(t.teamName) && t.projectPath)
          .map((t) => ({
            teamName: t.teamName,
            displayName: t.displayName,
            projectPath: t.projectPath!,
          }));
        setActiveTeamsForLaunch(refs);
      } catch {
        // best-effort
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [launchDialogOpen]);

  useEffect(() => {
    if (kanbanFilterQuery) {
      setKanbanSearch(kanbanFilterQuery);
      clearKanbanFilter();
    }
  }, [kanbanFilterQuery, clearKanbanFilter]);

  // Load sessions for the team's project
  const projectId = useMemo(
    () => resolveProjectIdByPath(data?.config.projectPath, projects, repositoryGroups),
    [projects, repositoryGroups, data?.config.projectPath]
  );

  const leadSessionId = data?.config.leadSessionId ?? null;
  const pendingReplyRefreshSourceId = useId();
  const sessionHistoryKey = useMemo(
    () => (data?.config.sessionHistory ?? []).join('|'),
    [data?.config.sessionHistory]
  );

  // Keep team message state fresh while we are explicitly waiting for a reply.
  // This stays enabled even for hidden mounted tabs, because the waiting state
  // is renderer-local and should keep its lightweight polling until resolved.
  useEffect(() => {
    const hasPendingReplies = Object.keys(pendingRepliesByMember).length > 0;
    syncTeamPendingReplyRefresh(
      teamName,
      pendingReplyRefreshSourceId,
      Boolean(data?.isAlive) && hasPendingReplies,
      TEAM_PENDING_REPLY_REFRESH_DELAY_MS
    );

    return () => {
      syncTeamPendingReplyRefresh(teamName, pendingReplyRefreshSourceId, false);
    };
  }, [
    data?.isAlive,
    pendingRepliesByMember,
    pendingReplyRefreshSourceId,
    syncTeamPendingReplyRefresh,
    teamName,
  ]);

  useEffect(() => {
    if (!projectId) return;

    let cancelled = false;
    setSessionsLoading(true);
    setSessionsError(null);

    void (async () => {
      try {
        const result = await api.getSessions(projectId);
        if (!cancelled) {
          setSessions(result);
        }
      } catch (e) {
        if (!cancelled) {
          setSessionsError(e instanceof Error ? e.message : '加载会话失败');
        }
      } finally {
        if (!cancelled) {
          setSessionsLoading(false);
        }
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [projectId]);

  // Live git branch tracking for the lead project and member worktrees
  const teamProjectPath = data?.config.projectPath?.trim() ?? null;
  const leadProjectPath = useMemo(() => {
    const explicitLeadPath = members.find((member) => isLeadMember(member))?.cwd?.trim();
    return explicitLeadPath && explicitLeadPath.length > 0 ? explicitLeadPath : teamProjectPath;
  }, [members, teamProjectPath]);
  const branchSyncPaths = useMemo(() => {
    const uniquePaths = new Map<string, string>();
    const addPath = (candidate: string | null | undefined): void => {
      const trimmed = candidate?.trim();
      if (!trimmed) return;
      const key = normalizePath(trimmed);
      if (!key || uniquePaths.has(key)) return;
      uniquePaths.set(key, trimmed);
    };

    addPath(leadProjectPath);
    for (const member of members) {
      addPath(member.cwd);
    }

    return Array.from(uniquePaths.values());
  }, [members, leadProjectPath]);
  useBranchSync(branchSyncPaths, { live: true });
  const trackedBranches = useStore(
    useShallow((s) =>
      Object.fromEntries(
        branchSyncPaths.map((projectPath) => {
          const normalizedPath = normalizePath(projectPath);
          return [normalizedPath, s.branchByPath[normalizedPath] ?? null] as const;
        })
      )
    )
  );
  const leadBranch = leadProjectPath
    ? (trackedBranches[normalizePath(leadProjectPath)] ?? null)
    : null;
  const membersWithLiveBranches = useMemo(() => {
    if (!data) return [];

    return members.map((member) => {
      const memberPath = member.cwd?.trim();
      const nextGitBranch =
        memberPath && !isLeadMember(member) && leadBranch !== null
          ? (() => {
              const branch = trackedBranches[normalizePath(memberPath)] ?? null;
              return branch && branch !== leadBranch ? branch : undefined;
            })()
          : undefined;

      if (member.gitBranch === nextGitBranch) {
        return member;
      }

      const nextMember: ResolvedTeamMember = { ...member };
      if (nextGitBranch) {
        nextMember.gitBranch = nextGitBranch;
      } else {
        delete nextMember.gitBranch;
      }
      return nextMember;
    });
  }, [leadBranch, members, trackedBranches]);
  const resolvedMemberColorMap = useMemo(
    () => buildMemberColorMap(membersWithLiveBranches),
    [membersWithLiveBranches]
  );

  // Filter sessions to team-only using sessionHistory + leadSessionId
  const teamSessionIds = useMemo(() => {
    const sessionIds = new Set<string>();
    if (data?.config.leadSessionId) {
      sessionIds.add(data.config.leadSessionId);
    }
    if (data?.config.sessionHistory) {
      for (const id of data.config.sessionHistory) {
        sessionIds.add(id);
      }
    }
    return sessionIds;
  }, [data?.config.leadSessionId, data?.config.sessionHistory]);

  const teamSessions = useMemo(() => {
    return sessions.filter((s) => teamSessionIds.has(s.id));
  }, [sessions, teamSessionIds]);

  // Auto-reset session filter if the selected session is no longer in teamSessions
  useEffect(() => {
    if (
      kanbanFilter.sessionId !== null &&
      !teamSessions.some((s) => s.id === kanbanFilter.sessionId)
    ) {
      setKanbanFilter((prev) => ({ ...prev, sessionId: null }));
    }
  }, [kanbanFilter.sessionId, teamSessions]);

  // Compute time-window for session filtering
  const timeWindow = useMemo<TimeWindow | null>(() => {
    if (kanbanFilter.sessionId === null) return null;

    const sorted = [...teamSessions].sort((a, b) => a.createdAt - b.createdAt);
    const idx = sorted.findIndex((s) => s.id === kanbanFilter.sessionId);
    if (idx === -1) return null;

    const start = sorted[idx].createdAt;
    const end = idx + 1 < sorted.length ? sorted[idx + 1].createdAt : Infinity;
    return { start, end };
  }, [kanbanFilter.sessionId, teamSessions]);

  // Filter tasks by time-window and owner
  const filteredTasks = useMemo(() => {
    if (!data) return [];
    let result = data.tasks;

    // Session time-window filter
    if (timeWindow) {
      result = result.filter((t) => {
        if (!t.createdAt) return true; // legacy tasks always included
        const ts = new Date(t.createdAt).getTime();
        return ts >= timeWindow.start && ts < timeWindow.end;
      });
    }

    // Owner filter
    if (kanbanFilter.selectedOwners.size > 0) {
      result = result.filter((t) =>
        t.owner
          ? kanbanFilter.selectedOwners.has(t.owner)
          : kanbanFilter.selectedOwners.has(UNASSIGNED_OWNER)
      );
    }

    return result;
  }, [data, timeWindow, kanbanFilter.selectedOwners]);

  const activeMembers = useStableActiveMembers(membersWithLiveBranches);

  const mergedKanbanTasks = useMemo(
    () => [...filteredTasks, ...scheduleKanbanTasks],
    [filteredTasks, scheduleKanbanTasks]
  );

  const kanbanDisplayTasks = useMemo(() => {
    const query = kanbanSearch.trim();
    if (!query) return mergedKanbanTasks;
    return filterKanbanTasks(mergedKanbanTasks, query);
  }, [mergedKanbanTasks, kanbanSearch]);

  const activeTeammateCount = useMemo(
    () => activeMembers.filter((m) => !isLeadMember(m)).length,
    [activeMembers]
  );

  const taskMap = useMemo(() => new Map((data?.tasks ?? []).map((t) => [t.id, t])), [data?.tasks]);
  const taskMapRef = useRef(taskMap);
  taskMapRef.current = taskMap;

  const memberTaskCounts = useMemo(() => buildTaskCountsByOwner(data?.tasks ?? []), [data?.tasks]);

  const openCreateTaskDialog = useCallback(
    (subject = '', description = '', owner = '', startImmediately?: boolean): void => {
      setCreateTaskDialog({
        open: true,
        defaultSubject: subject,
        defaultDescription: description,
        defaultOwner: owner,
        defaultStartImmediately: startImmediately,
      });
    },
    []
  );

  const closeCreateTaskDialog = useCallback((): void => {
    setCreateTaskDialog({
      open: false,
      defaultSubject: '',
      defaultDescription: '',
      defaultOwner: '',
      defaultStartImmediately: undefined,
    });
  }, []);

  const handleCreateTaskFromMessage = useCallback((subject: string, description: string) => {
    openCreateTaskDialog(subject, description);
  }, []);

  const handleReplyToMessage = useCallback((message: { from: string; text: string }) => {
    setSendDialogRecipient(message.from);
    setSendDialogDefaultText(undefined);
    setSendDialogDefaultChip(undefined);
    setReplyQuote({ from: message.from, text: stripAgentBlocks(message.text) });
    setSendDialogOpen(true);
  }, []);

  const openLaunchDialog = useCallback((mode: TeamLaunchDialogMode) => {
    setLaunchDialogState({ open: true, mode });
  }, []);

  const closeLaunchDialog = useCallback(() => {
    setLaunchDialogState((prev) => ({ ...prev, open: false }));
  }, []);

  const handleRestartTeam = useCallback(() => {
    openLaunchDialog('relaunch');
  }, [openLaunchDialog]);

  const handleLaunchDialogSubmit = useCallback(
    async (request: TeamLaunchRequest): Promise<void> => {
      await launchTeam(request);
      await Promise.all([fetchTeams(), selectTeam(teamName)]);
    },
    [fetchTeams, launchTeam, selectTeam, teamName]
  );

  const handleRelaunchDialogSubmit = useCallback(
    async (
      request: TeamLaunchRequest,
      nextMembers: TeamCreateRequest['members']
    ): Promise<void> => {
      await executeTeamRelaunch({
        teamName,
        isTeamAlive: data?.isAlive === true,
        request,
        members: nextMembers,
        stopTeam: (nextTeamName) => api.teams.stop(nextTeamName),
        replaceMembers: (nextTeamName, nextRequest) =>
          api.teams.replaceMembers(nextTeamName, nextRequest),
        launchTeam,
      });
      await Promise.all([fetchTeams(), selectTeam(teamName)]);
    },
    [data?.isAlive, fetchTeams, launchTeam, selectTeam, teamName]
  );

  const handleRestartTeamFromEdit = useCallback(async (): Promise<void> => {
    if (!data?.config.projectPath) {
      throw new Error('团队缺少项目路径，无法自动重启。');
    }
    await api.teams.stop(teamName);
    await launchTeam({
      teamName,
      cwd: data.config.projectPath,
    });
    await Promise.all([fetchTeams(), selectTeam(teamName)]);
  }, [data?.config.projectPath, fetchTeams, launchTeam, selectTeam, teamName]);

  const handleSaveAndRestartFromEdit = useCallback(
    async (runtimeConfig: {
      providerId: TeamProviderId;
      model: string | undefined;
      effort: EffortLevel | undefined;
      fastMode: TeamFastMode | undefined;
      clearContext: boolean;
    }): Promise<void> => {
      if (!data?.config.projectPath) {
        throw new Error('团队缺少项目路径，无法自动重启。');
      }
      await api.teams.stop(teamName);
      const request: TeamLaunchRequest = {
        teamName,
        cwd: data.config.projectPath,
        providerId: runtimeConfig.providerId,
        model: runtimeConfig.model,
        effort: runtimeConfig.effort,
        fastMode: runtimeConfig.fastMode,
        clearContext: runtimeConfig.clearContext,
      };
      await launchTeam(request);
    },
    [data?.config.projectPath, launchTeam, teamName]
  );

  const handleRestartMember = useCallback(
    async (memberName: string): Promise<void> => {
      await restartMember(teamName, memberName);
    },
    [restartMember, teamName]
  );

  const handleSkipMemberForLaunch = useCallback(
    async (memberName: string): Promise<void> => {
      await skipMemberForLaunch(teamName, memberName);
    },
    [skipMemberForLaunch, teamName]
  );

  const handleSelectMember = useCallback((member: ResolvedTeamMember) => {
    setSelectedMember(member);
    setSelectedMemberView(null);
  }, []);

  const closeSelectedMemberDialog = useCallback(() => {
    setSelectedMember(null);
    setSelectedMemberView(null);
  }, []);

  const handleSendMessageToMember = useCallback((member: ResolvedTeamMember) => {
    setSendDialogRecipient(member.name);
    setSendDialogDefaultText(undefined);
    setSendDialogDefaultChip(undefined);
    setReplyQuote(undefined);
    setSendDialogOpen(true);
  }, []);

  const handleAssignTaskToMember = useCallback(
    (member: ResolvedTeamMember) => {
      openCreateTaskDialog('', '', member.name);
    },
    [openCreateTaskDialog]
  );

  const handleOpenTaskById = useCallback((taskId: string) => {
    const task = taskMapRef.current.get(taskId);
    if (task) {
      setSelectedTask(task);
    }
  }, []);

  const handleOpenTask = useCallback((task: TeamTaskWithKanban) => {
    setSelectedTask(task);
  }, []);

  const handleTaskIdClick = useCallback(
    (taskId: string) => {
      const task =
        taskMap.get(taskId) ?? data?.tasks.find((candidate) => candidate.displayId === taskId);
      if (task) setSelectedTask(task);
    },
    [taskMap, data?.tasks]
  );

  const handleEditorAction = useCallback(
    (action: EditorSelectionAction) => {
      const chip = createChipFromSelection(action, []) ?? undefined;
      if (action.type === 'sendMessage') {
        setSendDialogDefaultText(chip ? undefined : action.formattedContext);
        setSendDialogDefaultChip(chip);
        setSendDialogRecipient(undefined);
        setReplyQuote(undefined);
        setSendDialogOpen(true);
      } else if (action.type === 'createTask') {
        if (chip) {
          setCreateTaskDialog({
            open: true,
            defaultSubject: '',
            defaultDescription: '',
            defaultOwner: '',
            defaultStartImmediately: undefined,
            defaultChip: chip,
          });
        } else {
          openCreateTaskDialog('', action.formattedContext);
        }
      }
    },

    []
  );

  // Pick up pending review request from GlobalTaskDetailDialog
  useEffect(() => {
    if (!pendingReviewRequest) return;
    setReviewDialogState({
      open: true,
      mode: 'task',
      taskId: pendingReviewRequest.taskId,
      initialFilePath: pendingReviewRequest.filePath,
      taskChangeRequestOptions: pendingReviewRequest.requestOptions,
    });
    if (pendingReviewRequest.filePath) {
      selectReviewFile(pendingReviewRequest.filePath);
    }
    setPendingReviewRequest(null);
  }, [pendingReviewRequest, selectReviewFile, setPendingReviewRequest]);

  // Pick up pending member profile request from MemberHoverCard
  const pendingMemberProfile = useStore((s) => s.pendingMemberProfile);
  useEffect(() => {
    if (!pendingMemberProfile || !data) return;
    const member = membersWithLiveBranches.find((m) => m.name === pendingMemberProfile);
    if (member) {
      setSelectedMember(member);
      setSelectedMemberView(null);
    }
    useStore.getState().closeMemberProfile();
  }, [pendingMemberProfile, membersWithLiveBranches]);

  const handleDeleteTask = useCallback(
    (taskId: string) => {
      void (async () => {
        const scheduleId = getScheduleIdFromBoardTaskId(taskId);
        const isScheduleTask = scheduleId !== null;
        const title = isScheduleTask ? '删除定时任务' : '删除任务';
        const message = isScheduleTask
          ? '将该定时任务从看板中删除（会同时删除计划）？'
          : `将任务 #${deriveTaskDisplayId(taskId)} 移入废纸篓？`;
        const confirmed = await confirm({
          title,
          message,
          confirmLabel: '删除',
          cancelLabel: '取消',
          variant: 'danger',
        });
        if (confirmed) {
          try {
            if (scheduleId) {
              await removeSchedule(scheduleId);
            } else {
              await softDeleteTask(teamName, taskId);
            }
          } catch {
            // error via store
          }
        }
      })();
    },
    [teamName, softDeleteTask, removeSchedule]
  );

  const handleViewChanges = useCallback(
    (taskId: string) => {
      const task = taskMap.get(taskId);
      setReviewDialogState({
        open: true,
        mode: 'task',
        taskId,
        taskChangeRequestOptions: task ? buildTaskChangeRequestOptions(task) : {},
      });
    },
    [taskMap]
  );

  const handleViewChangesForFile = useCallback(
    (taskId: string, filePath?: string) => {
      const task = taskMap.get(taskId);
      setReviewDialogState({
        open: true,
        mode: 'task',
        taskId,
        initialFilePath: filePath,
        taskChangeRequestOptions: task ? buildTaskChangeRequestOptions(task) : {},
      });
      if (filePath) {
        selectReviewFile(filePath);
      }
    },
    [selectReviewFile, taskMap]
  );

  const handleDeleteTeam = useCallback((): void => {
    setDeleteConfirmOpen(true);
  }, []);

  const confirmDeleteTeam = useCallback((): void => {
    setDeleteConfirmOpen(false);
    void (async () => {
      try {
        await deleteTeam(teamName);
        if (tabId) closeTab(tabId);
        openTeamsTab();
      } catch {
        // error is shown via store
      }
    })();
  }, [teamName, deleteTeam, openTeamsTab, closeTab, tabId]);

  const handleCreateTask = (
    subject: string,
    description: string,
    owner?: string,
    blockedBy?: string[],
    related?: string[],
    prompt?: string,
    startImmediately?: boolean,
    descriptionTaskRefs?: TaskRef[],
    promptTaskRefs?: TaskRef[]
  ): void => {
    setCreatingTask(true);
    void (async () => {
      try {
        await createTeamTask(teamName, {
          subject,
          description: description || undefined,
          owner,
          blockedBy,
          related,
          prompt,
          descriptionTaskRefs,
          promptTaskRefs,
          startImmediately,
        });

        if (prompt && owner && data?.isAlive && !isTeamProvisioning && startImmediately !== false) {
          const msg = `New task assigned to ${owner}: "${subject}". Instructions:\n${prompt}`;
          try {
            await api.teams.processSend(teamName, msg);
          } catch {
            // best-effort
          }
        }

        closeCreateTaskDialog();
      } catch {
        // error shown via store
      } finally {
        setCreatingTask(false);
      }
    })();
  };

  const sharedMessagesPanelProps = useMemo<SharedTeamMessagesPanelProps>(
    () => ({
      teamName,
      onPositionChange: keepMessagesInline,
      mountPoint: null,
      members: activeMembers,
      tasks: data?.tasks ?? [],
      isTeamAlive: data?.isAlive,
      timeWindow,
      teamSessionIds,
      currentLeadSessionId: data?.config.leadSessionId,
      pendingRepliesByMember,
      onPendingReplyChange: setPendingRepliesByMember,
      onMemberClick: handleSelectMember,
      onTaskClick: handleOpenTask,
      onCreateTaskFromMessage: handleCreateTaskFromMessage,
      onReplyToMessage: handleReplyToMessage,
      onRestartTeam: handleRestartTeam,
      onTaskIdClick: handleTaskIdClick,
      inlineScrollContainerRef: contentRef,
      showPositionControls: false,
    }),
    [
      activeMembers,
      data?.config.leadSessionId,
      data?.isAlive,
      data?.tasks,
      handleCreateTaskFromMessage,
      handleOpenTask,
      handleReplyToMessage,
      handleRestartTeam,
      handleSelectMember,
      handleTaskIdClick,
      pendingRepliesByMember,
      teamName,
      teamSessionIds,
      timeWindow,
      keepMessagesInline,
    ]
  );

  if (!teamName) {
    return (
      <div className="flex size-full items-center justify-center p-6 text-sm text-red-400">
        Invalid team tab
      </div>
    );
  }

  const spawnStatusWatcher = (
    <TeamSpawnStatusWatcher
      teamName={teamName}
      isTeamProvisioning={isTeamProvisioning}
      isTeamAlive={data?.isAlive}
    />
  );
  const teamAgentRuntimeWatcher = (
    <TeamAgentRuntimeWatcher
      teamName={teamName}
      isTeamProvisioning={isTeamProvisioning}
      isTeamAlive={data?.isAlive}
      isThisTabActive={isThisTabActive}
    />
  );
  const leadContextWatcher = (
    <LeadContextWatcher
      teamName={teamName}
      tabId={tabId}
      projectId={projectId}
      leadSessionId={leadSessionId}
      sessionHistoryKey={sessionHistoryKey}
      isThisTabActive={isThisTabActive}
      isTeamAlive={data?.isAlive}
      sessions={sessions}
      sessionsLoading={sessionsLoading}
    />
  );

  const renderBody = (): React.JSX.Element => {
    if ((loading && !data) || (data && data.teamName !== teamName)) {
      return (
        <div className="size-full overflow-auto p-4">
          <div className="mb-4 h-10 animate-pulse rounded-md bg-[var(--color-surface-raised)]" />
          <div ref={provisioningBannerRef}>
            <TeamProvisioningBanner teamName={teamName} />
          </div>
          <div className="space-y-3">
            <div className="h-24 animate-pulse rounded-md bg-[var(--color-surface-raised)]" />
            <div className="h-48 animate-pulse rounded-md bg-[var(--color-surface-raised)]" />
            <div className="h-48 animate-pulse rounded-md bg-[var(--color-surface-raised)]" />
          </div>
        </div>
      );
    }

    if (error === 'TEAM_DRAFT') {
      const draftTeamSummary = useStore.getState().teamByName[teamName];
      const draftDisplayName = draftTeamSummary?.displayName || teamName;

      return (
        <>
          <div className="size-full overflow-auto p-6">
            <div ref={provisioningBannerRef}>
              <TeamProvisioningBanner teamName={teamName} />
            </div>
            <div className="flex min-h-[calc(100vh-12rem)] items-center justify-center">
              <div className="max-w-md text-center">
                <p className="text-sm font-medium text-text">团队尚未启动</p>
                <p className="mt-2 text-xs text-text-secondary">
                  这是一个草稿团队。<strong>{draftDisplayName}</strong>{' '}
                  尚未完成启动，点击“启动团队”后即可选择模型并进入动态编排。
                </p>
                <div className="mt-4 flex justify-center gap-2">
                  <button
                    className="rounded-md bg-blue-600 px-4 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-500"
                    onClick={() => openLaunchDialog('launch')}
                  >
                    启动团队
                  </button>
                  <button
                    className="rounded-md bg-surface-raised px-4 py-1.5 text-xs font-medium text-text-secondary transition-colors hover:text-text"
                    onClick={() => {
                      void api.teams.deleteDraft(teamName).catch(() => {});
                    }}
                  >
                    删除
                  </button>
                </div>
              </div>
            </div>
          </div>
          <LaunchTeamDialog
            mode={launchDialogState.mode}
            open={launchDialogOpen}
            teamName={teamName}
            members={[]}
            defaultProjectPath={draftTeamSummary?.projectPath}
            provisioningError={provisioningError}
            clearProvisioningError={clearProvisioningError}
            onClose={closeLaunchDialog}
            onLaunch={handleLaunchDialogSubmit}
            onRelaunch={handleRelaunchDialogSubmit}
          />
        </>
      );
    }

    if (error) {
      return (
        <div className="flex size-full items-center justify-center p-6">
          <div className="text-center">
            <p className="text-sm font-medium text-red-400">团队加载失败</p>
            <p className="mt-2 text-xs text-[var(--color-text-muted)]">{error}</p>
            <div className="mt-4">
              <Button
                size="sm"
                variant="outline"
                onClick={() => void selectTeam(teamName, { allowReloadWhileProvisioning: true })}
              >
                重试加载
              </Button>
            </div>
          </div>
        </div>
      );
    }

    if (!data) {
      return (
        <div className="size-full overflow-auto p-4">
          <div ref={provisioningBannerRef}>
            <TeamProvisioningBanner teamName={teamName} />
          </div>
          <div className="flex flex-1 items-center justify-center p-6 text-sm text-[var(--color-text-muted)]">
            编排完成后，这里将显示团队数据
          </div>
        </div>
      );
    }

    const headerColorSet = data.config.color
      ? getTeamColorSet(data.config.color)
      : nameColorSet(data.config.name);
    const rawTeamSettings = (data.settings ?? {}) as Record<string, unknown>;
    const currentManagedSources =
      data.config.managedSources ??
      (typeof rawTeamSettings.admin_from === 'string' ? rawTeamSettings.admin_from : '*');
    const currentDisabledCommands =
      data.config.disabledCommands ??
      (Array.isArray(rawTeamSettings.disabled_commands)
        ? rawTeamSettings.disabled_commands.filter(
            (entry): entry is string => typeof entry === 'string' && entry.trim().length > 0
          )
        : []);
    const currentPlatformAllowFrom =
      data.config.platformAllowFrom ??
      (typeof rawTeamSettings.platform_allow_from === 'object' &&
      rawTeamSettings.platform_allow_from !== null &&
      !Array.isArray(rawTeamSettings.platform_allow_from)
        ? (rawTeamSettings.platform_allow_from as Record<string, string>)
        : {});

    return (
      <>
        <div className="flex size-full overflow-hidden">
          <LeadContextBridge
            teamName={teamName}
            tabId={tabId}
            projectId={projectId}
            leadSessionId={leadSessionId}
            fallbackProjectRoot={data.config.projectPath}
          />

          <div className="relative min-h-0 min-w-0 flex-1">
            <div
              ref={contentRef}
              className="size-full min-w-0 overflow-y-auto overflow-x-hidden p-4"
              data-team-name={teamName}
            >
              <div className="relative -mx-4 -mt-4 mb-3 overflow-hidden border-b border-[var(--color-border)] px-4 py-3">
                {headerColorSet ? (
                  <div
                    className="pointer-events-none absolute inset-0 z-0"
                    style={{ backgroundColor: getThemedBadge(headerColorSet, isLight) }}
                  />
                ) : null}
                <div
                  className={cn(
                    'flex items-start justify-between gap-2',
                    headerColorSet && 'relative z-10'
                  )}
                >
                  <div className="min-w-0 flex-1">
                    <div className="flex items-center gap-2">
                      <h2 className="text-base font-semibold text-[var(--color-text)]">
                        {data.config.name}
                      </h2>
                      {data.isAlive && (
                        <span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-medium text-emerald-400">
                          <span className="size-1.5 rounded-full bg-emerald-400" />
                          运行中
                        </span>
                      )}
                      {!data.isAlive && isTeamProvisioning && (
                        <span className="inline-flex items-center gap-1 rounded-full bg-yellow-500/15 px-1.5 py-0.5 text-[10px] font-medium text-yellow-400">
                          <span className="size-1.5 animate-pulse rounded-full bg-yellow-400" />
                          启动中...
                        </span>
                      )}
                    </div>
                  </div>
                  <div className="flex shrink-0 items-center gap-1.5">
                    <Tooltip>
                      <TooltipTrigger asChild>
                        <Button
                          variant="ghost"
                          size="sm"
                          className="h-7 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
                          disabled={isTeamProvisioning}
                          onClick={() => setEditDialogOpen(true)}
                        >
                          <Pencil size={12} />
                        </Button>
                      </TooltipTrigger>
                      <TooltipContent side="bottom">
                        {isTeamProvisioning ? '团队仍在编排中，暂时无法编辑' : '编辑团队'}
                      </TooltipContent>
                    </Tooltip>
                    <Tooltip>
                      <TooltipTrigger asChild>
                        <Button
                          variant="ghost"
                          size="sm"
                          className="h-7 gap-1 px-2 text-xs text-red-400 hover:bg-red-500/10 hover:text-red-300"
                          onClick={handleDeleteTeam}
                        >
                          <Trash2 size={12} />
                        </Button>
                      </TooltipTrigger>
                      <TooltipContent side="bottom">删除团队</TooltipContent>
                    </Tooltip>
                  </div>
                </div>
                {data.config.description && (
                  <p
                    className={cn(
                      'min-w-0 truncate text-xs text-[var(--color-text-muted)]',
                      headerColorSet && 'relative z-10'
                    )}
                  >
                    {data.config.description}
                  </p>
                )}
                <div
                  className={cn(
                    'mt-1 flex items-start justify-between gap-3',
                    headerColorSet && 'relative z-10'
                  )}
                >
                  <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-0.5">
                    {data.config.projectPath && (
                      <span className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]">
                        <FolderOpen size={11} className="shrink-0 text-[var(--color-text-muted)]" />
                        <Tooltip>
                          <TooltipTrigger asChild>
                            <span className="max-w-60 truncate font-mono">
                              {data.config.projectPath
                                .replace(/\\/g, '/')
                                .split('/')
                                .filter(Boolean)
                                .pop() ?? data.config.projectPath}
                            </span>
                          </TooltipTrigger>
                          <TooltipContent side="bottom">
                            <span className="font-mono text-xs">
                              {formatProjectPath(data.config.projectPath)}
                            </span>
                          </TooltipContent>
                        </Tooltip>
                      </span>
                    )}
                    {leadBranch && (
                      <span
                        className="flex items-center gap-1 text-[11px] text-[var(--color-text-secondary)]"
                        title={leadBranch}
                      >
                        <GitBranch size={11} className="shrink-0 text-[var(--color-text-muted)]" />
                        <span className="max-w-32 truncate">{leadBranch}</span>
                      </span>
                    )}
                  </div>
                </div>
                {(() => {
                  const currentPath = data.config.projectPath;
                  const history = data.config.projectPathHistory?.filter((p) => p !== currentPath);
                  if (!history || history.length === 0) return null;
                  return (
                    <div
                      className={cn(
                        'mt-0.5 flex items-center gap-1 text-[10px] text-[var(--color-text-muted)]',
                        headerColorSet && 'relative z-10'
                      )}
                    >
                      <History size={10} className="shrink-0" />
                      <span className="truncate">
                        历史路径：{history.map((p) => formatProjectPath(p)).join(', ')}
                      </span>
                    </div>
                  );
                })()}
              </div>

              {!data.isAlive && !isTeamProvisioning ? (
                <TeamOfflineStatusBanner
                  teamName={teamName}
                  onLaunch={() => openLaunchDialog('launch')}
                />
              ) : null}

              <div ref={provisioningBannerRef}>
                <TeamProvisioningBanner teamName={teamName} />
              </div>

              {data.warnings?.some((warning) => warning.toLowerCase().includes('kanban')) ? (
                <div className="mb-3 rounded-md border border-[var(--step-warning-border)] bg-[var(--step-warning-bg)] px-3 py-2 text-xs text-[var(--step-warning-text)]">
                  看板未完整加载，当前展示的是安全回退数据。
                </div>
              ) : null}
              {reviewActionError ? (
                <div className="mb-3 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2 text-xs text-[var(--step-error-text)]">
                  {reviewActionError}
                </div>
              ) : null}

              <CollapsibleTeamSection
                sectionId="team"
                title="团队"
                icon={<Users size={14} />}
                badge={activeTeammateCount === 0 ? '单人' : activeTeammateCount}
                defaultOpen
              >
                <TeamMemberListBridge
                  teamName={teamName}
                  members={membersWithLiveBranches}
                  memberTaskCounts={memberTaskCounts}
                  taskMap={taskMap}
                  pendingRepliesByMember={pendingRepliesByMember}
                  isTeamAlive={data.isAlive}
                  isTeamProvisioning={isTeamProvisioning}
                  launchParams={launchParams}
                  onMemberClick={handleSelectMember}
                  onSendMessage={handleSendMessageToMember}
                  onAssignTask={handleAssignTaskToMember}
                  onOpenTask={handleOpenTaskById}
                  onRestartMember={handleRestartMember}
                  onSkipMemberForLaunch={handleSkipMemberForLaunch}
                />
              </CollapsibleTeamSection>

              <CollapsibleTeamSection
                sectionId="kanban"
                title="任务"
                icon={<Columns3 size={14} />}
                badge={filteredTasks.length}
                defaultOpen
                forceOpen={kanbanSearch.trim().length > 0}
                action={
                  <Button
                    variant="ghost"
                    size="sm"
                    className="h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
                    onClick={(e) => {
                      e.stopPropagation();
                      openCreateTaskDialog();
                    }}
                  >
                    <Plus size={12} />
                    任务
                  </Button>
                }
              >
                <div className="min-w-0">
                  <KanbanBoard
                    tasks={kanbanDisplayTasks}
                    teamName={teamName}
                    kanbanState={data.kanbanState}
                    filter={kanbanFilter}
                    sort={kanbanSort}
                    sessions={teamSessions}
                    leadSessionId={data.config.leadSessionId}
                    members={activeMembers}
                    onFilterChange={setKanbanFilter}
                    onSortChange={setKanbanSort}
                    toolbarLeft={
                      <KanbanSearchInput
                        value={kanbanSearch}
                        onChange={setKanbanSearch}
                        tasks={filteredTasks}
                        members={activeMembers}
                      />
                    }
                    onRequestReview={(taskId) => {
                      void (async () => {
                        try {
                          await requestReview(teamName, taskId);
                        } catch {
                          // error via store
                        }
                      })();
                    }}
                    onApprove={(taskId) => {
                      void (async () => {
                        try {
                          await updateKanban(teamName, taskId, {
                            op: 'set_column',
                            column: 'approved',
                          });
                        } catch {
                          // error via store
                        }
                      })();
                    }}
                    onRequestChanges={(taskId) => {
                      setRequestChangesTaskId(taskId);
                    }}
                    onMoveBackToDone={(taskId) => {
                      void (async () => {
                        try {
                          await updateKanban(teamName, taskId, { op: 'remove' });
                          await updateTaskStatus(teamName, taskId, 'completed');
                        } catch {
                          // error via store
                        }
                      })();
                    }}
                    onStartTask={(taskId) => {
                      void (async () => {
                        const scheduleId = getScheduleIdFromBoardTaskId(taskId);
                        if (scheduleId) {
                          try {
                            await triggerScheduleNow(scheduleId);
                            await fetchRunHistory(scheduleId);
                          } catch {
                            // error via store
                          }
                          return;
                        }
                        try {
                          const result = await startTaskByUser(teamName, taskId);
                          if (data?.isAlive) {
                            const task = data.tasks.find((t) => t.id === taskId);
                            try {
                              if (result.notifiedOwner && task?.owner) {
                                await api.teams.processSend(
                                  teamName,
                                  `Task ${formatTaskDisplayLabel(task)} "${task.subject}" has started. Please begin working on it.`
                                );
                              } else if (!result.notifiedOwner) {
                                const desc = task?.description?.trim()
                                  ? `\nDescription: ${task.description.trim()}`
                                  : '';
                                await api.teams.processSend(
                                  teamName,
                                  `Task #${deriveTaskDisplayId(taskId)} "${task?.subject ?? ''}" has been moved to IN PROGRESS but has no assignee.${desc}\nPlease assign it to an available team member, or take it yourself if everyone is busy.`
                                );
                              }
                            } catch {
                              // best-effort
                            }
                          }
                        } catch {
                          // error via store
                        }
                      })();
                    }}
                    onCompleteTask={(taskId) => {
                      void (async () => {
                        if (isScheduleBoardTaskId(taskId)) {
                          return;
                        }
                        try {
                          await updateTaskStatus(teamName, taskId, 'completed');
                        } catch {
                          // error via store
                        }
                      })();
                    }}
                    onCancelTask={(taskId) => {
                      void (async () => {
                        if (isScheduleBoardTaskId(taskId)) {
                          return;
                        }
                        try {
                          const task = data?.tasks.find((t) => t.id === taskId);
                          await updateTaskStatus(teamName, taskId, 'pending');

                          // Notify assignee directly via inbox — they'll see it immediately
                          if (task?.owner) {
                            try {
                              await api.teams.sendMessage(teamName, {
                                member: task.owner,
                                text: `Task ${formatTaskDisplayLabel(task)} "${task.subject}" has been CANCELLED by the user and moved back to TODO. Stop working on it immediately.`,
                                summary: `Task ${formatTaskDisplayLabel(task)} cancelled`,
                              });
                            } catch {
                              // best-effort
                            }
                          }

                          // Also notify team lead so they can reassign/coordinate
                          if (data?.isAlive) {
                            try {
                              const ownerSuffix = task?.owner
                                ? ` ${task.owner} has been notified to stop.`
                                : '';
                              await api.teams.processSend(
                                teamName,
                                `Task #${deriveTaskDisplayId(taskId)} "${task?.subject ?? ''}" has been cancelled and moved back to TODO.${ownerSuffix}`
                              );
                            } catch {
                              // best-effort
                            }
                          }
                        } catch {
                          // error via store
                        }
                      })();
                    }}
                    onColumnOrderChange={(columnId, orderedTaskIds) => {
                      void (async () => {
                        try {
                          await updateKanbanColumnOrder(teamName, columnId, orderedTaskIds);
                        } catch {
                          // error via store
                        }
                      })();
                    }}
                    onScrollToTask={(taskId) => {
                      const el = document.querySelector(`[data-task-id="${taskId}"]`);
                      if (el) {
                        el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                        el.classList.remove('kanban-card-focus-pulse');
                        void (el as HTMLElement).offsetWidth;
                        el.classList.add('kanban-card-focus-pulse');
                        el.addEventListener(
                          'animationend',
                          () => el.classList.remove('kanban-card-focus-pulse'),
                          { once: true }
                        );
                      }
                    }}
                    onTaskClick={(task) => {
                      if (isScheduleBoardTaskId(task.id)) {
                        return;
                      }
                      setSelectedTask(task);
                    }}
                    onViewChanges={handleViewChanges}
                    onAddTask={(startImmediately) =>
                      openCreateTaskDialog('', '', '', startImmediately)
                    }
                    onDeleteTask={handleDeleteTask}
                    deletedTaskCount={deletedTasks.length}
                    onOpenTrash={() => setTrashOpen(true)}
                  />
                </div>
              </CollapsibleTeamSection>

              <TeamMessagesPanelBridge position="inline" {...sharedMessagesPanelProps} />

              {(data.processes?.length ?? 0) > 0 && (
                <CollapsibleTeamSection
                  sectionId="processes"
                  title="CLI 进程"
                  icon={<Terminal size={14} />}
                  badge={data.processes.filter((p) => !p.stoppedAt).length}
                  headerExtra={
                    data.processes.some((p) => !p.stoppedAt) ? (
                      <span
                        className="pointer-events-none relative inline-flex size-2 shrink-0"
                        title="活跃中"
                      >
                        <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-50" />
                        <span className="relative inline-flex size-2 rounded-full bg-emerald-400" />
                      </span>
                    ) : null
                  }
                  defaultOpen
                >
                  <ProcessesSection
                    teamName={teamName}
                    members={membersWithLiveBranches}
                    processes={data.processes}
                  />
                </CollapsibleTeamSection>
              )}

              <ReviewDialog
                open={requestChangesTaskId !== null}
                teamName={teamName}
                taskId={requestChangesTaskId}
                members={members}
                onCancel={() => setRequestChangesTaskId(null)}
                onSubmit={(comment, taskRefs) => {
                  if (!requestChangesTaskId) {
                    return;
                  }
                  void (async () => {
                    try {
                      await updateKanban(teamName, requestChangesTaskId, {
                        op: 'request_changes',
                        comment,
                        taskRefs,
                      });
                      setRequestChangesTaskId(null);
                    } catch {
                      // error state is handled in the store and shown in the view
                    }
                  })();
                }}
              />

              <TeamMemberDetailDialogBridge
                open={selectedMember !== null}
                member={selectedMember}
                teamName={teamName}
                members={membersWithLiveBranches}
                tasks={data.tasks}
                initialTab={selectedMemberView?.initialTab}
                initialActivityFilter={selectedMemberView?.initialActivityFilter}
                isTeamAlive={data.isAlive}
                isTeamProvisioning={isTeamProvisioning}
                launchParams={launchParams}
                onClose={closeSelectedMemberDialog}
                onSendMessage={() => {
                  const name = selectedMember?.name ?? '';
                  closeSelectedMemberDialog();
                  setSendDialogRecipient(name || undefined);
                  setSendDialogDefaultText(undefined);
                  setSendDialogDefaultChip(undefined);
                  setReplyQuote(undefined);
                  setSendDialogOpen(true);
                }}
                onAssignTask={() => {
                  const name = selectedMember?.name ?? '';
                  closeSelectedMemberDialog();
                  openCreateTaskDialog('', '', name);
                }}
                onRestartMember={handleRestartMember}
                onTaskClick={(task) => {
                  closeSelectedMemberDialog();
                  setSelectedTask(task);
                }}
                onUpdateRole={async (memberName, role) => {
                  setUpdatingRoleLoading(true);
                  try {
                    await updateMemberRole(teamName, memberName, role);
                    // Optimistically update local selectedMember to reflect new role
                    setSelectedMember((prev) => {
                      if (prev?.name !== memberName) return prev;
                      const normalized =
                        typeof role === 'string' && role.trim() ? role.trim() : undefined;
                      return { ...prev, role: normalized };
                    });
                  } finally {
                    setUpdatingRoleLoading(false);
                  }
                }}
                updatingRole={updatingRoleLoading}
                onRemoveMember={() => {
                  const name = selectedMember?.name;
                  if (!name) return;
                  setRemoveMemberConfirm(name);
                }}
                onViewMemberChanges={(memberName, filePath) => {
                  closeSelectedMemberDialog();
                  setReviewDialogState({
                    open: true,
                    mode: 'agent',
                    memberName,
                    initialFilePath: filePath,
                  });
                }}
              />

              <CreateTaskDialog
                open={createTaskDialog.open}
                teamName={teamName}
                members={activeMembers}
                tasks={data.tasks}
                isTeamAlive={data.isAlive && !isTeamProvisioning}
                defaultSubject={createTaskDialog.defaultSubject}
                defaultDescription={createTaskDialog.defaultDescription}
                defaultOwner={createTaskDialog.defaultOwner}
                defaultStartImmediately={createTaskDialog.defaultStartImmediately}
                defaultChip={createTaskDialog.defaultChip}
                onClose={closeCreateTaskDialog}
                onSubmit={handleCreateTask}
                submitting={creatingTask}
              />

              <EditTeamDialog
                open={editDialogOpen}
                teamName={teamName}
                currentName={data.config.name}
                currentDescription={data.config.description ?? ''}
                currentColor={data.config.color ?? ''}
                currentAgentType={data.config.agentType ?? data.harness ?? 'cursor'}
                currentWorkDir={data.workDir ?? data.config.projectPath ?? ''}
                currentPermissionMode={
                  data.config.permissionMode ?? data.permissionMode ?? 'default'
                }
                currentLanguage={
                  data.config.language ??
                  (typeof rawTeamSettings.language === 'string' ? rawTeamSettings.language : 'zh')
                }
                currentShowContextIndicator={
                  data.config.showContextIndicator ??
                  (typeof rawTeamSettings.show_context_indicator === 'boolean'
                    ? rawTeamSettings.show_context_indicator
                    : true)
                }
                currentReplyFooter={
                  data.config.replyFooter ??
                  (typeof rawTeamSettings.reply_footer === 'boolean'
                    ? rawTeamSettings.reply_footer
                    : true)
                }
                currentInjectSender={
                  data.config.injectSender ??
                  (typeof rawTeamSettings.inject_sender === 'boolean'
                    ? rawTeamSettings.inject_sender
                    : false)
                }
                currentManagedSources={currentManagedSources}
                currentDisabledCommands={currentDisabledCommands}
                currentPlatformAllowFrom={currentPlatformAllowFrom}
                currentMembers={membersWithLiveBranches.filter((m) => !isLeadMember(m))}
                leadMember={membersWithLiveBranches.find((m) => isLeadMember(m)) ?? null}
                resolvedMemberColorMap={resolvedMemberColorMap}
                isTeamAlive={data.isAlive && !isTeamProvisioning}
                isTeamProvisioning={isTeamProvisioning}
                projectPath={data.config.projectPath}
                savedLaunchRequest={savedLaunchRequest}
                onClose={() => setEditDialogOpen(false)}
                onSaved={() => {
                  void fetchTeams();
                  void selectTeam(teamName);
                }}
                onDeleteTeam={handleDeleteTeam}
                onRestartTeam={handleRestartTeamFromEdit}
                onSaveAndRestart={handleSaveAndRestartFromEdit}
              />

              <Dialog
                open={removeMemberConfirm !== null}
                onOpenChange={(open) => {
                  if (!open) setRemoveMemberConfirm(null);
                }}
              >
                <DialogContent className="max-w-sm">
                  <DialogHeader>
                    <DialogTitle>移除成员</DialogTitle>
                    <DialogDescription>
                      确认将 &ldquo;{removeMemberConfirm}&rdquo; 从团队中移除？任务与消息会保留，
                      但该名称将无法再次使用。
                    </DialogDescription>
                  </DialogHeader>
                  <DialogFooter>
                    <Button variant="ghost" size="sm" onClick={() => setRemoveMemberConfirm(null)}>
                      取消
                    </Button>
                    <Button
                      variant="destructive"
                      size="sm"
                      onClick={() => {
                        const name = removeMemberConfirm;
                        setRemoveMemberConfirm(null);
                        closeSelectedMemberDialog();
                        if (name) void removeMember(teamName, name);
                      }}
                    >
                      移除
                    </Button>
                  </DialogFooter>
                </DialogContent>
              </Dialog>

              <Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
                <DialogContent className="max-w-sm">
                  <DialogHeader>
                    <DialogTitle>删除团队</DialogTitle>
                    <DialogDescription>
                      确认删除团队 &ldquo;{data.config.name}
                      &rdquo;？此操作不可恢复，所有团队数据与任务都将被删除。
                    </DialogDescription>
                  </DialogHeader>
                  <DialogFooter>
                    <Button variant="ghost" size="sm" onClick={() => setDeleteConfirmOpen(false)}>
                      取消
                    </Button>
                    <Button variant="destructive" size="sm" onClick={confirmDeleteTeam}>
                      删除
                    </Button>
                  </DialogFooter>
                </DialogContent>
              </Dialog>

              <LaunchTeamDialog
                mode={launchDialogState.mode}
                open={launchDialogOpen}
                teamName={teamName}
                members={membersWithLiveBranches}
                defaultProjectPath={data.config.projectPath}
                projectPath={data.config.projectPath}
                provisioningError={provisioningError}
                clearProvisioningError={clearProvisioningError}
                activeTeams={activeTeamsForLaunch}
                onClose={closeLaunchDialog}
                onLaunch={handleLaunchDialogSubmit}
                onRelaunch={handleRelaunchDialogSubmit}
              />

              <SendMessageDialog
                open={sendDialogOpen}
                teamName={teamName}
                members={activeMembers}
                defaultRecipient={sendDialogRecipient}
                defaultText={sendDialogDefaultText}
                defaultChip={sendDialogDefaultChip}
                quotedMessage={replyQuote}
                isTeamAlive={data.isAlive}
                sending={sendingMessage}
                sendError={sendMessageError}
                sendWarning={sendMessageWarning}
                sendDebugDetails={sendMessageDebugDetails}
                lastResult={lastSendMessageResult}
                onSend={async (member, text, summary, attachments, actionMode, taskRefs) => {
                  const sentAtMs = Date.now();
                  setPendingRepliesByMember((prev) => ({ ...prev, [member]: sentAtMs }));
                  try {
                    const result = await sendTeamMessage(teamName, {
                      member,
                      text,
                      summary,
                      attachments,
                      actionMode,
                      taskRefs,
                    });
                    if (
                      result?.runtimeDelivery?.attempted === true &&
                      result.runtimeDelivery.delivered === false
                    ) {
                      setPendingRepliesByMember((prev) => {
                        if (prev[member] !== sentAtMs) return prev;
                        const next = { ...prev };
                        delete next[member];
                        return next;
                      });
                    }
                    return result;
                  } catch (error) {
                    setPendingRepliesByMember((prev) => {
                      if (prev[member] !== sentAtMs) return prev;
                      const next = { ...prev };
                      delete next[member];
                      return next;
                    });
                    throw error;
                  }
                }}
                onClose={() => {
                  setSendDialogOpen(false);
                  setReplyQuote(undefined);
                  setSendDialogDefaultText(undefined);
                  setSendDialogDefaultChip(undefined);
                }}
              />

              <TaskDetailDialog
                open={selectedTask !== null}
                task={selectedTask}
                teamName={teamName}
                kanbanTaskState={
                  selectedTask ? data?.kanbanState.tasks[selectedTask.id] : undefined
                }
                taskMap={taskMap}
                members={activeMembers}
                onClose={() => setSelectedTask(null)}
                onScrollToTask={(taskId) => {
                  setSelectedTask(null);
                  const el = document.querySelector(`[data-task-id="${taskId}"]`);
                  if (el) {
                    el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                    el.classList.remove('kanban-card-focus-pulse');
                    void (el as HTMLElement).offsetWidth;
                    el.classList.add('kanban-card-focus-pulse');
                    el.addEventListener(
                      'animationend',
                      () => el.classList.remove('kanban-card-focus-pulse'),
                      { once: true }
                    );
                  }
                }}
                onOwnerChange={(taskId, owner) => {
                  void (async () => {
                    try {
                      await updateTaskOwner(teamName, taskId, owner);
                    } catch {
                      // error via store
                    }
                  })();
                }}
                onViewChanges={handleViewChangesForFile}
                onOpenInEditor={(filePath) => {
                  const { revealFileInEditor } = useStore.getState();
                  revealFileInEditor(filePath);
                }}
                onDeleteTask={handleDeleteTask}
              />

              <TrashDialog
                open={trashOpen}
                tasks={deletedTasks}
                onClose={() => setTrashOpen(false)}
                onRestore={(taskId) => {
                  void (async () => {
                    try {
                      await restoreTask(teamName, taskId);
                    } catch {
                      // error via store
                    }
                  })();
                }}
              />

              <ChangeReviewDialog
                open={reviewDialogState.open}
                onOpenChange={(open) =>
                  setReviewDialogState((prev) => ({
                    ...prev,
                    open,
                    ...(open
                      ? {}
                      : { initialFilePath: undefined, taskChangeRequestOptions: undefined }),
                  }))
                }
                teamName={teamName}
                mode={reviewDialogState.mode}
                memberName={reviewDialogState.memberName}
                taskId={reviewDialogState.taskId}
                initialFilePath={reviewDialogState.initialFilePath}
                taskChangeRequestOptions={reviewDialogState.taskChangeRequestOptions}
                projectPath={data.config.projectPath}
                onEditorAction={handleEditorAction}
              />
            </div>
          </div>
        </div>

        {editorOpen && data.config.projectPath && (
          <Suspense fallback={null}>
            <ProjectEditorOverlay
              projectPath={data.config.projectPath}
              onClose={() => setEditorOpen(false)}
              onEditorAction={handleEditorAction}
            />
          </Suspense>
        )}
      </>
    );
  };

  return (
    <>
      {spawnStatusWatcher}
      {teamAgentRuntimeWatcher}
      {leadContextWatcher}
      {renderBody()}
    </>
  );
};
