Skip to content
Source
Sign inSign up

threadHistoryMerge.ts

188 lines6.1 KB

1import type {
2 OrchestrationV2ProjectedTurnItem,
3 OrchestrationV2ThreadProjection,
4} from "@t3tools/contracts";
5import { isOrchestrationV2TurnItemVisible } from "@t3tools/shared/orchestrationV2Timeline";
6​
7export type ThreadHistoryMeta = {
8 readonly historyCursor: string | null;
9 readonly hasMoreHistory: boolean;
10 readonly loading: boolean;
11 readonly error: string | null;
12 readonly expanded: boolean;
13 readonly latestLocalTurnOrdinal: number | null;
14};
15​
16export const EMPTY_THREAD_HISTORY_META: ThreadHistoryMeta = {
17 historyCursor: null,
18 hasMoreHistory: false,
19 loading: false,
20 error: null,
21 expanded: false,
22 latestLocalTurnOrdinal: null,
23};
24​
25export function isActiveHistoryRequestCursor(
26 requestCursor: string,
27 history: { readonly historyCursor: string | null },
28): boolean {
29 return history.historyCursor === requestCursor;
31​
32export function clearActiveHistoryLoading(
33 requestCursor: string,
34 history: ThreadHistoryMeta,
35): ThreadHistoryMeta {
36 if (!isActiveHistoryRequestCursor(requestCursor, history) || !history.loading) {
37 return history;
38 }
39 return { ...history, loading: false };
41​
42function projectedItemKey(row: OrchestrationV2ProjectedTurnItem): string {
43 return `${row.sourceThreadId}:${row.sourceItemId}`;
45​
46function renumberVisible(
47 rows: ReadonlyArray<OrchestrationV2ProjectedTurnItem>,
48): OrchestrationV2ProjectedTurnItem[] {
49 return rows.map((row, position) => (row.position === position ? row : { ...row, position }));
51​
52export function mergeOlderHistoryIntoProjection(
53 projection: OrchestrationV2ThreadProjection,
54 olderItems: ReadonlyArray<OrchestrationV2ProjectedTurnItem>,
55 work?: { readonly anchorId: string; readonly nextCursor: string | null },
56): OrchestrationV2ThreadProjection {
57 if (olderItems.length === 0 && work === undefined) {
58 return projection;
59 }
60​
61 const start =
62 work === undefined
63 ? -1
64 : projection.visibleTurnItems.findIndex((row) => row.sourceItemId === work.anchorId);
65 const anchor = projection.visibleTurnItems[start];
66 const deferred = anchor?.item.deferredWork;
67 if (work !== undefined && deferred === undefined) return projection;
68 const currentByKey = new Map(
69 projection.visibleTurnItems.map((row) => [projectedItemKey(row), row]),
70 );
71 const turnItemById = new Map(projection.turnItems.map((item) => [String(item.id), item]));
72 const pageKeys = new Set<string>();
73 const page: OrchestrationV2ProjectedTurnItem[] = [];
74 for (const pageRow of olderItems) {
75 if (
76 work &&
77 anchor &&
78 deferred &&
79 (pageRow.sourceThreadId !== anchor.sourceThreadId ||
80 pageRow.item.ordinal < anchor.item.ordinal ||
81 pageRow.item.ordinal > deferred.lastOrdinal)
82 )
83 continue;
84 const key = projectedItemKey(pageRow);
85 if (pageKeys.has(key)) continue;
86 let row = currentByKey.get(key) ?? pageRow;
87 if (
88 !currentByKey.has(key) &&
89 (row.visibility === "local" || row.sourceThreadId === projection.thread.id)
90 ) {
91 const currentItem = turnItemById.get(String(row.sourceItemId));
92 // Hidden live items stay hidden. Snapshots also retain interrupt requests and
93 // pending request cards outside the visible window; their history page shows them.
94 if (currentItem !== undefined) {
95 if (
96 (currentItem.type !== "run_interrupt_request" &&
97 currentItem.type !== "approval_request" &&
98 currentItem.type !== "user_input_request") ||
99 !isOrchestrationV2TurnItemVisible({
100 item: currentItem,
101 runs: projection.runs,
102 attempts: projection.attempts,
103 items: projection.turnItems,
104 })
105 )
106 continue;
107 row = { ...row, item: currentItem };
108 }
109 }
110 pageKeys.add(key);
111 page.push(row);
112 if (
113 (row.visibility === "local" || row.sourceThreadId === projection.thread.id) &&
114 !turnItemById.has(String(row.sourceItemId))
115 )
116 turnItemById.set(String(row.sourceItemId), row.item);
117 }
118 if (page.length === 0 && work === undefined) return projection;
119 let visible: OrchestrationV2ProjectedTurnItem[];
120 if (work && anchor && deferred) {
121 let end = start + 1;
122 while (end < projection.visibleTurnItems.length) {
123 const row = projection.visibleTurnItems[end]!;
124 if (
125 row.sourceThreadId !== anchor.sourceThreadId ||
126 row.visibility === "synthetic" ||
127 row.item.ordinal > deferred.lastOrdinal
128 )
129 break;
130 end += 1;
131 }
132 const item = { ...anchor.item };
133 if (work.nextCursor === null) delete item.deferredWork;
134 else item.deferredWork = { ...deferred, cursor: work.nextCursor };
135 const updated = { ...anchor, item };
136 const section = [
137 ...page,
138 ...projection.visibleTurnItems
139 .slice(start, end)
140 .filter((row) => !pageKeys.has(projectedItemKey(row))),
141 ];
142 const ordered = section
143 .map((row) => (row.sourceItemId === anchor.sourceItemId ? updated : row))
144 .sort((left, right) => left.item.ordinal - right.item.ordinal);
145 visible = [
146 ...projection.visibleTurnItems.slice(0, start),
147 ...ordered,
148 ...projection.visibleTurnItems.slice(end),
149 ];
150 if (anchor.visibility === "local") turnItemById.set(String(updated.item.id), updated.item);
151 } else {
152 visible = [
153 ...page,
154 ...projection.visibleTurnItems.filter((row) => !pageKeys.has(projectedItemKey(row))),
155 ];
156 }
157 if (
158 visible.length === projection.visibleTurnItems.length &&
159 visible.every((row, index) => row === projection.visibleTurnItems[index])
160 )
161 return projection;
162 return {
163 ...projection,
164 turnItems: [...turnItemById.values()],
165 visibleTurnItems: renumberVisible(visible),
166 };
168​
169export function applyHistoryPageMeta(
170 current: ThreadHistoryMeta,
171 page: {
172 readonly nextCursor: string | null;
173 readonly hasMoreHistory: boolean;
174 },
175): ThreadHistoryMeta {
176 return {
177 historyCursor: page.nextCursor,
178 hasMoreHistory: page.hasMoreHistory,
179 loading: false,
180 error: null,
181 expanded: true,
182 latestLocalTurnOrdinal: current.latestLocalTurnOrdinal,
183 };
185​
186export function shouldShowLoadEarlierControl(history: ThreadHistoryMeta): boolean {
187 return history.hasMoreHistory || history.error !== null;