import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { Button } from '@renderer/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@renderer/components/ui/dialog';
import { MemberSelect } from '@renderer/components/ui/MemberSelect';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { cn } from '@renderer/lib/utils';
import { useStore } from '@renderer/store';
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
import { isLeadMember } from '@shared/utils/leadDetection';
import { Brain, Expand, MessageSquare, Terminal, Wrench } from 'lucide-react';
import { MemberLogStreamWithLegacyFallback } from './members/MemberLogStreamWithLegacyFallback';
import { ClaudeLogsPanel } from './ClaudeLogsPanel';
import { CollapsibleTeamSection } from './CollapsibleTeamSection';
import {
buildSelectableLogMembers,
formatMemberLogSourceDescription,
formatMemberLogSourceLabel,
getMemberNameFromLogSourceKey,
LEAD_LOG_SOURCE_KEY,
memberLogSourceKey,
normalizeMemberLogSourceName,
resolveLeadLogMember,
} from './teamLogSources';
import { useClaudeLogsController } from './useClaudeLogsController';
import type { TeamLogSourceKey } from './teamLogSources';
import type { LastLogPreview } from './useClaudeLogsController';
import type { ResolvedTeamMember } from '@shared/types';
// =============================================================================
// Constants
// =============================================================================
const PREVIEW_ICONS = {
output: ,
thinking: ,
tool: ,
} as const;
const LogsHeaderSkeletonPill = ({
className,
}: Readonly<{ className?: string }>): React.JSX.Element => (
);
// =============================================================================
// Sub-components
// =============================================================================
interface ClaudeLogsSectionProps {
teamName: string;
position?: 'sidebar' | 'inline';
sidebarViewerMaxHeight?: number;
onOpenChange?: (isOpen: boolean) => void;
}
/**
* Compact inline preview of the most recent log item, shown in the section header.
*/
const LogPreviewInline = ({ preview }: { preview: LastLogPreview }): React.JSX.Element => {
const summaryText =
preview.summary.length > 60 ? preview.summary.slice(0, 60) + '...' : preview.summary;
return (
{PREVIEW_ICONS[preview.type]}
{preview.label}
{summaryText && (
<>
-
{summaryText}
>
)}
);
};
const TeamLogsSourceSelector = ({
leadMember,
members,
selectedKey,
onChange,
className,
triggerVariant = 'default',
}: {
leadMember: ResolvedTeamMember;
members: readonly ResolvedTeamMember[];
selectedKey: TeamLogSourceKey;
onChange: (key: TeamLogSourceKey) => void;
className?: string;
triggerVariant?: 'default' | 'avatar';
}): React.JSX.Element | null => {
const { t } = useAppTranslation('team');
const sourceMembers = useMemo(() => [leadMember, ...members], [leadMember, members]);
const selectedMemberName =
selectedKey === LEAD_LOG_SOURCE_KEY
? leadMember.name
: getMemberNameFromLogSourceKey(selectedKey);
if (sourceMembers.length <= 1) return null;
return (
{
const selectedMember = sourceMembers.find((member) => member.name === memberName);
if (!selectedMember || isLeadMember(selectedMember)) {
onChange(LEAD_LOG_SOURCE_KEY);
return;
}
onChange(memberLogSourceKey(selectedMember.name));
}}
placeholder={t('claudeLogs.sourceSelect.placeholder')}
searchPlaceholder={t('claudeLogs.sourceSelect.searchPlaceholder')}
emptyMessage={t('claudeLogs.sourceSelect.emptyMessage')}
ariaLabel={t('claudeLogs.sourceSelect.ariaLabel')}
triggerVariant={triggerVariant}
getMemberLabel={(member) =>
isLeadMember(member)
? t('claudeLogs.sourceSelect.leadLabel')
: formatMemberLogSourceLabel(member)
}
getMemberDescription={formatMemberLogSourceDescription}
/>
);
};
const MemberLogsSourcePanel = ({
teamName,
member,
enabled,
maxHeight,
}: {
teamName: string;
member: ResolvedTeamMember;
enabled: boolean;
maxHeight?: number;
}): React.JSX.Element => {
const content = (
);
if (maxHeight === undefined) {
return content;
}
return (
{content}
);
};
const TeamLogsDialog = ({
open,
onOpenChange,
teamName,
leadMember,
members,
selectedKey,
onSourceChange,
showingLeadLogs,
ctrl,
selectedMember,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
teamName: string;
leadMember: ResolvedTeamMember;
members: readonly ResolvedTeamMember[];
selectedKey: TeamLogSourceKey;
onSourceChange: (key: TeamLogSourceKey) => void;
showingLeadLogs: boolean;
ctrl: ReturnType;
selectedMember: ResolvedTeamMember | null;
}): React.JSX.Element => {
const { t } = useAppTranslation('team');
const sourceSelector =
members.length > 0 ? (
) : null;
return (
);
};
// =============================================================================
// Main component
// =============================================================================
export const ClaudeLogsSection = memo(function ClaudeLogsSection({
teamName,
position = 'inline',
sidebarViewerMaxHeight,
onOpenChange,
}: ClaudeLogsSectionProps): React.JSX.Element {
const { t } = useAppTranslation('team');
const teamMembers = useStore((state) => selectResolvedMembersForTeamName(state, teamName));
const [selectedSourceState, setSelectedSourceState] = useState<{
teamName: string;
sourceKey: TeamLogSourceKey;
}>(() => ({ teamName, sourceKey: LEAD_LOG_SOURCE_KEY }));
const selectedSourceKey =
selectedSourceState.teamName === teamName ? selectedSourceState.sourceKey : LEAD_LOG_SOURCE_KEY;
const setSelectedSourceKey = useCallback(
(sourceKey: TeamLogSourceKey) => {
setSelectedSourceState({ teamName, sourceKey });
},
[teamName]
);
const leadMember = useMemo(() => resolveLeadLogMember(teamMembers), [teamMembers]);
const selectableMembers = useMemo(() => buildSelectableLogMembers(teamMembers), [teamMembers]);
const selectedMemberName = getMemberNameFromLogSourceKey(selectedSourceKey);
const selectedMemberSourceName = selectedMemberName
? normalizeMemberLogSourceName(selectedMemberName)
: null;
const selectedMember = useMemo(
() =>
selectedMemberSourceName
? (selectableMembers.find(
(member) => normalizeMemberLogSourceName(member.name) === selectedMemberSourceName
) ?? null)
: null,
[selectableMembers, selectedMemberSourceName]
);
const effectiveSelectedSourceKey =
selectedSourceKey === LEAD_LOG_SOURCE_KEY
? LEAD_LOG_SOURCE_KEY
: selectedMember
? memberLogSourceKey(selectedMember.name)
: LEAD_LOG_SOURCE_KEY;
const showingLeadLogs = effectiveSelectedSourceKey === LEAD_LOG_SOURCE_KEY;
const ctrl = useClaudeLogsController(teamName, { enabled: showingLeadLogs });
const [dialogOpen, setDialogOpen] = useState(false);
const isSidebar = position === 'sidebar';
const showHeaderSkeleton =
showingLeadLogs && ctrl.loading && ctrl.data.lines.length === 0 && !ctrl.error;
useEffect(() => {
if (selectedSourceState.teamName !== teamName) {
setSelectedSourceState({ teamName, sourceKey: LEAD_LOG_SOURCE_KEY });
}
}, [selectedSourceState.teamName, teamName]);
useEffect(() => {
if (selectedSourceKey === LEAD_LOG_SOURCE_KEY) return;
if (selectedMember) {
const canonicalSourceKey = memberLogSourceKey(selectedMember.name);
if (selectedSourceKey !== canonicalSourceKey) {
setSelectedSourceKey(canonicalSourceKey);
}
return;
}
setSelectedSourceKey(LEAD_LOG_SOURCE_KEY);
}, [selectedMember, selectedSourceKey, setSelectedSourceKey]);
const sectionHeaderExtra = useMemo(
() => (
{showingLeadLogs && ctrl.online ? (
) : null}
{showingLeadLogs && ctrl.lastLogPreview ? (
) : null}
{showHeaderSkeleton ? (
) : null}
),
[ctrl.online, ctrl.lastLogPreview, isSidebar, showingLeadLogs, showHeaderSkeleton]
);
const canOpenFullscreen = showingLeadLogs ? ctrl.data.total > 0 : selectedMember !== null;
const compactSourceSelector =
selectableMembers.length > 0 ? (
) : null;
const afterBadge = showHeaderSkeleton ? (
<>
>
) : canOpenFullscreen ? (
{t('claudeLogs.fullscreen')}
) : undefined;
return (
<>
{/* When dialog is open, hide the compact log viewer to avoid two competing scroll containers */}
{dialogOpen ? (
{t('claudeLogs.viewingFullscreen')}
) : showingLeadLogs ? (
) : selectedMember ? (
<>
{compactSourceSelector}
>
) : (
{t('claudeLogs.sourceSelect.selectSourceEmpty')}
)}
>
);
});