1import type {
2 CheckpointId,
3 CheckpointRef,
4 CheckpointScopeId,
5 MessageId,
6 OrchestrationV2ThreadProjection,
7 RunId,
8} from "@t3tools/contracts";
9import * as DateTime from "effect/DateTime";
10
11export interface ThreadCheckpointSummary {
12 readonly checkpointId?: CheckpointId;
13 readonly scopeId?: CheckpointScopeId;
14 readonly runId: RunId;
15 readonly checkpointTurnCount: number;
16 readonly checkpointRef: CheckpointRef;
17 readonly status: "ready" | "missing" | "error" | "stale";
18 readonly files: ReadonlyArray<{
19 readonly path: string;
20 readonly kind: string;
21 readonly additions: number;
22 readonly deletions: number;
23 }>;
24 readonly assistantMessageId: MessageId | null;
25 readonly completedAt: string;
26}
27
28/** Derives the checkpoint/diff rows needed by review UIs from native V2 entities. */
29export function deriveThreadCheckpointSummaries(
30 projection: OrchestrationV2ThreadProjection,
31): ReadonlyArray<ThreadCheckpointSummary> {
32 return projection.checkpoints.flatMap((checkpoint) => {
33 if (checkpoint.appRunOrdinal === null || checkpoint.runId === null) return [];
34 const assistantMessageId =
35 projection.messages.findLast(
36 (message) => message.runId === checkpoint.runId && message.role === "assistant",
37 )?.id ?? null;
38 return [
39 {
40 checkpointId: checkpoint.id,
41 scopeId: checkpoint.scopeId,
42 runId: checkpoint.runId,
43 checkpointTurnCount: checkpoint.appRunOrdinal,
44 checkpointRef: checkpoint.ref,
45 status: checkpoint.status,
46 files: checkpoint.files,
47 assistantMessageId,
48 completedAt: DateTime.formatIso(checkpoint.capturedAt),
49 },
50 ];
51 });
52}