From ad2f602cba442345ad0239d8c7b5c1429be73d09 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 05:58:56 +0300 Subject: [PATCH] perf(renderer): gate timeline live lead props --- .../team/messages/MessagesPanel.tsx | 19 +- .../team/messages/MessagesPanel.test.ts | 164 +++++++++++++++--- 2 files changed, 156 insertions(+), 27 deletions(-) diff --git a/src/renderer/components/team/messages/MessagesPanel.tsx b/src/renderer/components/team/messages/MessagesPanel.tsx index ead4eb12..7356deaf 100644 --- a/src/renderer/components/team/messages/MessagesPanel.tsx +++ b/src/renderer/components/team/messages/MessagesPanel.tsx @@ -53,7 +53,11 @@ import { import { useShallow } from 'zustand/react/shallow'; import { ActivityTimeline, type TimelineViewport } from '../activity/ActivityTimeline'; -import { getThoughtGroupKey, groupTimelineItems } from '../activity/LeadThoughtsGroup'; +import { + getThoughtGroupKey, + groupTimelineItems, + isLeadThought, +} from '../activity/LeadThoughtsGroup'; import { MessageExpandDialog } from '../activity/MessageExpandDialog'; import { CollapsibleTeamSection } from '../CollapsibleTeamSection'; import { @@ -691,6 +695,15 @@ export const MessagesPanel = memo(function MessagesPanel({ searchQuery: messagesSearchQuery, }); }, [effectiveMessages, leadNames, messagesFilter, messagesSearchQuery, timeWindow]); + const firstTimelineMessage = activityTimelineMessages[0]; + const hasVisibleCurrentLeadThought = + firstTimelineMessage != null && + isLeadThought(firstTimelineMessage) && + (currentLeadSessionId ? firstTimelineMessage.leadSessionId === currentLeadSessionId : true); + const timelineLeadActivity = hasVisibleCurrentLeadThought ? leadActivity : undefined; + const timelineLeadContextUpdatedAt = hasVisibleCurrentLeadThought + ? leadContextUpdatedAt + : undefined; const hasTrackedPendingReplies = useMemo( () => Object.keys(pendingRepliesByMember).length > 0, @@ -1175,8 +1188,8 @@ export const MessagesPanel = memo(function MessagesPanel({ onToggleExpandOverride={toggleExpandOverride} currentLeadSessionId={currentLeadSessionId} isTeamAlive={isTeamAlive} - leadActivity={leadActivity} - leadContextUpdatedAt={leadContextUpdatedAt} + leadActivity={timelineLeadActivity} + leadContextUpdatedAt={timelineLeadContextUpdatedAt} teamNames={teamNames} teamColorByName={teamColorByName} onTeamClick={openTeamTab} diff --git a/test/renderer/components/team/messages/MessagesPanel.test.ts b/test/renderer/components/team/messages/MessagesPanel.test.ts index 4e3d4825..3de4e687 100644 --- a/test/renderer/components/team/messages/MessagesPanel.test.ts +++ b/test/renderer/components/team/messages/MessagesPanel.test.ts @@ -54,6 +54,7 @@ const readHookState = { markRead: vi.fn(), markAllRead: vi.fn(), }; +const activityTimelineRenderSpy = vi.hoisted(() => vi.fn()); const expandedHookState = { expandedSet: new Set(), @@ -161,37 +162,44 @@ vi.mock('@renderer/components/team/activity/ActivityTimeline', () => ({ loading, revisionMessageId, onReviseMessage, + leadActivity, + leadContextUpdatedAt, }: { messages: InboxMessage[]; loading?: boolean; revisionMessageId?: string | null; onReviseMessage?: (message: InboxMessage) => void; + leadActivity?: string; + leadContextUpdatedAt?: string; }) => - React.createElement( - 'div', - { 'data-testid': 'activity-timeline' }, - loading ? React.createElement('div', null, 'timeline-loading') : null, - messages.map((message) => - React.createElement( - 'div', - { - key: message.messageId ?? `${message.from}-${message.timestamp}`, - 'data-message-id': message.messageId ?? '', - }, - `${message.messageId ?? 'no-id'}:${message.text}`, - message.messageId === revisionMessageId - ? React.createElement( - 'button', - { - type: 'button', - onClick: () => onReviseMessage?.(message), - }, - 'Edit message' - ) - : null + (() => { + activityTimelineRenderSpy({ leadActivity, leadContextUpdatedAt, messages }); + return React.createElement( + 'div', + { 'data-testid': 'activity-timeline' }, + loading ? React.createElement('div', null, 'timeline-loading') : null, + messages.map((message) => + React.createElement( + 'div', + { + key: message.messageId ?? `${message.from}-${message.timestamp}`, + 'data-message-id': message.messageId ?? '', + }, + `${message.messageId ?? 'no-id'}:${message.text}`, + message.messageId === revisionMessageId + ? React.createElement( + 'button', + { + type: 'button', + onClick: () => onReviseMessage?.(message), + }, + 'Edit message' + ) + : null + ) ) - ) - ), + ); + })(), })); vi.mock('@renderer/components/team/activity/MessageExpandDialog', () => ({ @@ -238,6 +246,7 @@ describe('MessagesPanel idle summary invariants', () => { readHookState.readSet = new Set(); readHookState.markRead.mockReset(); readHookState.markAllRead.mockReset(); + activityTimelineRenderSpy.mockClear(); expandedHookState.expandedSet = new Set(); expandedHookState.toggle.mockReset(); storeState.sendTeamMessage.mockClear(); @@ -335,6 +344,113 @@ describe('MessagesPanel idle summary invariants', () => { }); }); + it('does not pass live lead status to timeline when newest visible item is not a current lead thought', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + storeState.teamMessagesByName['atlas-hq'] = { + canonicalMessages: [makeMessage({ messageId: 'm-1', text: 'ordinary message' })], + optimisticMessages: [], + feedRevision: 'rev-1', + nextCursor: null, + hasMore: false, + lastFetchedAt: Date.now(), + loadingHead: false, + loadingOlder: false, + headHydrated: true, + }; + root.render( + React.createElement(MessagesPanel, { + teamName: 'atlas-hq', + position: 'sidebar', + onPositionChange: vi.fn(), + members: [], + tasks: [], + isTeamAlive: true, + leadActivity: 'active', + leadContextUpdatedAt: '2026-05-31T10:00:00.000Z', + currentLeadSessionId: 'lead-session-current', + timeWindow: null, + pendingRepliesByMember: {}, + onPendingReplyChange: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + expect(activityTimelineRenderSpy).toHaveBeenCalled(); + expect(activityTimelineRenderSpy.mock.lastCall?.[0]).toMatchObject({ + leadActivity: undefined, + leadContextUpdatedAt: undefined, + }); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + + it('passes live lead status when newest visible item is the current lead thought', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + storeState.teamMessagesByName['atlas-hq'] = { + canonicalMessages: [ + makeMessage({ + from: 'lead', + to: undefined, + source: 'lead_session', + leadSessionId: 'lead-session-current', + messageId: 'lead-thought-1', + text: 'thinking', + }), + ], + optimisticMessages: [], + feedRevision: 'rev-1', + nextCursor: null, + hasMore: false, + lastFetchedAt: Date.now(), + loadingHead: false, + loadingOlder: false, + headHydrated: true, + }; + root.render( + React.createElement(MessagesPanel, { + teamName: 'atlas-hq', + position: 'sidebar', + onPositionChange: vi.fn(), + members: [], + tasks: [], + isTeamAlive: true, + leadActivity: 'active', + leadContextUpdatedAt: '2026-05-31T10:00:00.000Z', + currentLeadSessionId: 'lead-session-current', + timeWindow: null, + pendingRepliesByMember: {}, + onPendingReplyChange: vi.fn(), + }) + ); + await Promise.resolve(); + }); + + expect(activityTimelineRenderSpy).toHaveBeenCalled(); + expect(activityTimelineRenderSpy.mock.lastCall?.[0]).toMatchObject({ + leadActivity: 'active', + leadContextUpdatedAt: '2026-05-31T10:00:00.000Z', + }); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + it('persists sidebar scroll position after scroll settles', async () => { vi.useFakeTimers(); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);