From b09c4e4fd0d8f44a21f454857c9639d8a4ab5d50 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 9 Mar 2026 23:24:28 +0200 Subject: [PATCH] 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. --- .../components/team/TeamDetailView.tsx | 51 +++------------- .../team/messages/MessageComposer.tsx | 9 ++- src/renderer/utils/teamMessageFiltering.ts | 54 +++++++++++++++++ .../utils/teamMessageFiltering.test.ts | 60 +++++++++++++++++++ 4 files changed, 129 insertions(+), 45 deletions(-) create mode 100644 src/renderer/utils/teamMessageFiltering.ts create mode 100644 test/renderer/utils/teamMessageFiltering.test.ts diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 4bfe84b0..f8010eaf 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -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 ?? ''); diff --git a/src/renderer/components/team/messages/MessageComposer.tsx b/src/renderer/components/team/messages/MessageComposer.tsx index 7f5ffae1..eeeaed6e 100644 --- a/src/renderer/components/team/messages/MessageComposer.tsx +++ b/src/renderer/components/team/messages/MessageComposer.tsx @@ -442,7 +442,14 @@ export const MessageComposer = ({ setTeamSelectorOpen(false); }} > - + {target.color ? ( + + ) : ( + + )}
{target.displayName} diff --git a/src/renderer/utils/teamMessageFiltering.ts b/src/renderer/utils/teamMessageFiltering.ts new file mode 100644 index 00000000..2087bb7c --- /dev/null +++ b/src/renderer/utils/teamMessageFiltering.ts @@ -0,0 +1,54 @@ +import { isInboxNoiseMessage } from '@shared/utils/inboxNoise'; + +import type { InboxMessage } from '@shared/types'; + +export interface TeamMessagesFilter { + from: Set; + to: Set; + 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; +} diff --git a/test/renderer/utils/teamMessageFiltering.test.ts b/test/renderer/utils/teamMessageFiltering.test.ts new file mode 100644 index 00000000..123d6462 --- /dev/null +++ b/test/renderer/utils/teamMessageFiltering.test.ts @@ -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 { + 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'); + }); +});