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.
This commit is contained in:
@@ -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 = <T,>(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)
|
||||
|
||||
Reference in New Issue
Block a user