feat(logs): compact team log source controls

This commit is contained in:
777genius 2026-05-25 14:54:28 +03:00
parent 79faaf1f9f
commit 62ef88300a
8 changed files with 195 additions and 186 deletions

View file

@ -1,15 +1,13 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { api } from '@renderer/api';
import { useStore } from '@renderer/store';
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
import { useMemberLogStream } from '../hooks/useMemberLogStream';
import { ExecutionLogStreamView } from '../ui/ExecutionLogStreamView';
import { MemberRuntimeProcessLogsPanel } from '../ui/MemberRuntimeProcessLogsPanel';
import type { MemberLogStreamSegment, MemberRuntimeLogKind } from '../../contracts';
import type { MemberLogStreamSegment } from '../../contracts';
import type { ResolvedTeamMember } from '@shared/types';
interface MemberLogStreamSectionProps {
@ -19,10 +17,6 @@ interface MemberLogStreamSectionProps {
onInitialLoadErrorChange?: (hasError: boolean) => void;
}
function describeMemberStream(): string {
return 'Member-scoped transcript and runtime logs rendered with the same execution-log components used in Task Log Stream.';
}
function getSegmentMetaLabel(segment: MemberLogStreamSegment): string {
const details = [segment.source.label];
if (segment.source.laneId) {
@ -38,24 +32,15 @@ function buildMemberSegmentRenderKey(segment: MemberLogStreamSegment): string {
return `${segment.id}:${firstChunkId ?? segment.startTimestamp}`;
}
export function MemberLogStreamSection({
export const MemberLogStreamSection = ({
teamName,
member,
enabled = true,
onInitialLoadErrorChange,
}: Readonly<MemberLogStreamSectionProps>): React.JSX.Element {
}: Readonly<MemberLogStreamSectionProps>): React.JSX.Element => {
const { t } = useAppTranslation('team');
const [selectedLogView, setSelectedLogView] = useState<'execution' | 'process'>('execution');
const teamMembers = useStore((s) => selectResolvedMembersForTeamName(s, teamName));
const { stream, loading, error } = useMemberLogStream({ teamName, member, enabled });
const loadRuntimeLogTail = useCallback(
(input: {
readonly kind: MemberRuntimeLogKind;
readonly maxBytes: number;
readonly forceRefresh?: boolean;
}) => api.memberLogStream.getMemberRuntimeLogTail(teamName, member.name, input),
[member.name, teamName]
);
const hasInitialLoadError = Boolean(error && !stream && !loading);
const boundedHistoryNote = useMemo(() => {
if (!stream) return null;
@ -70,56 +55,24 @@ export function MemberLogStreamSection({
}, [hasInitialLoadError, onInitialLoadErrorChange]);
return (
<div className="space-y-4">
<div className="inline-flex rounded-xl bg-[var(--color-surface-subtle)] p-1">
<button
type="button"
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
selectedLogView === 'execution'
? 'bg-[var(--color-surface)] text-[var(--color-text)] shadow-sm'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
}`}
onClick={() => setSelectedLogView('execution')}
>
{t('memberLogStream.tabs.execution')}
</button>
<button
type="button"
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
selectedLogView === 'process'
? 'bg-[var(--color-surface)] text-[var(--color-text)] shadow-sm'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
}`}
onClick={() => setSelectedLogView('process')}
>
{t('memberLogStream.tabs.process')}
</button>
</div>
{selectedLogView === 'execution' ? (
<ExecutionLogStreamView
title={t('memberLogStream.logs.title')}
description={describeMemberStream()}
stream={stream}
loading={loading}
error={error}
teamName={teamName}
teamMembers={teamMembers}
loadingText={t('memberLogStream.logs.loading')}
emptyTitle={t('memberLogStream.logs.emptyTitle')}
emptyDescription={t('memberLogStream.logs.emptyDescription')}
selectionResetKey={`${teamName}:${member.name}`}
boundedHistoryNote={boundedHistoryNote}
forceSegmentHeaders
buildSegmentRenderKey={buildMemberSegmentRenderKey}
getSegmentMetaLabel={getSegmentMetaLabel}
/>
) : (
<MemberRuntimeProcessLogsPanel
enabled={enabled && selectedLogView === 'process'}
loadRuntimeLogTail={loadRuntimeLogTail}
/>
)}
</div>
<ExecutionLogStreamView
title=""
description=""
stream={stream}
loading={loading}
error={error}
teamName={teamName}
teamMembers={teamMembers}
loadingText={t('memberLogStream.logs.loading')}
emptyTitle={t('memberLogStream.logs.emptyTitle')}
emptyDescription={t('memberLogStream.logs.emptyDescription')}
selectionResetKey={`${teamName}:${member.name}`}
boundedHistoryNote={boundedHistoryNote}
forceSegmentHeaders
showIntro={false}
showSegmentParticipantBadge={false}
buildSegmentRenderKey={buildMemberSegmentRenderKey}
getSegmentMetaLabel={getSegmentMetaLabel}
/>
);
}
};

View file

@ -7,7 +7,7 @@ import {
type MemberLogStreamResponse,
normalizeMemberLogStreamResponse,
} from '../../contracts';
import { normalizeExecutionLogStream } from '../ui/ExecutionLogStreamView';
import { normalizeExecutionLogStream } from '../ui/executionLogStreamUtils';
import type { ResolvedTeamMember } from '@shared/types';

View file

@ -1,7 +1,7 @@
export { MemberLogStreamSection } from './adapters/MemberLogStreamSection';
export {
buildDefaultExecutionSegmentRenderKey,
ExecutionLogStreamView,
normalizeExecutionLogStream,
} from './ui/ExecutionLogStreamView';
} from './ui/executionLogStreamUtils';
export { ExecutionLogStreamView } from './ui/ExecutionLogStreamView';
export { isMemberLogStreamUiEnabled } from './utils/featureGates';

View file

@ -10,23 +10,14 @@ import {
getThemedText,
} from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { asEnhancedChunkArray } from '@renderer/types/data';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { isLeadMember } from '@shared/utils/leadDetection';
import { AlertCircle, Clock, FileText, Loader2 } from 'lucide-react';
import type {
BoardTaskLogActor,
BoardTaskLogParticipant,
BoardTaskLogSegment,
ResolvedTeamMember,
} from '@shared/types';
import { buildDefaultExecutionSegmentRenderKey } from './executionLogStreamUtils';
interface ExecutionLogStreamLike {
participants: BoardTaskLogParticipant[];
defaultFilter: string;
segments: BoardTaskLogSegment[];
}
import type { ExecutionLogStreamLike } from './executionLogStreamUtils';
import type { BoardTaskLogActor, BoardTaskLogSegment, ResolvedTeamMember } from '@shared/types';
interface ParticipantVisual {
name: string;
@ -47,6 +38,8 @@ export interface ExecutionLogStreamViewProps<TStream extends ExecutionLogStreamL
selectionResetKey: string;
boundedHistoryNote?: string | null;
forceSegmentHeaders?: boolean;
showIntro?: boolean;
showSegmentParticipantBadge?: boolean;
buildSegmentRenderKey?: (segment: TStream['segments'][number]) => string;
getSegmentMetaLabel?: (segment: TStream['segments'][number]) => string | null;
}
@ -72,26 +65,6 @@ function actorLabel(actor: BoardTaskLogActor): string {
return `member session ${actor.sessionId.slice(0, 8)}`;
}
export function normalizeExecutionLogStream<TStream extends ExecutionLogStreamLike>(
response: TStream
): TStream {
return {
...response,
segments: response.segments.map((segment) => ({
...segment,
chunks: asEnhancedChunkArray(segment.chunks) ?? [],
})),
};
}
export function buildDefaultExecutionSegmentRenderKey(segment: BoardTaskLogSegment): string {
const firstChunkId = segment.chunks[0]?.id;
if (firstChunkId) {
return `${segment.participantKey}:${firstChunkId}`;
}
return `${segment.participantKey}:${segment.startTimestamp}`;
}
function buildParticipantVisualMap(
stream: ExecutionLogStreamLike | null,
members: readonly ResolvedTeamMember[],
@ -129,14 +102,16 @@ const SegmentMarker = <TSegment extends BoardTaskLogSegment>({
visual,
teamName,
metaLabel,
showParticipantBadge,
}: {
segment: TSegment;
visual?: ParticipantVisual;
teamName: string;
metaLabel?: string | null;
showParticipantBadge: boolean;
}): React.JSX.Element => (
<div className="mb-2 flex flex-wrap items-center gap-2 text-[10px] text-[var(--color-text-muted)]">
{visual ? (
{showParticipantBadge && visual ? (
<MemberBadge
name={visual.name}
color={visual.color}
@ -159,16 +134,24 @@ const SegmentBlock = <TSegment extends BoardTaskLogSegment>({
teamName,
visual,
metaLabel,
showParticipantBadge,
}: {
segment: TSegment;
showHeader: boolean;
teamName: string;
visual?: ParticipantVisual;
metaLabel?: string | null;
showParticipantBadge: boolean;
}): React.JSX.Element => (
<div className="min-w-0 overflow-hidden">
{showHeader ? (
<SegmentMarker segment={segment} visual={visual} teamName={teamName} metaLabel={metaLabel} />
<SegmentMarker
segment={segment}
visual={visual}
teamName={teamName}
metaLabel={metaLabel}
showParticipantBadge={showParticipantBadge}
/>
) : null}
<MemberExecutionLog
chunks={segment.chunks}
@ -221,7 +204,7 @@ const ParticipantFilterChip = ({
);
};
export function ExecutionLogStreamView<TStream extends ExecutionLogStreamLike>({
export const ExecutionLogStreamView = <TStream extends ExecutionLogStreamLike>({
title,
description,
stream,
@ -235,9 +218,11 @@ export function ExecutionLogStreamView<TStream extends ExecutionLogStreamLike>({
selectionResetKey,
boundedHistoryNote,
forceSegmentHeaders = false,
showIntro = true,
showSegmentParticipantBadge = true,
buildSegmentRenderKey,
getSegmentMetaLabel,
}: Readonly<ExecutionLogStreamViewProps<TStream>>): React.JSX.Element {
}: Readonly<ExecutionLogStreamViewProps<TStream>>): React.JSX.Element => {
const { t } = useAppTranslation('team');
const [selectedParticipantKey, setSelectedParticipantKey] = useState<string>('all');
const appliedSelectionResetKeyRef = useRef<string | null>(null);
@ -291,7 +276,11 @@ export function ExecutionLogStreamView<TStream extends ExecutionLogStreamLike>({
if (loading) {
return (
<div className="space-y-2">
<h4 className="text-xs font-semibold uppercase text-[var(--color-text-muted)]">{title}</h4>
{showIntro ? (
<h4 className="text-xs font-semibold uppercase text-[var(--color-text-muted)]">
{title}
</h4>
) : null}
<div className="flex items-center gap-2 py-4 text-xs text-[var(--color-text-muted)]">
<Loader2 size={12} className="animate-spin" />
{loadingText}
@ -303,7 +292,11 @@ export function ExecutionLogStreamView<TStream extends ExecutionLogStreamLike>({
if (error) {
return (
<div className="space-y-2">
<h4 className="text-xs font-semibold uppercase text-[var(--color-text-muted)]">{title}</h4>
{showIntro ? (
<h4 className="text-xs font-semibold uppercase text-[var(--color-text-muted)]">
{title}
</h4>
) : null}
<div className="flex items-center gap-2 py-4 text-xs text-red-400">
<AlertCircle size={14} />
{error}
@ -314,8 +307,14 @@ export function ExecutionLogStreamView<TStream extends ExecutionLogStreamLike>({
return (
<div className="space-y-3">
<h4 className="text-xs font-semibold uppercase text-[var(--color-text-muted)]">{title}</h4>
<p className="text-xs text-[var(--color-text-muted)]">{description}</p>
{showIntro ? (
<>
<h4 className="text-xs font-semibold uppercase text-[var(--color-text-muted)]">
{title}
</h4>
<p className="text-xs text-[var(--color-text-muted)]">{description}</p>
</>
) : null}
{boundedHistoryNote ? (
<p className="text-[11px] text-amber-300">{boundedHistoryNote}</p>
) : null}
@ -362,10 +361,11 @@ export function ExecutionLogStreamView<TStream extends ExecutionLogStreamLike>({
teamName={teamName}
visual={participantVisuals.get(segment.participantKey)}
metaLabel={getSegmentMetaLabel?.(segment)}
showParticipantBadge={showSegmentParticipantBadge}
/>
))}
</div>
)}
</div>
);
}
};

View file

@ -0,0 +1,29 @@
import { asEnhancedChunkArray } from '@renderer/types/data';
import type { BoardTaskLogParticipant, BoardTaskLogSegment } from '@shared/types';
export interface ExecutionLogStreamLike {
participants: BoardTaskLogParticipant[];
defaultFilter: string;
segments: BoardTaskLogSegment[];
}
export function normalizeExecutionLogStream<TStream extends ExecutionLogStreamLike>(
response: TStream
): TStream {
return {
...response,
segments: response.segments.map((segment) => ({
...segment,
chunks: asEnhancedChunkArray(segment.chunks) ?? [],
})),
};
}
export function buildDefaultExecutionSegmentRenderKey(segment: BoardTaskLogSegment): string {
const firstChunkId = segment.chunks[0]?.id;
if (firstChunkId) {
return `${segment.participantKey}:${firstChunkId}`;
}
return `${segment.participantKey}:${segment.startTimestamp}`;
}

View file

@ -97,40 +97,40 @@ export const ClaudeLogsPanel = ({
<div className="min-w-0 shrink-0">{toolbarControlsStart}</div>
) : null}
{data.total > 0 ? (
<>
<div
className={cn(
'flex items-center gap-1.5 rounded-md border border-[var(--color-border)] bg-transparent px-2 py-1',
compactMetaInTooltip ? 'min-w-0 max-w-48 flex-1' : 'w-48'
)}
>
<Search size={12} className="shrink-0 text-[var(--color-text-muted)]" />
<input
type="text"
placeholder={t('claudeLogs.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="min-w-0 flex-1 bg-transparent text-xs text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none"
/>
{searchQuery && (
<button
type="button"
className="shrink-0 rounded p-0.5 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
onClick={() => setSearchQuery('')}
aria-label={t('claudeLogs.clearSearch')}
>
<X size={14} />
</button>
)}
</div>
{toolbarAccessory}
<ClaudeLogsFilterPopover
filter={filter}
open={filterOpen}
onOpenChange={setFilterOpen}
onApply={setFilter}
<div
className={cn(
'flex items-center gap-1.5 rounded-md border border-[var(--color-border)] bg-transparent px-2 py-1',
compactMetaInTooltip ? 'min-w-0 max-w-48 flex-1' : 'w-48'
)}
>
<Search size={12} className="shrink-0 text-[var(--color-text-muted)]" />
<input
type="text"
placeholder={t('claudeLogs.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="min-w-0 flex-1 bg-transparent text-xs text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:outline-none"
/>
</>
{searchQuery && (
<button
type="button"
className="shrink-0 rounded p-0.5 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
onClick={() => setSearchQuery('')}
aria-label={t('claudeLogs.clearSearch')}
>
<X size={14} />
</button>
)}
</div>
) : null}
{toolbarAccessory}
{data.total > 0 ? (
<ClaudeLogsFilterPopover
filter={filter}
open={filterOpen}
onOpenChange={setFilterOpen}
onApply={setFilter}
/>
) : null}
{pendingNewCount > 0 && (
<Button

View file

@ -102,12 +102,14 @@ const TeamLogsSourceSelector = ({
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]);
@ -135,6 +137,7 @@ const TeamLogsSourceSelector = ({
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')
@ -146,15 +149,6 @@ const TeamLogsSourceSelector = ({
);
};
const MemberSourcePill = ({ member }: { member: ResolvedTeamMember }): React.JSX.Element => (
<span
className="min-w-0 truncate rounded-md border border-[var(--color-border)] px-1.5 py-0.5 text-[10px] text-[var(--color-text-muted)]"
title={formatMemberLogSourceLabel(member)}
>
{formatMemberLogSourceLabel(member)}
</span>
);
const MemberLogsSourcePanel = ({
teamName,
member,
@ -354,7 +348,6 @@ export const ClaudeLogsSection = memo(function ClaudeLogsSection({
{showingLeadLogs && ctrl.lastLogPreview ? (
<LogPreviewInline preview={ctrl.lastLogPreview} />
) : null}
{!showingLeadLogs && selectedMember ? <MemberSourcePill member={selectedMember} /> : null}
{showHeaderSkeleton ? (
<span className="flex min-w-0 flex-1 items-center gap-1.5 opacity-70">
<LogsHeaderSkeletonPill className="size-3 rounded" />
@ -365,18 +358,23 @@ export const ClaudeLogsSection = memo(function ClaudeLogsSection({
) : null}
</span>
),
[
ctrl.online,
ctrl.lastLogPreview,
isSidebar,
selectedMember,
showingLeadLogs,
showHeaderSkeleton,
]
[ctrl.online, ctrl.lastLogPreview, isSidebar, showingLeadLogs, showHeaderSkeleton]
);
const canOpenFullscreen = showingLeadLogs ? ctrl.data.total > 0 : selectedMember !== null;
const compactSourceSelector =
selectableMembers.length > 0 ? (
<TeamLogsSourceSelector
leadMember={leadMember}
members={selectableMembers}
selectedKey={effectiveSelectedSourceKey}
onChange={setSelectedSourceKey}
className="shrink-0 pb-0"
triggerVariant="avatar"
/>
) : null;
const afterBadge = showHeaderSkeleton ? (
<>
<LogsHeaderSkeletonPill className="h-5 w-14" />
@ -422,12 +420,6 @@ export const ClaudeLogsSection = memo(function ClaudeLogsSection({
contentClassName="pt-0 [overflow-anchor:none]"
>
{/* When dialog is open, hide the compact log viewer to avoid two competing scroll containers */}
<TeamLogsSourceSelector
leadMember={leadMember}
members={selectableMembers}
selectedKey={effectiveSelectedSourceKey}
onChange={setSelectedSourceKey}
/>
{dialogOpen ? (
<div className="flex items-center gap-2 p-2 text-xs text-[var(--color-text-muted)]">
<Expand size={12} />
@ -439,14 +431,18 @@ export const ClaudeLogsSection = memo(function ClaudeLogsSection({
viewerClassName={cn('max-h-[213px]', isSidebar && 'cli-logs-sidebar')}
viewerMaxHeight={isSidebar ? sidebarViewerMaxHeight : undefined}
compactMetaInTooltip={isSidebar}
toolbarAccessory={compactSourceSelector}
/>
) : selectedMember ? (
<MemberLogsSourcePanel
teamName={teamName}
member={selectedMember}
enabled={effectiveSelectedSourceKey === memberLogSourceKey(selectedMember.name)}
maxHeight={isSidebar ? sidebarViewerMaxHeight : undefined}
/>
<>
<div className="flex justify-end pb-2">{compactSourceSelector}</div>
<MemberLogsSourcePanel
teamName={teamName}
member={selectedMember}
enabled={effectiveSelectedSourceKey === memberLogSourceKey(selectedMember.name)}
maxHeight={isSidebar ? sidebarViewerMaxHeight : undefined}
/>
</>
) : (
<div className="py-4 text-center text-xs text-[var(--color-text-muted)]">
{t('claudeLogs.sourceSelect.selectSourceEmpty')}

View file

@ -65,10 +65,21 @@ vi.mock('@renderer/components/team/useClaudeLogsController', () => ({
}));
vi.mock('@renderer/components/team/ClaudeLogsPanel', () => ({
ClaudeLogsPanel: ({ toolbarControlsStart }: { toolbarControlsStart?: React.ReactNode }) =>
ClaudeLogsPanel: ({
toolbarAccessory,
toolbarControlsStart,
}: {
toolbarAccessory?: React.ReactNode;
toolbarControlsStart?: React.ReactNode;
}) =>
React.createElement(
'div',
{ 'data-testid': 'lead-logs-panel' },
{
'data-testid': 'lead-logs-panel',
'data-has-toolbar-accessory': toolbarAccessory ? 'true' : 'false',
'data-has-toolbar-controls-start': toolbarControlsStart ? 'true' : 'false',
},
toolbarAccessory,
toolbarControlsStart,
'lead-panel'
),
@ -103,6 +114,7 @@ vi.mock('@renderer/components/ui/MemberSelect', () => ({
searchPlaceholder,
emptyMessage,
ariaLabel,
triggerVariant,
}: {
members: ResolvedTeamMember[];
value: string | null;
@ -111,6 +123,7 @@ vi.mock('@renderer/components/ui/MemberSelect', () => ({
searchPlaceholder?: string;
emptyMessage?: string;
ariaLabel?: string;
triggerVariant?: 'default' | 'avatar';
}) =>
React.createElement(
'div',
@ -118,6 +131,7 @@ vi.mock('@renderer/components/ui/MemberSelect', () => ({
'data-testid': 'member-select',
'data-search-placeholder': searchPlaceholder,
'data-empty-message': emptyMessage,
'data-trigger-variant': triggerVariant ?? 'default',
},
React.createElement(
'select',
@ -273,11 +287,22 @@ describe('ClaudeLogsSection source filtering', () => {
expect(memberSelect?.getAttribute('data-search-placeholder')).toBe('Search log sources...');
expect(select.getAttribute('data-trigger-aria-label')).toBe('Log source');
expect(host.querySelector('[data-testid="lead-logs-panel"]')).not.toBeNull();
expect(
host
.querySelector('[data-testid="lead-logs-panel"]')
?.getAttribute('data-has-toolbar-accessory')
).toBe('true');
expect(
host
.querySelector('[data-testid="lead-logs-panel"]')
?.getAttribute('data-has-toolbar-controls-start')
).toBe('false');
expect(sectionState.memberLogStreamCalls).toEqual([]);
expect(sectionState.controllerCalls.at(-1)).toEqual({
teamName: 'demo-team',
enabled: true,
});
expect(memberSelect?.getAttribute('data-trigger-variant')).toBe('avatar');
await act(async () => {
root.unmount();
@ -379,9 +404,12 @@ describe('ClaudeLogsSection source filtering', () => {
expect(host.querySelector('[data-testid="member-log-stream"]')?.textContent).toBe('Builder');
const teammateSelect = host.querySelector(
'select[aria-label="Log source"]'
) as HTMLSelectElement;
await act(async () => {
select.value = 'team-lead';
select.dispatchEvent(new Event('change', { bubbles: true }));
teammateSelect.value = 'team-lead';
teammateSelect.dispatchEvent(new Event('change', { bubbles: true }));
await Promise.resolve();
});
@ -436,7 +464,7 @@ describe('ClaudeLogsSection source filtering', () => {
await Promise.resolve();
});
const select = host.querySelector('select[aria-label="Log source"]') as HTMLSelectElement;
let select = host.querySelector('select[aria-label="Log source"]') as HTMLSelectElement;
expect(Array.from(select.options).map((option) => option.textContent)).toEqual([
'Lead',
'Builder',
@ -450,6 +478,7 @@ describe('ClaudeLogsSection source filtering', () => {
});
expect(host.querySelector('[data-testid="member-log-stream"]')?.textContent).toBe('Builder');
select = host.querySelector('select[aria-label="Log source"]') as HTMLSelectElement;
await act(async () => {
select.value = 'Reviewer';
select.dispatchEvent(new Event('change', { bubbles: true }));
@ -837,9 +866,11 @@ describe('ClaudeLogsSection source filtering', () => {
expect(
(dialog.querySelector('select[aria-label="Log source"]') as HTMLSelectElement).value
).toBe('team-lead');
expect(
dialog.querySelector('[data-testid="lead-logs-panel"] [data-testid="member-select"]')
).not.toBeNull();
const dialogMemberSelect = dialog.querySelector(
'[data-testid="lead-logs-panel"] [data-testid="member-select"]'
);
expect(dialogMemberSelect).not.toBeNull();
expect(dialogMemberSelect?.getAttribute('data-trigger-variant')).toBe('default');
const dialogSelect = dialog.querySelector(
'select[aria-label="Log source"]'