From bf9850a6a91b4afa868be2bb7fc456113a609bf4 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Sun, 4 Oct 2026 00:03:33 +0800 Subject: [PATCH] perf(activity): keep only loaded activity pages in the renderer, driven by Main's summary The renderer no longer holds the whole history. Startup sends queued changes, lets Main reconcile leftover pending/running records against the stored tasks (same localized interruptedOnRestart line), then loads the first page (200). The Activity page loads a full visible batch (500, as before paging); "load more" shows the next batch and loads the next page behind it; a filter switch refetches that filter's first page; leaving the page trims to one batch. Counts, project/conversation groups, titles, conversation status and the clear count come from activityHistory.summary (refreshed on entry, by the page timer, the refresh button and after load more / filter changes). Records that are not loaded are still updated incrementally: the update contract gains content-matched changes (upsert-call, update-request, resolve-approval, settle-request, remove-request, remove-call) that Main applies in SQL to shown rows; the store applies the same semantics to its pages through the shared reference. update returns Main's stored call records so a call whose earlier record was not loaded keeps one id. Task refresh reconciles via Main SQL. Legacy localStorage records migrate as incremental changes; the renderer no longer calls replace. Tests: randomized sequences of every action, page loads, trims, flushes and injected failures against the former full-list code on a real database; first page <= 200 after startup; flush payload depends only on changed records (including unloaded ones); ActivityPanel output with pages + summary equals the full-list rendering for randomized datasets with duplicates; restart interrupts leftover records with the same detail; clear empties the database, pages and summary; SQL change semantics equal the reference. --- .../activity-history-repository.test.ts | 31 +- .../assistant/activity-history-repository.ts | 105 ++++- src/main/assistant/assistant-database.ts | 5 +- src/main/ipc.ts | 9 +- src/preload/index.ts | 6 +- src/renderer/src/ActivityPanel.test.tsx | 93 ++++ src/renderer/src/ActivityPanel.tsx | 149 ++++-- src/renderer/src/App.test.tsx | 68 ++- src/renderer/src/App.tsx | 40 +- src/renderer/src/activity-selectors.ts | 30 +- src/renderer/src/activity-store.ts | 446 +++++++++++------- src/renderer/src/activity-sync.test.ts | 394 ++++++++++------ src/renderer/src/activity-sync.ts | 263 ++++++++--- src/shared/activity-history-reference.ts | 85 +++- src/shared/assistant-contracts.ts | 60 ++- src/shared/contracts.ts | 3 +- 16 files changed, 1294 insertions(+), 493 deletions(-) diff --git a/src/main/assistant/activity-history-repository.test.ts b/src/main/assistant/activity-history-repository.test.ts index 9bd65e51..713f5071 100644 --- a/src/main/assistant/activity-history-repository.test.ts +++ b/src/main/assistant/activity-history-repository.test.ts @@ -5,8 +5,9 @@ import { join, resolve } from 'node:path' import { DatabaseSync } from 'node:sqlite' import { build } from 'esbuild' import { afterEach, describe, expect, it } from 'vitest' -import type { ActivityRecord, AssistantTask } from '../../shared/assistant-contracts' +import type { ActivityHistoryChange, ActivityRecord, AssistantTask } from '../../shared/assistant-contracts' import { + applyActivityChanges, firstOccurrences, referenceActivityPage, referenceActivityReconcile, @@ -89,6 +90,34 @@ describe('ActivityHistoryRepository', () => { } }) + it('applies content-matched changes in SQL exactly like the reference, including legacy duplicates', () => { + for (const seed of [4, 8, 15]) { + const random = makeRandom(seed) + const pick = (items: readonly T[]): T => items[Math.floor(random() * items.length)]! + const { database } = open() + let list = randomList(random, 150).map((item, index) => + item.kind === 'tool' || item.kind === 'subagent' ? { ...item, callId: `call-${index % 4}` } : item) + database.replaceActivityHistory({ records: list, legacyHistoryMayBeIncomplete: false }) + list = firstOccurrences(list) + for (let round = 0; round < 80; round++) { + const requestId = `request-${Math.floor(random() * 10)}` + const status = pick(statuses) + const changes: ActivityHistoryChange[] = [pick([ + (): ActivityHistoryChange => ({ type: 'upsert-call', record: { ...randomList(random, 1)[0]!, id: `new-${round}`, + kind: pick(['tool', 'subagent'] as const), requestId, callId: `call-${Math.floor(random() * 4)}` } }), + (): ActivityHistoryChange => ({ type: 'update-request', requestId, status, ...(random() < 0.5 ? { detail: 'd' } : {}) }), + (): ActivityHistoryChange => ({ type: 'resolve-approval', conversationId: `conversation-${Math.floor(random() * 6)}`, status, detailLine: 'x' }), + (): ActivityHistoryChange => ({ type: 'settle-request', requestId, status, detailLine: 'y' }), + (): ActivityHistoryChange => ({ type: 'remove-request', requestId }), + (): ActivityHistoryChange => ({ type: 'remove-call', requestId, callId: `call-${Math.floor(random() * 4)}` }) + ])()] + database.updateActivityHistory({ changes }) + list = applyActivityChanges(list, changes) + expect(firstOccurrences(database.getActivityHistory().records)).toEqual(list) + } + } + }) + it('reconciles with the stored tasks like the former renderer code', () => { const { database } = open() const random = makeRandom(5) diff --git a/src/main/assistant/activity-history-repository.ts b/src/main/assistant/activity-history-repository.ts index 5523ad37..93c16f92 100644 --- a/src/main/assistant/activity-history-repository.ts +++ b/src/main/assistant/activity-history-repository.ts @@ -8,6 +8,7 @@ import { type ActivityHistoryPage, type ActivityHistorySnapshot, type ActivityHistorySummary, + type ActivityHistoryUpdateResult, type ActivityRecord } from '../../shared/assistant-contracts' import { @@ -275,9 +276,15 @@ export class ActivityHistoryRepository { }) } - /** Applies incremental changes in one transaction, without reading the history. */ - update(input: unknown): void { + /** + * Applies incremental changes in one transaction, without reading the + * history. Returns the stored record of every upsert-call, which keeps + * the id and createdAt of an earlier record of the call the renderer may + * not have loaded. + */ + update(input: unknown): ActivityHistoryUpdateResult { const update = activityHistoryUpdateSchema.parse(input) + const calls: ActivityRecord[] = [] this.write(() => { const database = this.database const nextSeq = database.prepare( @@ -294,25 +301,95 @@ export class ActivityHistoryRepository { 'UPDATE activity_history_records SET record_json = ? WHERE record_key = ? AND record_json != ?' ) const remove = database.prepare('DELETE FROM activity_history_records WHERE record_id = ?') + const seq = (): number => (nextSeq.get() as { seq: number }).seq + // Shown rows (first occurrences) a content-matched change applies to, newest first. + type Row = { record_key: string; order_seq: number; record: ActivityRecord } + const select = (where: string, limit: number, ...args: SqlValue[]): Row[] => + (database.prepare( + `SELECT record_key, order_seq, record_json FROM activity_history_records + WHERE occurrence = 0 AND ${where} ORDER BY order_seq DESC LIMIT ${limit}` + ).all(...args) as Array<{ record_key: string; order_seq: number; record_json: string }>) + .map((row) => ({ record_key: row.record_key, order_seq: row.order_seq, record: parseRow(row.record_json) })) + const frontSeq = database.prepare( + 'SELECT MAX(order_seq) AS seq FROM activity_history_records WHERE occurrence = 0' + ) + const rewrite = (key: string, record: ActivityRecord): void => { + const json = JSON.stringify(activityRecordSchema.parse(record)) + replaceInPlace.run(json, key, json) + } for (const change of update.changes) { - if (change.type === 'remove-duplicates') { - database.exec('DELETE FROM activity_history_records WHERE occurrence != 0') - } else if (change.type === 'remove') { - remove.run(change.id) - } else { - // The first occurrence stays first when moved to the front, so its key is stable. - const key = activityRecordKey(change.record.id, 0) - const json = JSON.stringify(change.record) - const seq = (): number => (nextSeq.get() as { seq: number }).seq - if (!exists.get(key)) insert.run(key, json, seq()) - else if (change.position === 'front') moveToFront.run(json, seq(), key) - else replaceInPlace.run(json, key, json) + switch (change.type) { + case 'remove-duplicates': + database.exec('DELETE FROM activity_history_records WHERE occurrence != 0') + break + case 'remove': + remove.run(change.id) + break + case 'upsert': { + // The first occurrence stays first when moved to the front, so its key is stable. + const key = activityRecordKey(change.record.id, 0) + const json = JSON.stringify(change.record) + if (!exists.get(key)) insert.run(key, json, seq()) + else if (change.position === 'front') moveToFront.run(json, seq(), key) + else replaceInPlace.run(json, key, json) + break + } + case 'upsert-call': { + const incoming = change.record + const [existing] = select( + "request_id = ? AND kind = ? AND json_extract(record_json, '$.callId') = ?", 1, + incoming.requestId, incoming.kind, incoming.callId! + ) + if (!existing) { + insert.run(activityRecordKey(incoming.id, 0), JSON.stringify(incoming), seq()) + calls.push(incoming) + break + } + const before = existing.record + const unchanged = before.conversationId === incoming.conversationId && before.title === incoming.title && + before.detail === incoming.detail && before.status === incoming.status + const front = existing.order_seq === (frontSeq.get() as { seq: number }).seq + const next = unchanged ? before : { ...incoming, id: before.id, createdAt: before.createdAt, scope: before.scope } + calls.push(next) + if (unchanged && front) break + moveToFront.run(JSON.stringify(activityRecordSchema.parse(next)), seq(), existing.record_key) + break + } + case 'update-request': + for (const row of select("request_id = ? AND kind = 'request'", -1, change.requestId)) { + rewrite(row.record_key, { ...row.record, status: change.status, detail: change.detail ?? row.record.detail }) + } + break + case 'resolve-approval': { + const [row] = select("conversation_id = ? AND kind = 'approval' AND status = 'pending'", 1, + change.conversationId) + if (row) { + rewrite(row.record_key, { ...row.record, status: change.status, detail: `${row.record.detail}\n${change.detailLine}` }) + } + break + } + case 'settle-request': + for (const row of select("request_id = ? AND kind != 'request' AND status_group = 'active'", -1, + change.requestId)) { + rewrite(row.record_key, { ...row.record, status: change.status, detail: `${row.record.detail}\n${change.detailLine}` }) + } + break + case 'remove-request': + for (const row of select('request_id = ?', -1, change.requestId)) remove.run(row.record.id) + break + case 'remove-call': + for (const row of select( + "request_id = ? AND kind = 'tool' AND json_extract(record_json, '$.callId') = ?", -1, + change.requestId, change.callId + )) remove.run(row.record.id) + break } } if (update.legacyHistoryMayBeIncomplete !== undefined) { this.writeIncomplete(update.legacyHistoryMayBeIncomplete) } }) + return { calls } } /** diff --git a/src/main/assistant/assistant-database.ts b/src/main/assistant/assistant-database.ts index db5d3d7c..4486d76c 100644 --- a/src/main/assistant/assistant-database.ts +++ b/src/main/assistant/assistant-database.ts @@ -41,6 +41,7 @@ import type { ActivityHistoryPage, ActivityHistorySnapshot, ActivityHistorySummary, + ActivityHistoryUpdateResult, ConversationQueueItem, ConversationAnsweredQuestion, ConversationBranchInput, @@ -5310,8 +5311,8 @@ export class AssistantDatabase { } /** Applies incremental changes in one transaction (PERF-15). */ - updateActivityHistory(input: unknown): void { - this.activityHistory().update(input) + updateActivityHistory(input: unknown): ActivityHistoryUpdateResult { + return this.activityHistory().update(input) } /** Deletes every activity record in one transaction. */ diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 7e318163..09773725 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -6869,7 +6869,7 @@ export function registerIpcHandlers( ipcChannels.activityHistoryUpdate, (event, input: unknown) => { assertTrustedSender(event, window) - assistantDatabase.updateActivityHistory(input) + return assistantDatabase.updateActivityHistory(input) } ) registerHandler(ipcChannels.activityHistoryClear, (event) => { @@ -6886,12 +6886,7 @@ export function registerIpcHandlers( }) registerHandler(ipcChannels.activityHistoryReconcile, (event, input: unknown) => { assertTrustedSender(event, window) - const request = activityHistoryReconcileRequestSchema.parse(input) - // Requests still running in Main are active too, whichever window started them. - return assistantDatabase.reconcileActivityHistory({ - ...request, - activeRequestIds: [...new Set([...request.activeRequestIds, ...activeRequests.keys()])] - }) + return assistantDatabase.reconcileActivityHistory(activityHistoryReconcileRequestSchema.parse(input)) }) registerHandler(ipcChannels.tokenUsageSummary, (event) => { diff --git a/src/preload/index.ts b/src/preload/index.ts index 18d2bd43..fe761df7 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -74,6 +74,7 @@ import type { ActivityHistorySummary, ActivityHistorySummaryRequest, ActivityHistoryUpdate, + ActivityHistoryUpdateResult, ActivityRecord, TokenUsageSummary, ConversationBranchInput, @@ -1296,9 +1297,8 @@ const desktopApi: DesktopApi = { legacyHistoryMayBeIncomplete }) }, - update: async (update: ActivityHistoryUpdate) => { - await ipcRenderer.invoke(ipcChannels.activityHistoryUpdate, update) - }, + update: (update: ActivityHistoryUpdate) => + ipcRenderer.invoke(ipcChannels.activityHistoryUpdate, update) as Promise, clear: async () => { await ipcRenderer.invoke(ipcChannels.activityHistoryClear) }, diff --git a/src/renderer/src/ActivityPanel.test.tsx b/src/renderer/src/ActivityPanel.test.tsx index 22113bfd..adf744b7 100644 --- a/src/renderer/src/ActivityPanel.test.tsx +++ b/src/renderer/src/ActivityPanel.test.tsx @@ -991,3 +991,96 @@ describe('ActivityPanel', () => { expect(screen.queryByRole('button', { name: '刷新' })).not.toBeInTheDocument() }) }) + +describe('ActivityPanel paged by Main', () => { + function makeRandom(seed: number): () => number { + let state = seed + return () => (state = (state * 1664525 + 1013904223) >>> 0) / 4294967296 + } + function randomList(random: () => number, length: number): ActivityRecord[] { + const pick = (items: readonly T[]): T => items[Math.floor(random() * items.length)]! + const list: ActivityRecord[] = [] + for (let index = 0; index < length; index++) { + const duplicate = list.length > 0 && random() < 0.08 + list.push({ + id: duplicate ? pick(list).id : `random-${index}`, + conversationId: `conversation-${Math.floor(random() * 5)}`, + requestId: `request-${Math.floor(random() * 8)}`, + scope: random() < 0.5 ? { kind: 'project', projectId: 'p1', projectName: '项目一' } : { kind: 'global' }, + kind: pick(['request', 'tool', 'approval', 'subagent', 'result'] as const), + title: `标题 ${index}`, + detail: `详情 ${index}`, + status: pick(['pending', 'running', 'completed', 'failed', 'denied', 'cancelled', 'interrupted'] as const), + createdAt: Date.UTC(2026, 0, 1) + Math.floor(random() * 30) * 1000 + }) + } + return list + } + const filterButtons = (container: HTMLElement): HTMLButtonElement[] => + [...container.querySelectorAll('.activity-panel__toolbar [role="group"] button')] + /** The visible text and status classes of the page; what the user can compare. */ + function snapshot(container: HTMLElement): string { + return [...container.querySelectorAll('.activity-panel__toolbar, .activity-project, .activity-panel__load-more')] + .map((element) => `${element.textContent ?? ''}|${[...element.querySelectorAll('.status-badge')].map((badge) => badge.className).join(',')}`) + .join('\n') + } + + it('shows the same counts, groups, titles, status and clear count as the full-list computation', async () => { + const { firstOccurrences, referenceActivitySummary } = await import('../../shared/activity-history-reference') + for (const seed of [2, 13, 77, 404]) { + const random = makeRandom(seed) + const stored = randomList(random, 120 + Math.floor(random() * 500)) + // The former page got the whole shown list (first occurrences). + const full = firstOccurrences(stored) + const props = { tokenUsage: makeTokenUsage(), onClear: vi.fn(), onOpenConversation: vi.fn() } + for (const filter of ['all', 'active', 'failed'] as const) { + const before = render() + if (filter !== 'all') { + fireEvent.click(filterButtons(before.container)[filter === 'active' ? 1 : 2]!) + } + const expected = snapshot(before.container) + before.unmount() + // Now: only a loaded prefix (as Main pages it) plus Main's summary. + const matching = full.filter((record) => + filter === 'all' ? true : filter === 'active' + ? record.status === 'pending' || record.status === 'running' + : ['failed', 'denied', 'cancelled', 'interrupted'].includes(record.status)) + const loaded = matching.slice(0, 500) + const summary = referenceActivitySummary(stored, [...new Set(loaded.map((record) => record.conversationId))]) + const after = render( + + ) + expect(snapshot(after.container)).toBe(expected) + after.unmount() + } + } + }) + + it('loads the next page behind "load more" and switches filters through the callbacks', () => { + const records = Array.from({ length: 500 }, (_, index) => makeRecord(index)) + const onLoadMore = vi.fn() + const onFilterChange = vi.fn() + const summary = { counts: { all: 1_200, active: 0, failed: 0 }, conversations: {}, legacyHistoryMayBeIncomplete: false } + const { container } = render( + + ) + expect(container.querySelectorAll('.activity-list__item')).toHaveLength(500) + const loadMore = container.querySelector('.activity-panel__load-more')! + expect(loadMore.textContent).toContain('700') + fireEvent.click(loadMore) + expect(onLoadMore).toHaveBeenCalledOnce() + fireEvent.click(filterButtons(container)[2]!) + expect(onFilterChange).toHaveBeenCalledWith('failed') + expect(container.textContent).toContain('1,200') + }) +}) \ No newline at end of file diff --git a/src/renderer/src/ActivityPanel.tsx b/src/renderer/src/ActivityPanel.tsx index 462fa81a..e569c5c9 100644 --- a/src/renderer/src/ActivityPanel.tsx +++ b/src/renderer/src/ActivityPanel.tsx @@ -1,8 +1,9 @@ import { Activity, ChevronRight, RefreshCw, Trash2 } from 'lucide-react' -import { memo, useMemo, useState } from 'react' +import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { InlineHelp } from './InlineHelp' import type { + ActivityHistorySummary, AssistantProject, TokenUsageSummary } from '../../shared/assistant-contracts' @@ -41,7 +42,23 @@ const activityRenderBatchSize = 500 export type ActivityPanelProps = { projects?: readonly AssistantProject[] + /** + * The records to show, newest first. With `paging` they are the loaded + * pages of `paging.filter`; without, the whole history. + */ records: readonly ActivityRecord[] + /** + * Paged history (PERF-15): counts, titles and conversation status come + * from `summary`, computed by Main over the whole history; "load more" + * and filter changes go through the callbacks. + */ + paging?: { + filter: ActivityFilter + summary?: ActivityHistorySummary + hasMore: boolean + onFilterChange: (filter: ActivityFilter) => void + onLoadMore: () => void + } tokenUsage: TokenUsageSummary onClear: () => void onOpenConversation: (conversationId: string) => void @@ -187,11 +204,15 @@ function getConversationTitles( return conversationTitles } -function groupActivityRecordsByProject( - records: readonly ActivityRecord[], +type ConversationFacts = { + titles: ReadonlyMap + status: (conversationId: string, shown: readonly ActivityRecord[]) => ActivityRecord['status'] +} + +/** Titles and status from the whole list (unpaged). */ +function conversationFactsFromRecords( allRecords: readonly ActivityRecord[] -): ProjectActivityGroup[] { - const conversationTitles = getConversationTitles(allRecords) +): ConversationFacts { const allConversationRecords = new Map() for (const record of allRecords) { const conversationRecords = @@ -199,6 +220,35 @@ function groupActivityRecordsByProject( conversationRecords.push(record) allConversationRecords.set(record.conversationId, conversationRecords) } + return { + titles: getConversationTitles(allRecords), + status: (conversationId, shown) => + conversationStatus(allConversationRecords.get(conversationId) ?? shown) + } +} + +/** Titles and status Main computed over the whole history; loaded records fill gaps. */ +function conversationFactsFromSummary( + summary: ActivityHistorySummary, + loaded: readonly ActivityRecord[] +): ConversationFacts { + const fallback = conversationFactsFromRecords(loaded) + const titles = new Map(fallback.titles) + for (const [conversationId, facts] of Object.entries(summary.conversations)) { + if (facts.title !== undefined) titles.set(conversationId, facts.title) + } + return { + titles, + status: (conversationId, shown) => + summary.conversations[conversationId]?.status ?? fallback.status(conversationId, shown) + } +} + +function groupActivityRecordsByProject( + records: readonly ActivityRecord[], + facts: ConversationFacts +): ProjectActivityGroup[] { + const conversationTitles = facts.titles const projectGroups = new Map< string, { @@ -231,9 +281,7 @@ function groupActivityRecordsByProject( conversationTitles.get(conversationId) ?? items[0]!.title, records: items, latestAt: Math.max(...items.map((record) => record.createdAt)), - status: conversationStatus( - allConversationRecords.get(conversationId) ?? items - ) + status: facts.status(conversationId, items) }) ) return { @@ -254,13 +302,23 @@ export const ActivityPanel = memo(function ActivityPanel({ tokenUsage, onClear, onOpenConversation, - onRefresh + onRefresh, + paging }: ActivityPanelProps): React.JSX.Element { const { t, i18n } = useTranslation('activity') const { t: tWorkspace } = useTranslation('workspace') const [activeView, setActiveView] = useState('tasks') - const [filter, setFilter] = useState('all') + const [localFilter, setLocalFilter] = useState('all') + const filter = paging?.filter ?? localFilter + const onFilterChange = paging?.onFilterChange + const setFilter = useCallback( + (next: ActivityFilter): void => { + if (onFilterChange) onFilterChange(next) + else setLocalFilter(next) + }, + [onFilterChange] + ) const [tokenGroup, setTokenGroup] = useState>('project') const [systemTokenGroup, setSystemTokenGroup] = @@ -414,6 +472,7 @@ export const ActivityPanel = memo(function ActivityPanel({ }), [projectDisplayNames, tokenUsage] ) + const summary = paging?.summary const matchingRecords = useMemo( () => displayRecords.filter((record) => matchesFilter(record, filter)), [displayRecords, filter] @@ -422,9 +481,16 @@ export const ActivityPanel = memo(function ActivityPanel({ () => matchingRecords.slice(0, visibleRecordCount), [matchingRecords, visibleRecordCount] ) + const conversationFacts = useMemo( + () => + summary + ? conversationFactsFromSummary(summary, displayRecords) + : conversationFactsFromRecords(displayRecords), + [displayRecords, summary] + ) const projectGroups = useMemo( - () => groupActivityRecordsByProject(filteredRecords, displayRecords), - [displayRecords, filteredRecords] + () => groupActivityRecordsByProject(filteredRecords, conversationFacts), + [conversationFacts, filteredRecords] ) const timelineBounds = useMemo(() => { const timestamps = filteredRecords.map((record) => record.createdAt) @@ -450,12 +516,23 @@ export const ActivityPanel = memo(function ActivityPanel({ const selectedTimelineRecord = filteredRecords.find( (record) => record.id === selectedTimelineRecordId ) - const conversationTitles = useMemo( - () => getConversationTitles(displayRecords), - [displayRecords] - ) - const activeCount = displayRecords.filter(isActive).length - const failedCount = displayRecords.filter(isFailed).length + const conversationTitles = conversationFacts.titles + // Before the first summary arrives, the loaded records are the best estimate. + const totalCount = summary?.counts.all ?? displayRecords.length + const activeCount = + summary?.counts.active ?? displayRecords.filter(isActive).length + const failedCount = + summary?.counts.failed ?? displayRecords.filter(isFailed).length + const matchingTotal = + filter === 'all' ? totalCount : filter === 'active' ? activeCount : failedCount + // Records of this filter that exist but are not shown yet. + const remainingCount = paging + ? Math.max( + matchingTotal - filteredRecords.length, + matchingRecords.length - filteredRecords.length, + paging.hasMore ? 1 : 0 + ) + : matchingRecords.length - filteredRecords.length const filters: ReadonlyArray<{ value: ActivityFilter label: string @@ -463,8 +540,8 @@ export const ActivityPanel = memo(function ActivityPanel({ { value: 'all', label: t('filters.all', { - count: displayRecords.length, - formattedCount: formatCount(displayRecords.length) + count: totalCount, + formattedCount: formatCount(totalCount) }) }, { @@ -691,19 +768,19 @@ export const ActivityPanel = memo(function ActivityPanel({ />