diff --git a/src/renderer/components/team/ClaudeLogsPanel.tsx b/src/renderer/components/team/ClaudeLogsPanel.tsx
index 05b5784c..4325808f 100644
--- a/src/renderer/components/team/ClaudeLogsPanel.tsx
+++ b/src/renderer/components/team/ClaudeLogsPanel.tsx
@@ -29,6 +29,7 @@ interface ClaudeLogsPanelProps {
/** Extra className for the panel wrapper. */
className?: string;
compactMetaInTooltip?: boolean;
+ toolbarAccessory?: React.ReactNode;
}
// =============================================================================
@@ -41,6 +42,7 @@ export const ClaudeLogsPanel = ({
viewerMaxHeight,
className,
compactMetaInTooltip = false,
+ toolbarAccessory,
}: ClaudeLogsPanelProps): React.JSX.Element => {
const {
data,
@@ -86,10 +88,20 @@ export const ClaudeLogsPanel = ({
'Team is not running.'
)}
-
+
{data.total > 0 ? (
<>
-
+
)}
+ {toolbarAccessory}
selectResolvedMembersForTeamName(s, teamName));
const [dialogOpen, setDialogOpen] = useState(false);
const isSidebar = position === 'sidebar';
const showHeaderSkeleton = ctrl.loading && ctrl.data.lines.length === 0 && !ctrl.error;
+ const leadLogMember = useMemo(
+ () => resolvedMembers.find((member) => !member.removedAt && isLeadLogSourceMember(member)),
+ [resolvedMembers]
+ );
+ const sidebarLogSourceSelect =
+ isSidebar && leadLogMember ? (
+ undefined}
+ size="sm"
+ triggerVariant="avatar"
+ popoverAlign="end"
+ />
+ ) : null;
const sectionHeaderExtra = useMemo(
() => (
@@ -173,6 +193,7 @@ export const ClaudeLogsSection = memo(function ClaudeLogsSection({
viewerClassName={cn('max-h-[213px]', isSidebar && 'cli-logs-sidebar')}
viewerMaxHeight={isSidebar ? sidebarViewerMaxHeight : undefined}
compactMetaInTooltip={isSidebar}
+ toolbarAccessory={sidebarLogSourceSelect}
/>
)}
diff --git a/src/renderer/components/team/claudeLogsSourceMember.ts b/src/renderer/components/team/claudeLogsSourceMember.ts
new file mode 100644
index 00000000..2cf41b7d
--- /dev/null
+++ b/src/renderer/components/team/claudeLogsSourceMember.ts
@@ -0,0 +1,11 @@
+import { isLeadMember } from '@shared/utils/leadDetection';
+
+import type { ResolvedTeamMember } from '@shared/types';
+
+export function isLeadLogSourceMember(member: ResolvedTeamMember): boolean {
+ if (isLeadMember(member)) return true;
+ const normalizedName = member.name.trim().toLowerCase();
+ if (normalizedName === 'lead') return true;
+ const normalizedRole = member.role?.trim().toLowerCase();
+ return normalizedRole === 'lead' || normalizedRole === 'team lead';
+}
diff --git a/src/renderer/components/ui/MemberSelect.tsx b/src/renderer/components/ui/MemberSelect.tsx
index 0f80cccf..ec79b656 100644
--- a/src/renderer/components/ui/MemberSelect.tsx
+++ b/src/renderer/components/ui/MemberSelect.tsx
@@ -8,9 +8,10 @@ import {
agentAvatarUrl,
buildMemberAvatarMap,
buildMemberColorMap,
+ displayMemberName,
} from '@renderer/utils/memberHelpers';
import { Command as CommandPrimitive } from 'cmdk';
-import { Check, ChevronsUpDown } from 'lucide-react';
+import { Check, ChevronsUpDown, UserRound } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
@@ -25,6 +26,9 @@ interface MemberSelectProps {
allowUnassigned?: boolean;
/** Size variant */
size?: 'sm' | 'md';
+ /** Full select by default. Avatar mode is for dense toolbars/sidebar surfaces. */
+ triggerVariant?: 'default' | 'avatar';
+ popoverAlign?: 'start' | 'center' | 'end';
disabled?: boolean;
className?: string;
}
@@ -38,6 +42,8 @@ export const MemberSelect = ({
placeholder = 'Select member...',
allowUnassigned = false,
size = 'sm',
+ triggerVariant = 'default',
+ popoverAlign,
disabled = false,
className,
}: MemberSelectProps): React.JSX.Element => {
@@ -57,6 +63,28 @@ export const MemberSelect = ({
const avatarClass = size === 'md' ? 'size-6' : 'size-5';
const textSize = size === 'md' ? 'text-xs' : 'text-[10px]';
const triggerHeight = size === 'md' ? 'h-9' : 'h-8';
+ const isAvatarTrigger = triggerVariant === 'avatar';
+ const effectivePopoverAlign = popoverAlign ?? (isAvatarTrigger ? 'end' : 'start');
+ const avatarTriggerSize = size === 'md' ? 'size-9' : 'size-8';
+ const selectedLabel =
+ selectedMember != null
+ ? displayMemberName(selectedMember.name)
+ : value
+ ? displayMemberName(value)
+ : allowUnassigned
+ ? 'Unassigned'
+ : placeholder;
+
+ const renderAvatarByName = (name: string): React.ReactNode => (
+
+ );
+ const renderMemberAvatar = (member: ResolvedTeamMember): React.ReactNode =>
+ renderAvatarByName(member.name);
// eslint-disable-next-line sonarjs/function-return-type -- option renderer returns mixed node structure
const renderMemberInline = (member: ResolvedTeamMember): React.ReactNode => {
@@ -90,29 +118,48 @@ export const MemberSelect = ({
{
await Promise.resolve();
});
});
+
+ it('renders toolbar accessory beside log search and filters', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const ctrl = createController({
+ isAlive: true,
+ data: {
+ lines: ['[stdout] ready'],
+ total: 1,
+ hasMore: false,
+ },
+ filteredText: '[stdout]\nready',
+ });
+
+ await act(async () => {
+ root.render(
+ React.createElement(ClaudeLogsPanel, {
+ ctrl,
+ compactMetaInTooltip: true,
+ toolbarAccessory: React.createElement(
+ 'button',
+ { type: 'button', 'data-testid': 'log-member-selector' },
+ 'Lead'
+ ),
+ })
+ );
+ await Promise.resolve();
+ });
+
+ const search = host.querySelector('input[placeholder="Search logs..."]');
+ const accessory = host.querySelector('[data-testid="log-member-selector"]');
+ const filter = host.querySelector('[data-testid="logs-filter"]');
+
+ expect(search).not.toBeNull();
+ expect(accessory).not.toBeNull();
+ expect(filter).not.toBeNull();
+ expect(search?.parentElement?.className).toContain('flex-1');
+ expect(search?.compareDocumentPosition(accessory as Node) ?? 0).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+ expect(accessory?.compareDocumentPosition(filter as Node) ?? 0).toBe(
+ Node.DOCUMENT_POSITION_FOLLOWING
+ );
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
});
diff --git a/test/renderer/components/team/ClaudeLogsSection.test.ts b/test/renderer/components/team/ClaudeLogsSection.test.ts
new file mode 100644
index 00000000..e4683640
--- /dev/null
+++ b/test/renderer/components/team/ClaudeLogsSection.test.ts
@@ -0,0 +1,29 @@
+import { isLeadLogSourceMember } from '@renderer/components/team/claudeLogsSourceMember';
+import { describe, expect, it } from 'vitest';
+
+import type { ResolvedTeamMember } from '@shared/types';
+
+function member(overrides: Partial): ResolvedTeamMember {
+ return {
+ name: 'alice',
+ status: 'active',
+ currentTaskId: null,
+ taskCount: 0,
+ lastActiveAt: null,
+ messageCount: 0,
+ ...overrides,
+ };
+}
+
+describe('isLeadLogSourceMember', () => {
+ it('accepts canonical and cached lead aliases for compact log source UI', () => {
+ expect(isLeadLogSourceMember(member({ name: 'team-lead' }))).toBe(true);
+ expect(isLeadLogSourceMember(member({ name: 'Lead' }))).toBe(true);
+ expect(isLeadLogSourceMember(member({ name: 'current', role: 'Team Lead' }))).toBe(true);
+ });
+
+ it('does not treat arbitrary leadership-like roles as the lead log source', () => {
+ expect(isLeadLogSourceMember(member({ name: 'alice', role: 'Tech Lead' }))).toBe(false);
+ expect(isLeadLogSourceMember(member({ name: 'lead-reviewer' }))).toBe(false);
+ });
+});
diff --git a/test/renderer/components/ui/MemberSelect.test.tsx b/test/renderer/components/ui/MemberSelect.test.tsx
new file mode 100644
index 00000000..b9083e87
--- /dev/null
+++ b/test/renderer/components/ui/MemberSelect.test.tsx
@@ -0,0 +1,64 @@
+import React, { act } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import { MemberSelect } from '@renderer/components/ui/MemberSelect';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import type { ResolvedTeamMember } from '@shared/types';
+
+function member(name: string): ResolvedTeamMember {
+ return {
+ name,
+ status: 'active',
+ currentTaskId: null,
+ taskCount: 0,
+ lastActiveAt: null,
+ messageCount: 0,
+ };
+}
+
+describe('MemberSelect', () => {
+ afterEach(() => {
+ document.body.innerHTML = '';
+ vi.unstubAllGlobals();
+ });
+
+ it('uses an avatar trigger for dense surfaces while keeping the full member list popover', async () => {
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const root = createRoot(host);
+ const onChange = vi.fn();
+
+ await act(async () => {
+ root.render(
+
+ );
+ await Promise.resolve();
+ });
+
+ const trigger = host.querySelector('button[role="combobox"]') as HTMLButtonElement | null;
+ expect(trigger).not.toBeNull();
+ expect(trigger?.getAttribute('aria-label')).toBe('Select member: Lead');
+ expect(trigger?.getAttribute('title')).toBe('Lead');
+ expect(host.textContent).not.toContain('Lead');
+
+ await act(async () => {
+ trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+ await Promise.resolve();
+ });
+
+ expect(document.body.textContent).toContain('Lead');
+ expect(document.body.textContent).toContain('Alice');
+
+ await act(async () => {
+ root.unmount();
+ await Promise.resolve();
+ });
+ });
+});