refactor: streamline message filtering in TeamDetailView

- Replaced manual message filtering logic with a dedicated utility function, `filterTeamMessages`, to enhance readability and maintainability.
- Removed the temporary lead member message filtering, simplifying the message handling process.
- Updated dependencies in the filtering logic to accommodate new parameters for time window and search query.
This commit is contained in:
iliya 2026-03-09 23:24:28 +02:00
parent 1f4c550ed3
commit b09c4e4fd0
4 changed files with 129 additions and 45 deletions

View file

@ -27,9 +27,9 @@ 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 { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
import { toMessageKey } from '@renderer/utils/teamMessageKey';
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import {
AlertTriangle,
@ -600,51 +600,14 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
[data?.members]
);
const leadMemberName = useMemo(
() => activeMembers.find((m) => m.agentType === 'team-lead')?.name,
[activeMembers]
);
const filteredMessages = useMemo(() => {
if (!data) return [];
let list = data.messages;
// Temporarily hide lead→user messages from the UI
// (notifications and other processing still receive them via data.messages)
if (leadMemberName) {
list = list.filter((m) => !(m.to?.trim() === 'user' && m.from?.trim() === leadMemberName));
}
if (timeWindow) {
list = list.filter((m) => {
const ts = new Date(m.timestamp).getTime();
return ts >= timeWindow.start && ts < timeWindow.end;
});
}
if (!messagesFilter.showNoise) {
list = list.filter((m) => !isInboxNoiseMessage(typeof m.text === 'string' ? m.text : ''));
}
const hasFrom = messagesFilter.from.size > 0;
const hasTo = messagesFilter.to.size > 0;
if (hasFrom || hasTo) {
list = list.filter((m) => {
const fromMatch = hasFrom && m.from?.trim() && messagesFilter.from.has(m.from.trim());
const toMatch = hasTo && m.to?.trim() && messagesFilter.to.has(m.to.trim());
// When both filters active → OR (show messages matching either direction)
// When only one active → just that filter
return fromMatch || toMatch;
});
}
const q = messagesSearchQuery.trim().toLowerCase();
if (q) {
list = list.filter((m) => {
const text = (m.text ?? '').toLowerCase();
const summary = (m.summary ?? '').toLowerCase();
const from = (m.from ?? '').toLowerCase();
const to = (m.to ?? '').toLowerCase();
return text.includes(q) || summary.includes(q) || from.includes(q) || to.includes(q);
});
}
return list;
}, [data, timeWindow, messagesFilter, messagesSearchQuery, leadMemberName]);
return filterTeamMessages(data.messages, {
timeWindow,
filter: messagesFilter,
searchQuery: messagesSearchQuery,
});
}, [data, timeWindow, messagesFilter, messagesSearchQuery]);
const { readSet, markRead, markAllRead } = useTeamMessagesRead(teamName ?? '');
const { expandedSet, toggle: toggleExpandOverride } = useTeamMessagesExpanded(teamName ?? '');

View file

@ -442,7 +442,14 @@ export const MessageComposer = ({
setTeamSelectorOpen(false);
}}
>
<ArrowRightLeft size={11} className="shrink-0 text-purple-400" />
{target.color ? (
<span
className="inline-block size-2 shrink-0 rounded-full"
style={{ backgroundColor: target.color }}
/>
) : (
<ArrowRightLeft size={11} className="shrink-0 text-purple-400" />
)}
<div className="min-w-0 flex-1">
<div className="truncate text-[var(--color-text)]">
{target.displayName}

View file

@ -0,0 +1,54 @@
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
import type { InboxMessage } from '@shared/types';
export interface TeamMessagesFilter {
from: Set<string>;
to: Set<string>;
showNoise: boolean;
}
export function filterTeamMessages(
messages: InboxMessage[],
options: {
timeWindow?: { start: number; end: number } | null;
filter: TeamMessagesFilter;
searchQuery: string;
}
): InboxMessage[] {
const { timeWindow, filter, searchQuery } = options;
let list = messages;
if (timeWindow) {
list = list.filter((m) => {
const ts = new Date(m.timestamp).getTime();
return ts >= timeWindow.start && ts < timeWindow.end;
});
}
if (!filter.showNoise) {
list = list.filter((m) => !isInboxNoiseMessage(typeof m.text === 'string' ? m.text : ''));
}
const hasFrom = filter.from.size > 0;
const hasTo = filter.to.size > 0;
if (hasFrom || hasTo) {
list = list.filter((m) => {
const fromMatch = hasFrom && m.from?.trim() && filter.from.has(m.from.trim());
const toMatch = hasTo && m.to?.trim() && filter.to.has(m.to.trim());
return fromMatch || toMatch;
});
}
const q = searchQuery.trim().toLowerCase();
if (q) {
list = list.filter((m) => {
const text = (m.text ?? '').toLowerCase();
const summary = (m.summary ?? '').toLowerCase();
const from = (m.from ?? '').toLowerCase();
const to = (m.to ?? '').toLowerCase();
return text.includes(q) || summary.includes(q) || from.includes(q) || to.includes(q);
});
}
return list;
}

View file

@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
import type { InboxMessage } from '@shared/types';
function makeMessage(overrides: Partial<InboxMessage> = {}): InboxMessage {
return {
from: 'team-lead',
text: 'Hello',
timestamp: '2026-03-09T12:00:00.000Z',
read: true,
messageId: 'msg-1',
...overrides,
};
}
describe('filterTeamMessages', () => {
it('keeps lead-to-user messages visible', () => {
const messages = [
makeMessage({
from: 'lead',
to: 'user',
text: 'Accepted cross-team request. Delegating now.',
source: 'lead_process',
}),
];
const result = filterTeamMessages(messages, {
timeWindow: null,
filter: { from: new Set(), to: new Set(), showNoise: true },
searchQuery: '',
});
expect(result).toHaveLength(1);
expect(result[0].to).toBe('user');
expect(result[0].source).toBe('lead_process');
});
it('still filters noise messages when showNoise is false', () => {
const messages = [
makeMessage({
text: '{"type":"idle_notification","idleReason":"available"}',
}),
makeMessage({
messageId: 'msg-2',
text: 'Real visible message',
}),
];
const result = filterTeamMessages(messages, {
timeWindow: null,
filter: { from: new Set(), to: new Set(), showNoise: false },
searchQuery: '',
});
expect(result).toHaveLength(1);
expect(result[0].messageId).toBe('msg-2');
});
});