| 1 | import { compactArchivedToolItems } from "./archivedToolItems"; |
| 2 | import { duplicateLiveItemIds } from "./hydrateHistoryApply"; |
| 3 | import { historyRevisionIsOlder } from "./sessionTranscriptMode"; |
| 4 | import type { HistoryMutation, Item } from "./useController"; |
| 5 | import { canonicalUserConfirmations, settleLocalSubmissions, type LocalSubmissionFields } from "./localSubmissionState"; |
| 6 | import { reconcileSessionOperationItems } from "./sessionMaintenanceOperation"; |
| 7 | |
| 8 | type WindowFields = LocalSubmissionFields & { |
| 9 | items: Item[]; |
| 10 | historyPrefixCount: number; |
| 11 | hydrateHistoryLoaded?: boolean; |
| 12 | hydratePlaceholderItems?: Item[]; |
| 13 | historyStartTurn: number; |
| 14 | historyEndTurn: number; |
| 15 | historyTotalTurns: number; |
| 16 | historyHasOlder: boolean; |
| 17 | historyHasNewer: boolean; |
| 18 | historyOlderLoading: boolean; |
| 19 | historyOlderError?: string; |
| 20 | historyNewerLoading: boolean; |
| 21 | historyNewerError?: string; |
| 22 | historyRevision?: number; |
| 23 | historyDigest?: string; |
| 24 | historyLayoutRevision: number; |
| 25 | historyMutation: HistoryMutation; |
| 26 | pendingSubmissionId?: string; |
| 27 | }; |
| 28 | |
| 29 | export type HistoryWindowMutationAction = { |
| 30 | type: "history_replace" | "history_rebase" | "history_prepend" | "history_append"; |
| 31 | items: Item[]; |
| 32 | removeIds?: string[]; |
| 33 | startTurn: number; |
| 34 | endTurn?: number; |
| 35 | totalTurns: number; |
| 36 | hasOlder: boolean; |
| 37 | hasNewer?: boolean; |
| 38 | revision?: number; |
| 39 | digest?: string; |
| 40 | }; |
| 41 | |
| 42 | function common<S extends WindowFields>(state: S, action: HistoryWindowMutationAction, items: Item[], prefix: number, kind: "replace" | "prepend" | "append"): S { |
| 43 | return { |
| 44 | ...state, |
| 45 | items: compactArchivedToolItems(items), |
| 46 | historyPrefixCount: prefix, |
| 47 | hydrateHistoryLoaded: true, |
| 48 | hydratePlaceholderItems: undefined, |
| 49 | historyStartTurn: action.startTurn, |
| 50 | historyEndTurn: action.endTurn ?? (kind === "prepend" ? state.historyEndTurn : action.totalTurns), |
| 51 | historyTotalTurns: action.totalTurns, |
| 52 | historyHasOlder: action.hasOlder, |
| 53 | historyHasNewer: Boolean(action.hasNewer), |
| 54 | historyOlderLoading: false, |
| 55 | historyOlderError: undefined, |
| 56 | historyNewerLoading: false, |
| 57 | historyNewerError: undefined, |
| 58 | historyRevision: action.revision, |
| 59 | historyDigest: action.digest, |
| 60 | historyMutation: { seq: state.historyMutation.seq + 1, kind }, |
| 61 | }; |
| 62 | } |
| 63 | |
| 64 | export function reduceHistoryWindowState<S extends WindowFields>(state: S, action: HistoryWindowMutationAction): S { |
| 65 | if (historyRevisionIsOlder(state.historyRevision, action.revision)) return state; |
| 66 | if (action.type === "history_replace") { |
| 67 | const items = reconcileSessionOperationItems(action.items, state.items); |
| 68 | return settleLocalSubmissions(common(state, action, items, items.length, "replace"), items); |
| 69 | } |
| 70 | if (action.type === "history_rebase") { |
| 71 | const liveTail = state.items.slice(Math.min(state.historyPrefixCount, state.items.length)); |
| 72 | const duplicates = new Set(duplicateLiveItemIds(action.items, liveTail)); |
| 73 | const items = reconcileSessionOperationItems([...action.items, ...liveTail.filter((item) => !duplicates.has(item.id))], state.items); |
| 74 | return settleLocalSubmissions({ ...common(state, action, items, action.items.length, "replace"), historyLayoutRevision: state.historyLayoutRevision + 1 }, items, canonicalUserConfirmations(action.items)); |
| 75 | } |
| 76 | if (action.type === "history_prepend") { |
| 77 | const remove = action.removeIds?.length ? new Set(action.removeIds) : undefined; |
| 78 | const rest = remove ? state.items.filter((item) => !remove.has(item.id)) : state.items; |
| 79 | const prefix = state.items.slice(0, Math.min(state.historyPrefixCount, state.items.length)); |
| 80 | const retainedPrefix = remove ? prefix.filter((item) => !remove.has(item.id)) : prefix; |
| 81 | const incoming = new Set(action.items.map(item => item.id)); |
| 82 | const items = reconcileSessionOperationItems([...new Map(action.items.map(item => [item.id, item])).values(), ...rest.filter(item => !incoming.has(item.id))], state.items); |
| 83 | return settleLocalSubmissions(common(state, action, items, action.items.length + retainedPrefix.filter(item => !incoming.has(item.id)).length, "prepend"), items, canonicalUserConfirmations(action.items)); |
| 84 | } |
| 85 | const items = reconcileSessionOperationItems(action.items, state.items); |
| 86 | return settleLocalSubmissions({ |
| 87 | ...common(state, action, items, items.length, "append"), |
| 88 | historyLayoutRevision: state.historyLayoutRevision + 1, |
| 89 | }, items); |
| 90 | } |
| 91 |