+ ) : null}
{data.total > 0 ? (
<>
diff --git a/src/renderer/components/team/ClaudeLogsSection.tsx b/src/renderer/components/team/ClaudeLogsSection.tsx
index 818e68fb..cfadb871 100644
--- a/src/renderer/components/team/ClaudeLogsSection.tsx
+++ b/src/renderer/components/team/ClaudeLogsSection.tsx
@@ -1,16 +1,33 @@
-import { memo, useMemo, useState } from 'react';
+import { memo, useCallback, useEffect, useMemo, useState } from 'react';
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 { Brain, Expand, MessageSquare, Wrench } from 'lucide-react';
+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 { ClaudeLogsDialog } from './ClaudeLogsDialog';
+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
@@ -78,6 +95,177 @@ const LogPreviewInline = ({ preview }: { preview: LastLogPreview }): React.JSX.E
);
};
+const TeamLogsSourceSelector = ({
+ leadMember,
+ members,
+ selectedKey,
+ onChange,
+ className,
+}: {
+ leadMember: ResolvedTeamMember;
+ members: readonly ResolvedTeamMember[];
+ selectedKey: TeamLogSourceKey;
+ onChange: (key: TeamLogSourceKey) => void;
+ className?: string;
+}): React.JSX.Element | null => {
+ 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="Select log source..."
+ searchPlaceholder="Search log sources..."
+ emptyMessage="No log sources found."
+ ariaLabel="Log source"
+ getMemberLabel={(member) =>
+ isLeadMember(member) ? 'Lead' : formatMemberLogSourceLabel(member)
+ }
+ getMemberDescription={formatMemberLogSourceDescription}
+ />
+
+ );
+};
+
+const MemberSourcePill = ({ member }: { member: ResolvedTeamMember }): React.JSX.Element => (
+
+ {formatMemberLogSourceLabel(member)}
+
+);
+
+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 sourceSelector =
+ members.length > 0 ? (
+
+ ) : null;
+
+ return (
+
+ );
+};
+
// =============================================================================
// Main component
// =============================================================================
@@ -88,22 +276,79 @@ export const ClaudeLogsSection = memo(function ClaudeLogsSection({
sidebarViewerMaxHeight,
onOpenChange,
}: ClaudeLogsSectionProps): React.JSX.Element {
- const ctrl = useClaudeLogsController(teamName);
+ 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 = ctrl.loading && ctrl.data.lines.length === 0 && !ctrl.error;
+ 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(
() => (
- {ctrl.online ? (
+ {showingLeadLogs && ctrl.online ? (
) : null}
- {ctrl.lastLogPreview ? : null}
+ {showingLeadLogs && ctrl.lastLogPreview ? (
+
+ ) : null}
+ {!showingLeadLogs && selectedMember ? : null}
{showHeaderSkeleton ? (
@@ -114,9 +359,18 @@ export const ClaudeLogsSection = memo(function ClaudeLogsSection({
) : null}
),
- [ctrl.online, ctrl.lastLogPreview, isSidebar, showHeaderSkeleton]
+ [
+ ctrl.online,
+ ctrl.lastLogPreview,
+ isSidebar,
+ selectedMember,
+ showingLeadLogs,
+ showHeaderSkeleton,
+ ]
);
+ const canOpenFullscreen = showingLeadLogs ? ctrl.data.total > 0 : selectedMember !== null;
+
const afterBadge = showHeaderSkeleton ? (
<>
@@ -124,7 +378,7 @@ export const ClaudeLogsSection = memo(function ClaudeLogsSection({
>
- ) : ctrl.data.total > 0 ? (
+ ) : canOpenFullscreen ? (
@@ -135,7 +159,7 @@ export const MemberSelect = ({
onWheel={(e) => e.stopPropagation()}
>