From abe26ddcc4d21694e8223b6307882d65126cdfc9 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 6 May 2026 23:30:51 +0300 Subject: [PATCH] perf(team): skip data refresh for log activity pulses --- src/renderer/store/index.ts | 26 ++++++---- .../renderer/store/teamChangeThrottle.test.ts | 50 ++++++++++++++++--- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts index 426c1523..225c165b 100644 --- a/src/renderer/store/index.ts +++ b/src/renderer/store/index.ts @@ -581,15 +581,19 @@ export function initializeNotificationListeners(): () => void { }; const markTaskLogActivity = (teamName: string, taskId: string): void => { clearTaskLogActivityTimer(teamName, taskId); - useStore.setState((prev) => ({ - activeTaskLogActivityByTeam: { - ...prev.activeTaskLogActivityByTeam, - [teamName]: { - ...(prev.activeTaskLogActivityByTeam[teamName] ?? {}), - [taskId]: true, + const isAlreadyActive = + useStore.getState().activeTaskLogActivityByTeam[teamName]?.[taskId] === true; + if (!isAlreadyActive) { + useStore.setState((prev) => ({ + activeTaskLogActivityByTeam: { + ...prev.activeTaskLogActivityByTeam, + [teamName]: { + ...(prev.activeTaskLogActivityByTeam[teamName] ?? {}), + [taskId]: true, + }, }, - }, - })); + })); + } const timerKey = buildTaskLogActivityTimerKey(teamName, taskId); const timer = setTimeout(() => { taskLogActivityTimers.delete(timerKey); @@ -1703,9 +1707,13 @@ export function initializeNotificationListeners(): () => void { seedCurrentRunIdIfMissing(); const visible = isTeamVisibleInAnyPane(event.teamName); if (event.taskId && visible) { - if (isTaskLogActivityChangeEvent(event)) { + const isLogActivitySignal = isTaskLogActivityChangeEvent(event); + if (isLogActivitySignal) { markTaskLogActivity(event.teamName, event.taskId); } + if (event.taskSignalKind === 'log') { + return; + } const existingDetailTimer = teamRefreshTimers.get(event.teamName); noteTeamRefreshFanout({ teamName: event.teamName, diff --git a/test/renderer/store/teamChangeThrottle.test.ts b/test/renderer/store/teamChangeThrottle.test.ts index 27cea90a..a93d68c5 100644 --- a/test/renderer/store/teamChangeThrottle.test.ts +++ b/test/renderer/store/teamChangeThrottle.test.ts @@ -1623,7 +1623,7 @@ describe('team change throttling', () => { expect(useStore.getState().activeTaskLogActivityByTeam['my-team']).toBeUndefined(); }); - it('schedules a bounded team data refresh for visible task log signals', async () => { + it('pulses visible task log activity without refreshing team data for explicit log signals', async () => { const state = useStore.getState(); const refreshTeamDataSpy = vi.spyOn(state, 'refreshTeamData'); @@ -1638,11 +1638,13 @@ describe('team change throttling', () => { ); expect(refreshTeamDataSpy).not.toHaveBeenCalled(); + expect(useStore.getState().activeTaskLogActivityByTeam['my-team']).toEqual({ + 'task-live': true, + }); await vi.advanceTimersByTimeAsync(800); - expect(refreshTeamDataSpy).toHaveBeenCalledTimes(1); - expect(refreshTeamDataSpy).toHaveBeenCalledWith('my-team', { withDedup: true }); + expect(refreshTeamDataSpy).not.toHaveBeenCalled(); }); it('refreshes visible team data for task change freshness without pulsing live log activity', async () => { @@ -1667,6 +1669,30 @@ describe('team change throttling', () => { expect(refreshTeamDataSpy).toHaveBeenCalledWith('my-team', { withDedup: true }); }); + it('keeps the bounded team data refresh for legacy task log change events', async () => { + const state = useStore.getState(); + const refreshTeamDataSpy = vi.spyOn(state, 'refreshTeamData'); + + hoisted.onTeamChangeCb?.( + {}, + { + type: 'task-log-change', + teamName: 'my-team', + taskId: 'task-live', + detail: 'opencode-runtime-task-event:start', + } + ); + + expect(useStore.getState().activeTaskLogActivityByTeam['my-team']).toEqual({ + 'task-live': true, + }); + + await vi.advanceTimersByTimeAsync(800); + + expect(refreshTeamDataSpy).toHaveBeenCalledTimes(1); + expect(refreshTeamDataSpy).toHaveBeenCalledWith('my-team', { withDedup: true }); + }); + it('skips the bounded task log refresh if the team is hidden before execution', async () => { const state = useStore.getState(); const refreshTeamDataSpy = vi.spyOn(state, 'refreshTeamData'); @@ -1696,6 +1722,12 @@ describe('team change throttling', () => { it('extends task log activity pulse on repeated log signals and ignores hidden teams', async () => { const state = useStore.getState(); const refreshTeamDataSpy = vi.spyOn(state, 'refreshTeamData'); + const activitySnapshots: Array | undefined> = []; + const unsubscribeActivitySnapshots = useStore.subscribe((nextState, prevState) => { + if (nextState.activeTaskLogActivityByTeam !== prevState.activeTaskLogActivityByTeam) { + activitySnapshots.push(nextState.activeTaskLogActivityByTeam['my-team']); + } + }); hoisted.onTeamChangeCb?.({}, { type: 'task-log-change', @@ -1704,8 +1736,10 @@ describe('team change throttling', () => { taskSignalKind: 'log', }); + expect(activitySnapshots).toEqual([{ 'task-live': true }]); + await vi.advanceTimersByTimeAsync(2000); - expect(refreshTeamDataSpy).toHaveBeenCalledTimes(1); + expect(refreshTeamDataSpy).not.toHaveBeenCalled(); hoisted.onTeamChangeCb?.({}, { type: 'task-log-change', @@ -1714,14 +1748,17 @@ describe('team change throttling', () => { taskSignalKind: 'log', }); + expect(activitySnapshots).toEqual([{ 'task-live': true }]); + await vi.advanceTimersByTimeAsync(2499); - expect(refreshTeamDataSpy).toHaveBeenCalledTimes(2); + expect(refreshTeamDataSpy).not.toHaveBeenCalled(); expect(useStore.getState().activeTaskLogActivityByTeam['my-team']).toEqual({ 'task-live': true, }); await vi.advanceTimersByTimeAsync(1); expect(useStore.getState().activeTaskLogActivityByTeam['my-team']).toBeUndefined(); + expect(activitySnapshots).toEqual([{ 'task-live': true }, undefined]); useStore.setState({ paneLayout: { @@ -1740,7 +1777,8 @@ describe('team change throttling', () => { expect(useStore.getState().activeTaskLogActivityByTeam['my-team']).toBeUndefined(); await vi.advanceTimersByTimeAsync(800); - expect(refreshTeamDataSpy).toHaveBeenCalledTimes(2); + expect(refreshTeamDataSpy).not.toHaveBeenCalled(); + unsubscribeActivitySnapshots(); }); it('applies targeted tool resets without clearing sibling tools', async () => {