authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-18 22:55:57-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-19 00:35:32-08:00
log9a8a5b3935fc9027de2af5b109a689038d6c14d1
tree0b3e9484cf7fac322b754fb919456b7ff536bd8a
parent58a8d653a8f16d3c0e8d8ff86728fcf4d28e32fb

Simplify turn diff summaries and harden checkpoint diff retries

- Remove persisted/stored patch bodies from turn diff summaries and keep metadata-only file stats - Drop checkpoint diff hydration paths in chat/diff views; rely on selected checkpoint/conversation diffs only - Sync checkpoint turn counts on session ensure and tune checkpoint diff query retry/backoff for not-ready errors - Update tests for session logic, persistence, store reducer, and provider query retry behavior

11 files changed, 197 insertions(+), 348 deletions(-)

apps/web/src/components/ChatView.tsx+85-133
...@@ -26,12 +26,11 @@ import {...@@ -26,12 +26,11 @@ import {
26 useRef,26 useRef,
27 useState,27 useState,
28} from "react";28} from "react";
29import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";29import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
30import { useDebouncedValue } from "@tanstack/react-pacer";30import { useDebouncedValue } from "@tanstack/react-pacer";
31import { type VirtualItem, useVirtualizer } from "@tanstack/react-virtual";31import { type VirtualItem, useVirtualizer } from "@tanstack/react-virtual";
32import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";32import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";
33import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";33import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";
34import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
35import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";34import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";
3635
37import { isElectron } from "../env";36import { isElectron } from "../env";
...@@ -50,7 +49,6 @@ import {...@@ -50,7 +49,6 @@ import {
50 resolveModelSlug,49 resolveModelSlug,
51} from "../model-logic";50} from "../model-logic";
52import {51import {
53 countDiffStat,
54 derivePendingApprovals,52 derivePendingApprovals,
55 derivePhase,53 derivePhase,
56 deriveTimelineEntries,54 deriveTimelineEntries,
...@@ -380,6 +378,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -380,6 +378,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
380 const dragDepthRef = useRef(0);378 const dragDepthRef = useRef(0);
381 const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});379 const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});
382 const checkpointHydrationSessionRequestRef = useRef(new Set<string>());380 const checkpointHydrationSessionRequestRef = useRef(new Set<string>());
381 const checkpointTurnCountSyncFingerprintRef = useRef(new Map<string, string>());
383382
384 const activeThread = state.threads.find((t) => t.id === threadId);383 const activeThread = state.threads.find((t) => t.id === threadId);
385 const activeThreadId = activeThread?.id ?? null;384 const activeThreadId = activeThread?.id ?? null;
...@@ -492,68 +491,6 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -492,68 +491,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
492 });491 });
493 }, [activeThreadId, dispatch, inferredCheckpointTurnCountByTurnId, turnDiffSummaries]);492 }, [activeThreadId, dispatch, inferredCheckpointTurnCountByTurnId, turnDiffSummaries]);
494493
495 const checkpointDiffHydrationTargets = useMemo(() => {
496 if (!api || !activeThreadId || !activeSessionId || turnDiffSummaries.length === 0) {
497 return [];
498 }
499 return turnDiffSummaries.flatMap((summary) => {
500 const turnCount = inferredCheckpointTurnCountByTurnId[summary.turnId];
501 if (summary.checkpointDiffLoaded || typeof turnCount !== "number") {
502 return [];
503 }
504 return [
505 {
506 turnId: summary.turnId,
507 fromTurnCount: Math.max(0, turnCount - 1),
508 toTurnCount: turnCount,
509 },
510 ];
511 });
512 }, [
513 activeSessionId,
514 activeThreadId,
515 api,
516 inferredCheckpointTurnCountByTurnId,
517 turnDiffSummaries,
518 ]);
519
520 const checkpointDiffHydrationQueries = useQueries({
521 queries: checkpointDiffHydrationTargets.map((target) =>
522 checkpointDiffQueryOptions(api, {
523 sessionId: activeSessionId,
524 threadRuntimeId: activeThreadRuntimeId,
525 fromTurnCount: target.fromTurnCount,
526 toTurnCount: target.toTurnCount,
527 cacheScope: `turn:${target.turnId}`,
528 }),
529 ),
530 });
531
532 useEffect(() => {
533 if (!activeThreadId || checkpointDiffHydrationTargets.length === 0) {
534 return;
535 }
536
537 const checkpointDiffByTurnId: Record<string, string> = {};
538 for (let index = 0; index < checkpointDiffHydrationTargets.length; index += 1) {
539 const target = checkpointDiffHydrationTargets[index];
540 const query = checkpointDiffHydrationQueries[index];
541 const diff = query?.data?.diff;
542 if (!target || !diff) {
543 continue;
544 }
545 checkpointDiffByTurnId[target.turnId] = diff;
546 }
547 if (Object.keys(checkpointDiffByTurnId).length === 0) {
548 return;
549 }
550 dispatch({
551 type: "SET_THREAD_TURN_CHECKPOINT_DIFFS",
552 threadId: activeThreadId,
553 checkpointDiffByTurnId,
554 });
555 }, [activeThreadId, checkpointDiffHydrationQueries, checkpointDiffHydrationTargets, dispatch]);
556
557 const completionSummary = useMemo(() => {494 const completionSummary = useMemo(() => {
558 if (!activeThread?.latestTurnStartedAt) return null;495 if (!activeThread?.latestTurnStartedAt) return null;
559 if (!activeThread.latestTurnCompletedAt) return null;496 if (!activeThread.latestTurnCompletedAt) return null;
...@@ -1407,6 +1344,53 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1407,6 +1344,53 @@ export default function ChatView({ threadId }: ChatViewProps) {
1407 focusComposer();1344 focusComposer();
1408 };1345 };
14091346
1347 const syncCheckpointTurnCounts = useCallback(
1348 async (input: {
1349 threadId: string;
1350 sessionId: string;
1351 threadRuntimeId: string | null;
1352 }): Promise<void> => {
1353 if (!api || turnDiffSummaries.length === 0) {
1354 return;
1355 }
1356
1357 const turnIds = turnDiffSummaries.map((summary) => summary.turnId);
1358 if (turnIds.length === 0) {
1359 return;
1360 }
1361
1362 const syncKey = `${input.threadId}:${input.sessionId}:${input.threadRuntimeId ?? "none"}`;
1363 const turnFingerprint = turnIds.join(",");
1364 if (checkpointTurnCountSyncFingerprintRef.current.get(syncKey) === turnFingerprint) {
1365 return;
1366 }
1367 checkpointTurnCountSyncFingerprintRef.current.set(syncKey, turnFingerprint);
1368
1369 try {
1370 const result = await api.providers.listCheckpoints({
1371 sessionId: input.sessionId,
1372 });
1373 const turnIdSet = new Set(turnIds);
1374 const checkpointTurnCountByTurnId = Object.fromEntries(
1375 result.checkpoints
1376 .filter((checkpoint) => checkpoint.turnCount > 0 && turnIdSet.has(checkpoint.id))
1377 .map((checkpoint) => [checkpoint.id, checkpoint.turnCount] as const),
1378 );
1379 if (Object.keys(checkpointTurnCountByTurnId).length === 0) {
1380 return;
1381 }
1382 dispatch({
1383 type: "SET_THREAD_TURN_CHECKPOINT_COUNTS",
1384 threadId: input.threadId,
1385 checkpointTurnCountByTurnId,
1386 });
1387 } catch {
1388 checkpointTurnCountSyncFingerprintRef.current.delete(syncKey);
1389 }
1390 },
1391 [api, dispatch, turnDiffSummaries],
1392 );
1393
1410 const ensureSession = useCallback(1394 const ensureSession = useCallback(
1411 async (cwdOverride?: string): Promise<EnsuredSessionInfo | null> => {1395 async (cwdOverride?: string): Promise<EnsuredSessionInfo | null> => {
1412 if (!api || !activeThread || !activeProject) return null;1396 if (!api || !activeThread || !activeProject) return null;
...@@ -1418,6 +1402,11 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1418,6 +1402,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
1418 : sessionThreadId === activeThread.codexThreadId1402 : sessionThreadId === activeThread.codexThreadId
1419 ? "resumed"1403 ? "resumed"
1420 : "fallback_new";1404 : "fallback_new";
1405 await syncCheckpointTurnCounts({
1406 threadId: activeThread.id,
1407 sessionId: activeThread.session.sessionId,
1408 threadRuntimeId: sessionThreadId,
1409 });
1421 return {1410 return {
1422 sessionId: activeThread.session.sessionId,1411 sessionId: activeThread.session.sessionId,
1423 resolvedThreadId: sessionThreadId,1412 resolvedThreadId: sessionThreadId,
...@@ -1448,6 +1437,11 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1448,6 +1437,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
1448 : resolvedThreadId === priorCodexThreadId1437 : resolvedThreadId === priorCodexThreadId
1449 ? "resumed"1438 ? "resumed"
1450 : "fallback_new";1439 : "fallback_new";
1440 await syncCheckpointTurnCounts({
1441 threadId: activeThread.id,
1442 sessionId: session.sessionId,
1443 threadRuntimeId: resolvedThreadId,
1444 });
1451 return {1445 return {
1452 sessionId: session.sessionId,1446 sessionId: session.sessionId,
1453 resolvedThreadId,1447 resolvedThreadId,
...@@ -1472,6 +1466,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1472,6 +1466,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1472 runtimeApprovalPolicy,1466 runtimeApprovalPolicy,
1473 runtimeSandboxMode,1467 runtimeSandboxMode,
1474 selectedModel,1468 selectedModel,
1469 syncCheckpointTurnCounts,
1475 ],1470 ],
1476 );1471 );
14771472
...@@ -1480,22 +1475,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1480,22 +1475,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1480 if (phase === "running" || isSending || isConnecting || isRevertingCheckpoint) return;1475 if (phase === "running" || isSending || isConnecting || isRevertingCheckpoint) return;
1481 const hasThreadIdentity = Boolean(activeThreadRuntimeId);1476 const hasThreadIdentity = Boolean(activeThreadRuntimeId);
1482 if (!hasThreadIdentity) return;1477 if (!hasThreadIdentity) return;
1483 const selectedDiffTurnId =1478 if (turnDiffSummaries.length === 0) return;
1484 state.diffOpen && state.diffThreadId === activeThreadId ? state.diffTurnId : null;
1485 const selectedDiffTurn =
1486 selectedDiffTurnId === null
1487 ? undefined
1488 : (turnDiffSummaries.find((summary) => summary.turnId === selectedDiffTurnId) ??
1489 turnDiffSummaries[0]);
1490 const selectedTurnMissingPatchBody = Boolean(
1491 selectedDiffTurn &&
1492 !selectedDiffTurn.unifiedDiff &&
1493 selectedDiffTurn.files.every((file) => !file.diff),
1494 );
1495 const hasPendingDiffHydration =
1496 turnDiffSummaries.some((summary) => !summary.checkpointDiffLoaded) ||
1497 selectedTurnMissingPatchBody;
1498 if (!hasPendingDiffHydration) return;
14991479
1500 const requestKey = `${activeThreadId}:${activeThreadRuntimeId ?? "none"}`;1480 const requestKey = `${activeThreadId}:${activeThreadRuntimeId ?? "none"}`;
1501 if (checkpointHydrationSessionRequestRef.current.has(requestKey)) {1481 if (checkpointHydrationSessionRequestRef.current.has(requestKey)) {
...@@ -1522,9 +1502,6 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1522,9 +1502,6 @@ export default function ChatView({ threadId }: ChatViewProps) {
1522 isSending,1502 isSending,
1523 phase,1503 phase,
1524 setThreadError,1504 setThreadError,
1525 state.diffOpen,
1526 state.diffThreadId,
1527 state.diffTurnId,
1528 turnDiffSummaries,1505 turnDiffSummaries,
1529 ]);1506 ]);
15301507
...@@ -2739,50 +2716,32 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -2739,50 +2716,32 @@ const MessagesTimeline = memo(function MessagesTimeline({
2739 row.message.id,2716 row.message.id,
2740 );2717 );
2741 if (!turnSummary) return null;2718 if (!turnSummary) return null;
2742 const isCheckpointDiffLoading =2719 const summaryStat = turnSummary.files.reduce(
2743 !turnSummary.checkpointDiffLoaded && turnSummary.files.length === 0;2720 (acc, file) => {
2744 const summaryStat = turnSummary.unifiedDiff2721 if (typeof file.additions !== "number" || typeof file.deletions !== "number") {
2745 ? countDiffStat(turnSummary.unifiedDiff)2722 return acc;
2746 : turnSummary.files.reduce(2723 }
2747 (acc, file) => {2724 return {
2748 const next =2725 additions: acc.additions + file.additions,
2749 typeof file.additions === "number" &&2726 deletions: acc.deletions + file.deletions,
2750 typeof file.deletions === "number"2727 };
2751 ? { additions: file.additions, deletions: file.deletions }2728 },
2752 : file.diff2729 { additions: 0, deletions: 0 },
2753 ? countDiffStat(file.diff)2730 );
2754 : null;2731 const changedFileCountLabel = String(turnSummary.files.length);
2755 if (!next) {
2756 return acc;
2757 }
2758 return {
2759 additions: acc.additions + next.additions,
2760 deletions: acc.deletions + next.deletions,
2761 };
2762 },
2763 { additions: 0, deletions: 0 },
2764 );
2765 const changedFileCountLabel = isCheckpointDiffLoading
2766 ? "..."
2767 : String(turnSummary.files.length);
2768 return (2732 return (
2769 <div className="mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5">2733 <div className="mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5">
2770 <div className="mb-1.5 flex items-center justify-between gap-2">2734 <div className="mb-1.5 flex items-center justify-between gap-2">
2771 <p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65">2735 <p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65">
2772 <span>Changed files ({changedFileCountLabel})</span>2736 <span>Changed files ({changedFileCountLabel})</span>
2773 {!isCheckpointDiffLoading &&2737 {(summaryStat.additions > 0 || summaryStat.deletions > 0) && (
2774 (summaryStat.additions > 0 || summaryStat.deletions > 0) && (2738 <>
2775 <>2739 <span className="mx-1">•</span>
2776 <span className="mx-1">•</span>2740 <span className="text-success">+{summaryStat.additions}</span>
2777 <span className="text-success">2741 <span className="mx-0.5 text-muted-foreground/70">/</span>
2778 +{summaryStat.additions}2742 <span className="text-destructive">-{summaryStat.deletions}</span>
2779 </span>2743 </>
2780 <span className="mx-0.5 text-muted-foreground/70">/</span>2744 )}
2781 <span className="text-destructive">
2782 -{summaryStat.deletions}
2783 </span>
2784 </>
2785 )}
2786 </p>2745 </p>
2787 <Button2746 <Button
2788 type="button"2747 type="button"
...@@ -2795,11 +2754,6 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -2795,11 +2754,6 @@ const MessagesTimeline = memo(function MessagesTimeline({
2795 View diff2754 View diff
2796 </Button>2755 </Button>
2797 </div>2756 </div>
2798 {isCheckpointDiffLoading && (
2799 <p className="mb-1.5 text-[11px] text-muted-foreground/70">
2800 Loading checkpoint diff...
2801 </p>
2802 )}
2803 {turnSummary.files.length > 0 ? (2757 {turnSummary.files.length > 0 ? (
2804 <div className="flex flex-wrap gap-1.5">2758 <div className="flex flex-wrap gap-1.5">
2805 {turnSummary.files.map((file) => (2759 {turnSummary.files.map((file) => (
...@@ -2817,9 +2771,7 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -2817,9 +2771,7 @@ const MessagesTimeline = memo(function MessagesTimeline({
2817 additions: file.additions,2771 additions: file.additions,
2818 deletions: file.deletions,2772 deletions: file.deletions,
2819 }2773 }
2820 : file.diff2774 : null;
2821 ? countDiffStat(file.diff)
2822 : null;
2823 if (!stat) {2775 if (!stat) {
2824 return file.path;2776 return file.path;
2825 }2777 }
...@@ -2841,11 +2793,11 @@ const MessagesTimeline = memo(function MessagesTimeline({...@@ -2841,11 +2793,11 @@ const MessagesTimeline = memo(function MessagesTimeline({
2841 </button>2793 </button>
2842 ))}2794 ))}
2843 </div>2795 </div>
2844 ) : !isCheckpointDiffLoading ? (2796 ) : (
2845 <p className="text-[11px] text-muted-foreground/70">2797 <p className="text-[11px] text-muted-foreground/70">
2846 No changed files.2798 No changed files.
2847 </p>2799 </p>
2848 ) : null}2800 )}
2849 </div>2801 </div>
2850 );2802 );
2851 })()}2803 })()}
apps/web/src/components/DiffPanel.tsx+3-96
...@@ -5,11 +5,11 @@ import {...@@ -5,11 +5,11 @@ import {
5 Virtualizer,5 Virtualizer,
6 WorkerPoolContextProvider,6 WorkerPoolContextProvider,
7} from "@pierre/diffs/react";7} from "@pierre/diffs/react";
8import { useQuery, useQueryClient } from "@tanstack/react-query";8import { useQuery } from "@tanstack/react-query";
9import { useParams } from "@tanstack/react-router";9import { useParams } from "@tanstack/react-router";
10import { Columns2Icon, Rows3Icon } from "lucide-react";10import { Columns2Icon, Rows3Icon } from "lucide-react";
11import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";11import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
12import { checkpointDiffQueryOptions, providerQueryKeys } from "~/lib/providerReactQuery";12import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
13import { cn } from "~/lib/utils";13import { cn } from "~/lib/utils";
14import { isElectron } from "../env";14import { isElectron } from "../env";
15import { useNativeApi } from "../hooks/useNativeApi";15import { useNativeApi } from "../hooks/useNativeApi";
...@@ -103,7 +103,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -103,7 +103,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
103 const api = useNativeApi();103 const api = useNativeApi();
104 const { resolvedTheme } = useTheme();104 const { resolvedTheme } = useTheme();
105 const { state, dispatch } = useStore();105 const { state, dispatch } = useStore();
106 const queryClient = useQueryClient();
107 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");106 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
108 const patchViewportRef = useRef<HTMLDivElement>(null);107 const patchViewportRef = useRef<HTMLDivElement>(null);
109 const params = useParams({ strict: false });108 const params = useParams({ strict: false });
...@@ -193,99 +192,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -193,99 +192,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
193 ? "Failed to load checkpoint diff."192 ? "Failed to load checkpoint diff."
194 : null;193 : null;
195194
196 useEffect(() => {195 const selectedPatch = selectedTurn ? selectedTurnCheckpointDiff : conversationCheckpointDiff;
197 if (!activeThread?.id || !selectedTurn || !selectedTurnCheckpointDiff) {
198 return;
199 }
200 dispatch({
201 type: "SET_THREAD_TURN_CHECKPOINT_DIFFS",
202 threadId: activeThread.id,
203 checkpointDiffByTurnId: {
204 [selectedTurn.turnId]: selectedTurnCheckpointDiff,
205 },
206 });
207 }, [activeThread?.id, dispatch, selectedTurn, selectedTurnCheckpointDiff]);
208
209 const selectedPatch = useMemo(() => {
210 const patchForSummary = (summary: (typeof turnDiffSummaries)[number]): string | undefined => {
211 const checkpointTurnCount =
212 summary.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[summary.turnId];
213 if (activeSessionId && typeof checkpointTurnCount === "number") {
214 const checkpointPatch = queryClient.getQueryData<{ diff: string }>(
215 providerQueryKeys.checkpointDiff({
216 sessionId: activeSessionId,
217 threadRuntimeId: activeThreadRuntimeId,
218 fromTurnCount: Math.max(0, checkpointTurnCount - 1),
219 toTurnCount: checkpointTurnCount,
220 cacheScope: `turn:${summary.turnId}`,
221 }),
222 )?.diff;
223 if (checkpointPatch) {
224 return checkpointPatch;
225 }
226 }
227 if (summary.unifiedDiff) {
228 return summary.unifiedDiff;
229 }
230 const filePatches = summary.files
231 .map((file) => file.diff?.trim())
232 .filter((patch): patch is string => Boolean(patch));
233 if (filePatches.length === 0) {
234 return undefined;
235 }
236 return filePatches.join("\n\n");
237 };
238
239 if (selectedTurn) {
240 return selectedTurnCheckpointDiff ?? patchForSummary(selectedTurn);
241 }
242
243 if (conversationCheckpointDiff) {
244 return conversationCheckpointDiff;
245 }
246
247 // Fallback when a conversation checkpoint diff isn't available yet:
248 // keep one patch per file path (latest change wins) so files aren't duplicated.
249 const latestPatchByPath = new Map<string, string>();
250 for (const summary of turnDiffSummaries) {
251 for (const file of summary.files) {
252 if (latestPatchByPath.has(file.path)) {
253 continue;
254 }
255 const patch = file.diff?.trim();
256 if (!patch) {
257 continue;
258 }
259 latestPatchByPath.set(file.path, patch);
260 }
261 }
262 if (latestPatchByPath.size > 0) {
263 return Array.from(latestPatchByPath.entries())
264 .toSorted(([leftPath], [rightPath]) =>
265 leftPath.localeCompare(rightPath, undefined, { numeric: true, sensitivity: "base" }),
266 )
267 .map(([, patch]) => patch)
268 .join("\n\n");
269 }
270
271 const patches = turnDiffSummaries
272 .toReversed()
273 .map((summary) => patchForSummary(summary)?.trim())
274 .filter((patch): patch is string => Boolean(patch));
275 if (patches.length === 0) {
276 return undefined;
277 }
278 return patches.join("\n\n");
279 }, [
280 activeSessionId,
281 activeThreadRuntimeId,
282 conversationCheckpointDiff,
283 inferredCheckpointTurnCountByTurnId,
284 queryClient,
285 selectedTurn,
286 selectedTurnCheckpointDiff,
287 turnDiffSummaries,
288 ]);
289 const renderablePatch = useMemo(() => getRenderablePatch(selectedPatch), [selectedPatch]);196 const renderablePatch = useMemo(() => getRenderablePatch(selectedPatch), [selectedPatch]);
290 const renderableFiles = useMemo(() => {197 const renderableFiles = useMemo(() => {
291 if (!renderablePatch || renderablePatch.kind !== "files") {198 if (!renderablePatch || renderablePatch.kind !== "files") {
apps/web/src/lib/providerReactQuery.test.ts+50
...@@ -52,4 +52,54 @@ describe("checkpointDiffQueryOptions", () => {...@@ -52,4 +52,54 @@ describe("checkpointDiffQueryOptions", () => {
52 toTurnCount: 4,52 toTurnCount: 4,
53 });53 });
54 });54 });
55
56 it("retries checkpoint-not-ready errors longer than generic failures", () => {
57 const options = checkpointDiffQueryOptions(undefined, {
58 sessionId: "session-id",
59 threadRuntimeId: "thread-id",
60 fromTurnCount: 1,
61 toTurnCount: 2,
62 cacheScope: "turn:abc",
63 });
64 const retry = options.retry;
65
66 expect(retry?.(1, new Error("Checkpoint turn count 2 exceeds current turn count 1."))).toBe(
67 true,
68 );
69 expect(
70 retry?.(
71 11,
72 new Error("Filesystem checkpoint is unavailable for turn 2 in thread thread-1."),
73 ),
74 ).toBe(true);
75 expect(
76 retry?.(
77 12,
78 new Error("Filesystem checkpoint is unavailable for turn 2 in thread thread-1."),
79 ),
80 ).toBe(false);
81 expect(retry?.(2, new Error("Something else failed."))).toBe(true);
82 expect(retry?.(3, new Error("Something else failed."))).toBe(false);
83 });
84
85 it("backs off longer for checkpoint-not-ready errors", () => {
86 const options = checkpointDiffQueryOptions(undefined, {
87 sessionId: "session-id",
88 threadRuntimeId: "thread-id",
89 fromTurnCount: 1,
90 toTurnCount: 2,
91 cacheScope: "turn:abc",
92 });
93 const retryDelay = options.retryDelay;
94
95 const checkpointDelay = retryDelay?.(
96 4,
97 new Error("Checkpoint turn count 2 exceeds current turn count 1."),
98 );
99 const genericDelay = retryDelay?.(4, new Error("Network failure"));
100
101 expect(typeof checkpointDelay).toBe("number");
102 expect(typeof genericDelay).toBe("number");
103 expect((checkpointDelay ?? 0) > (genericDelay ?? 0)).toBe(true);
104 });
55});105});
apps/web/src/lib/providerReactQuery.ts+29-2
...@@ -23,6 +23,25 @@ export const providerQueryKeys = {...@@ -23,6 +23,25 @@ export const providerQueryKeys = {
23 ] as const,23 ] as const,
24};24};
2525
26function asCheckpointErrorMessage(error: unknown): string {
27 if (error instanceof Error) {
28 return error.message;
29 }
30 if (typeof error === "string") {
31 return error;
32 }
33 return "";
34}
35
36function isCheckpointTemporarilyUnavailable(error: unknown): boolean {
37 const message = asCheckpointErrorMessage(error).toLowerCase();
38 return (
39 message.includes("exceeds current turn count") ||
40 message.includes("checkpoint is unavailable for turn") ||
41 message.includes("filesystem checkpoint is unavailable")
42 );
43}
44
26export function checkpointDiffQueryOptions(45export function checkpointDiffQueryOptions(
27 api: NativeApi | undefined,46 api: NativeApi | undefined,
28 input: CheckpointDiffQueryInput,47 input: CheckpointDiffQueryInput,
...@@ -54,7 +73,15 @@ export function checkpointDiffQueryOptions(...@@ -54,7 +73,15 @@ export function checkpointDiffQueryOptions(
54 },73 },
55 enabled: !!api && !!input.sessionId && hasValidRange,74 enabled: !!api && !!input.sessionId && hasValidRange,
56 staleTime: Infinity,75 staleTime: Infinity,
57 retry: 8,76 retry: (failureCount, error) => {
58 retryDelay: (attempt) => Math.min(2_000, 150 * 2 ** (attempt - 1)),77 if (isCheckpointTemporarilyUnavailable(error)) {
78 return failureCount < 12;
79 }
80 return failureCount < 3;
81 },
82 retryDelay: (attempt, error) =>
83 isCheckpointTemporarilyUnavailable(error)
84 ? Math.min(5_000, 250 * 2 ** (attempt - 1))
85 : Math.min(1_000, 100 * 2 ** (attempt - 1)),
59 });86 });
60}87}
apps/web/src/persistenceSchema.test.ts+1-4
...@@ -263,7 +263,7 @@ describe("hydratePersistedState", () => {...@@ -263,7 +263,7 @@ describe("hydratePersistedState", () => {
263 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);263 expect(hydrated?.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`);
264 });264 });
265265
266 it("hydrates persisted turn diff summaries and clears persisted loaded flags", () => {266 it("hydrates persisted turn diff summaries as metadata-only entries", () => {
267 const payload = JSON.stringify({267 const payload = JSON.stringify({
268 version: 7,268 version: 7,
269 runtimeMode: "full-access",269 runtimeMode: "full-access",
...@@ -290,7 +290,6 @@ describe("hydratePersistedState", () => {...@@ -290,7 +290,6 @@ describe("hydratePersistedState", () => {
290 turnId: "turn-1",290 turnId: "turn-1",
291 completedAt: "2026-02-08T10:05:00.000Z",291 completedAt: "2026-02-08T10:05:00.000Z",
292 checkpointTurnCount: 1,292 checkpointTurnCount: 1,
293 checkpointDiffLoaded: true,
294 files: [293 files: [
295 {294 {
296 path: "src/app.ts",295 path: "src/app.ts",
...@@ -421,10 +420,8 @@ describe("toPersistedState", () => {...@@ -421,10 +420,8 @@ describe("toPersistedState", () => {
421 kind: "modified",420 kind: "modified",
422 additions: 3,421 additions: 3,
423 deletions: 1,422 deletions: 1,
424 diff: "diff --git a/src/app.ts b/src/app.ts",
425 },423 },
426 ],424 ],
427 unifiedDiff: "diff --git a/src/app.ts b/src/app.ts",
428 },425 },
429 ],426 ],
430 };427 };
apps/web/src/persistenceSchema.ts-1
...@@ -61,7 +61,6 @@ const persistedTurnDiffSummarySchema = z.object({...@@ -61,7 +61,6 @@ const persistedTurnDiffSummarySchema = z.object({
61 files: z.array(persistedTurnDiffFileChangeSchema),61 files: z.array(persistedTurnDiffFileChangeSchema),
62 assistantMessageId: z.string().min(1).optional(),62 assistantMessageId: z.string().min(1).optional(),
63 checkpointTurnCount: z.number().int().min(0).optional(),63 checkpointTurnCount: z.number().int().min(0).optional(),
64 checkpointDiffLoaded: z.boolean().optional(),
65});64});
6665
67const persistedThreadSchema = z.object({66const persistedThreadSchema = z.object({
apps/web/src/session-logic.test.ts+6-6
...@@ -493,11 +493,11 @@ describe("deriveTurnDiffSummaries", () => {...@@ -493,11 +493,11 @@ describe("deriveTurnDiffSummaries", () => {
493 expect(summaries).toHaveLength(1);493 expect(summaries).toHaveLength(1);
494 expect(summaries[0]?.turnId).toBe("turn-1");494 expect(summaries[0]?.turnId).toBe("turn-1");
495 expect(summaries[0]?.assistantMessageId).toBe("msg-1");495 expect(summaries[0]?.assistantMessageId).toBe("msg-1");
496 expect(summaries[0]?.unifiedDiff).toContain("diff --git a/src/a.ts b/src/a.ts");
497 expect(summaries[0]?.files.map((file) => file.path)).toEqual(["src/a.ts", "src/b.ts"]);496 expect(summaries[0]?.files.map((file) => file.path)).toEqual(["src/a.ts", "src/b.ts"]);
498 expect(summaries[0]?.files[0]?.kind).toBe("modified");497 expect(summaries[0]?.files[0]?.kind).toBe("modified");
499 expect(summaries[0]?.files[0]?.additions).toBe(1);498 expect(summaries[0]?.files[0]?.additions).toBe(1);
500 expect(summaries[0]?.files[0]?.deletions).toBe(1);499 expect(summaries[0]?.files[0]?.deletions).toBe(1);
500 expect("unifiedDiff" in (summaries[0] as Record<string, unknown>)).toBe(false);
501 });501 });
502502
503 it("includes completed turns even when no fileChange tool events were emitted", () => {503 it("includes completed turns even when no fileChange tool events were emitted", () => {
...@@ -537,9 +537,10 @@ describe("deriveTurnDiffSummaries", () => {...@@ -537,9 +537,10 @@ describe("deriveTurnDiffSummaries", () => {
537 payload: {537 payload: {
538 diff: [538 diff: [
539 "diff --git a/src/example.ts b/src/example.ts",539 "diff --git a/src/example.ts b/src/example.ts",
540 "@@ -1 +1 @@",540 "@@ -1 +1,2 @@",
541 "-old",541 "-old",
542 "+older",542 "+older",
543 "+extra",
543 ].join("\n"),544 ].join("\n"),
544 },545 },
545 }),546 }),
...@@ -572,8 +573,9 @@ describe("deriveTurnDiffSummaries", () => {...@@ -572,8 +573,9 @@ describe("deriveTurnDiffSummaries", () => {
572 ]);573 ]);
573574
574 expect(summaries).toHaveLength(1);575 expect(summaries).toHaveLength(1);
575 expect(summaries[0]?.unifiedDiff).toContain("+newest");576 expect(summaries[0]?.files[0]?.path).toBe("src/example.ts");
576 expect(summaries[0]?.unifiedDiff).not.toContain("+older");577 expect(summaries[0]?.files[0]?.additions).toBe(1);
578 expect(summaries[0]?.files[0]?.deletions).toBe(1);
577 });579 });
578});580});
579581
...@@ -593,8 +595,6 @@ describe("deriveTurnDiffFilesFromUnifiedDiff", () => {...@@ -593,8 +595,6 @@ describe("deriveTurnDiffFilesFromUnifiedDiff", () => {
593 );595 );
594596
595 expect(files.map((file) => file.path)).toEqual(["src/a.ts", "src/b.ts"]);597 expect(files.map((file) => file.path)).toEqual(["src/a.ts", "src/b.ts"]);
596 expect(files[0]?.diff).toContain("diff --git a/src/a.ts b/src/a.ts");
597 expect(files[1]?.diff).toContain("diff --git a/src/b.ts b/src/b.ts");
598 expect(files[0]?.additions).toBe(1);598 expect(files[0]?.additions).toBe(1);
599 expect(files[0]?.deletions).toBe(1);599 expect(files[0]?.deletions).toBe(1);
600 });600 });
apps/web/src/session-logic.ts+4-7
...@@ -453,7 +453,7 @@ interface MutableTurnDiffSummary {...@@ -453,7 +453,7 @@ interface MutableTurnDiffSummary {
453 completedAt: string | undefined;453 completedAt: string | undefined;
454 status: string | undefined;454 status: string | undefined;
455 assistantMessageId: string | undefined;455 assistantMessageId: string | undefined;
456 unifiedDiff: string | undefined;456 hasTurnDiffUpdate: boolean;
457 filesByPath: Map<string, TurnDiffFileChange>;457 filesByPath: Map<string, TurnDiffFileChange>;
458}458}
459459
...@@ -485,7 +485,6 @@ function parseFileChangeEntriesFromEvent(event: ProviderEvent): TurnDiffFileChan...@@ -485,7 +485,6 @@ function parseFileChangeEntriesFromEvent(event: ProviderEvent): TurnDiffFileChan
485 parsed.push({485 parsed.push({
486 path,486 path,
487 ...(kind ? { kind } : {}),487 ...(kind ? { kind } : {}),
488 ...(diff ? { diff } : {}),
489 ...(additions !== undefined ? { additions } : stat ? { additions: stat.additions } : {}),488 ...(additions !== undefined ? { additions } : stat ? { additions: stat.additions } : {}),
490 ...(deletions !== undefined ? { deletions } : stat ? { deletions: stat.deletions } : {}),489 ...(deletions !== undefined ? { deletions } : stat ? { deletions: stat.deletions } : {}),
491 });490 });
...@@ -565,7 +564,6 @@ export function deriveTurnDiffFilesFromUnifiedDiff(diff: string): TurnDiffFileCh...@@ -565,7 +564,6 @@ export function deriveTurnDiffFilesFromUnifiedDiff(diff: string): TurnDiffFileCh
565 const stat = countDiffStat(fileDiff);564 const stat = countDiffStat(fileDiff);
566 return {565 return {
567 path,566 path,
568 diff: fileDiff,
569 additions: stat.additions,567 additions: stat.additions,
570 deletions: stat.deletions,568 deletions: stat.deletions,
571 };569 };
...@@ -645,7 +643,7 @@ export function deriveTurnDiffSummaries(events: ProviderEvent[]): TurnDiffSummar...@@ -645,7 +643,7 @@ export function deriveTurnDiffSummaries(events: ProviderEvent[]): TurnDiffSummar
645 completedAt: undefined,643 completedAt: undefined,
646 status: undefined,644 status: undefined,
647 assistantMessageId: undefined,645 assistantMessageId: undefined,
648 unifiedDiff: undefined,646 hasTurnDiffUpdate: false,
649 filesByPath: new Map<string, TurnDiffFileChange>(),647 filesByPath: new Map<string, TurnDiffFileChange>(),
650 };648 };
651 byTurnId.set(turnId, next);649 byTurnId.set(turnId, next);
...@@ -677,10 +675,10 @@ export function deriveTurnDiffSummaries(events: ProviderEvent[]): TurnDiffSummar...@@ -677,10 +675,10 @@ export function deriveTurnDiffSummaries(events: ProviderEvent[]): TurnDiffSummar
677 }675 }
678 }676 }
679677
680 if (event.method === "turn/diff/updated" && summary.unifiedDiff === undefined) {678 if (event.method === "turn/diff/updated" && !summary.hasTurnDiffUpdate) {
679 summary.hasTurnDiffUpdate = true;
681 const diff = normalizeDetail(asString(asObject(event.payload)?.diff));680 const diff = normalizeDetail(asString(asObject(event.payload)?.diff));
682 if (diff) {681 if (diff) {
683 summary.unifiedDiff = diff;
684 for (const file of deriveTurnDiffFilesFromUnifiedDiff(diff)) {682 for (const file of deriveTurnDiffFilesFromUnifiedDiff(diff)) {
685 mergeTurnDiffFileChange(summary.filesByPath, file);683 mergeTurnDiffFileChange(summary.filesByPath, file);
686 }684 }
...@@ -703,7 +701,6 @@ export function deriveTurnDiffSummaries(events: ProviderEvent[]): TurnDiffSummar...@@ -703,7 +701,6 @@ export function deriveTurnDiffSummaries(events: ProviderEvent[]): TurnDiffSummar
703 files: Array.from(summary.filesByPath.values()).toSorted((a, b) =>701 files: Array.from(summary.filesByPath.values()).toSorted((a, b) =>
704 a.path.localeCompare(b.path),702 a.path.localeCompare(b.path),
705 ),703 ),
706 ...(summary.unifiedDiff ? { unifiedDiff: summary.unifiedDiff } : {}),
707 ...(summary.assistantMessageId ? { assistantMessageId: summary.assistantMessageId } : {}),704 ...(summary.assistantMessageId ? { assistantMessageId: summary.assistantMessageId } : {}),
708 });705 });
709 }706 }
apps/web/src/store.test.ts+18-24
...@@ -911,7 +911,7 @@ describe("store reducer thread continuity", () => {...@@ -911,7 +911,7 @@ describe("store reducer thread continuity", () => {
911 expect(next.threads[0]?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn_1"]);911 expect(next.threads[0]?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn_1"]);
912 });912 });
913913
914 it("keeps checkpoint-derived turn files when later events for the same turn arrive", () => {914 it("keeps existing turn file metadata when later events for the same turn arrive", () => {
915 const state = makeState(915 const state = makeState(
916 makeThread({916 makeThread({
917 turnDiffSummaries: [917 turnDiffSummaries: [
...@@ -921,11 +921,10 @@ describe("store reducer thread continuity", () => {...@@ -921,11 +921,10 @@ describe("store reducer thread continuity", () => {
921 files: [921 files: [
922 {922 {
923 path: "src/from-checkpoint.ts",923 path: "src/from-checkpoint.ts",
924 diff: "diff --git a/src/from-checkpoint.ts b/src/from-checkpoint.ts",924 additions: 1,
925 deletions: 1,
925 },926 },
926 ],927 ],
927 unifiedDiff: "diff --git a/src/from-checkpoint.ts b/src/from-checkpoint.ts",
928 checkpointDiffLoaded: true,
929 },928 },
930 ],929 ],
931 }),930 }),
...@@ -945,11 +944,11 @@ describe("store reducer thread continuity", () => {...@@ -945,11 +944,11 @@ describe("store reducer thread continuity", () => {
945 expect(next.threads[0]?.turnDiffSummaries[0]?.files.map((file) => file.path)).toEqual([944 expect(next.threads[0]?.turnDiffSummaries[0]?.files.map((file) => file.path)).toEqual([
946 "src/from-checkpoint.ts",945 "src/from-checkpoint.ts",
947 ]);946 ]);
948 expect(next.threads[0]?.turnDiffSummaries[0]?.checkpointDiffLoaded).toBe(true);947 expect(next.threads[0]?.turnDiffSummaries[0]?.files[0]?.additions).toBe(1);
949 expect(next.threads[0]?.turnDiffSummaries[0]?.unifiedDiff).toContain("from-checkpoint.ts");948 expect(next.threads[0]?.turnDiffSummaries[0]?.files[0]?.deletions).toBe(1);
950 });949 });
951950
952 it("updates turn summaries from checkpoint diffs and marks them as loaded", () => {951 it("updates checkpoint turn counts from authoritative checkpoint mappings", () => {
953 const state = makeState(952 const state = makeState(
954 makeThread({953 makeThread({
955 turnDiffSummaries: [954 turnDiffSummaries: [
...@@ -959,31 +958,26 @@ describe("store reducer thread continuity", () => {...@@ -959,31 +958,26 @@ describe("store reducer thread continuity", () => {
959 files: [],958 files: [],
960 checkpointTurnCount: 1,959 checkpointTurnCount: 1,
961 },960 },
961 {
962 turnId: "turn-2",
963 completedAt: "2026-02-09T00:00:04.000Z",
964 files: [],
965 checkpointTurnCount: 2,
966 },
962 ],967 ],
963 }),968 }),
964 );969 );
965970
966 const next = reducer(state, {971 const next = reducer(state, {
967 type: "SET_THREAD_TURN_CHECKPOINT_DIFFS",972 type: "SET_THREAD_TURN_CHECKPOINT_COUNTS",
968 threadId: "thread-local-1",973 threadId: "thread-local-1",
969 checkpointDiffByTurnId: {974 checkpointTurnCountByTurnId: {
970 "turn-1": [975 "turn-1": 2,
971 "diff --git a/src/a.ts b/src/a.ts",976 "turn-2": 3,
972 "@@ -1 +1 @@",
973 "-old-a",
974 "+new-a",
975 "diff --git a/src/b.ts b/src/b.ts",
976 "@@ -1 +1 @@",
977 "-old-b",
978 "+new-b",
979 ].join("\n"),
980 },977 },
981 });978 });
982979
983 expect(next.threads[0]?.turnDiffSummaries[0]?.files.map((file) => file.path)).toEqual([980 expect(next.threads[0]?.turnDiffSummaries[0]?.checkpointTurnCount).toBe(2);
984 "src/a.ts",981 expect(next.threads[0]?.turnDiffSummaries[1]?.checkpointTurnCount).toBe(3);
985 "src/b.ts",
986 ]);
987 expect(next.threads[0]?.turnDiffSummaries[0]?.checkpointDiffLoaded).toBe(true);
988 });982 });
989});983});
apps/web/src/store.ts+1-72
...@@ -15,7 +15,6 @@ import {...@@ -15,7 +15,6 @@ import {
15 applyEventToMessages,15 applyEventToMessages,
16 asObject,16 asObject,
17 asString,17 asString,
18 deriveTurnDiffFilesFromUnifiedDiff,
19 deriveTurnDiffSummaries,18 deriveTurnDiffSummaries,
20 inferCheckpointTurnCountByTurnId,19 inferCheckpointTurnCountByTurnId,
21 evolveSession,20 evolveSession,
...@@ -94,11 +93,6 @@ type Action =...@@ -94,11 +93,6 @@ type Action =
94 threadId: string;93 threadId: string;
95 checkpointTurnCountByTurnId: Record<string, number>;94 checkpointTurnCountByTurnId: Record<string, number>;
96 }95 }
97 | {
98 type: "SET_THREAD_TURN_CHECKPOINT_DIFFS";
99 threadId: string;
100 checkpointDiffByTurnId: Record<string, string>;
101 }
102 | {96 | {
103 type: "SET_THREAD_BRANCH";97 type: "SET_THREAD_BRANCH";
104 threadId: string;98 threadId: string;
...@@ -236,21 +230,12 @@ function mergeTurnDiffSummaries(...@@ -236,21 +230,12 @@ function mergeTurnDiffSummaries(
236 return summary;230 return summary;
237 }231 }
238232
239 const prefersCheckpointDiff = previous.checkpointDiffLoaded || summary.checkpointDiffLoaded;
240 const files =233 const files =
241 prefersCheckpointDiff ||234 summary.files.length === 0 && previous.files.length > 0 ? previous.files : summary.files;
242 (summary.files.length === 0 && previous.files.length > 0 && summary.unifiedDiff === undefined)
243 ? previous.files
244 : summary.files;
245 const unifiedDiff =
246 prefersCheckpointDiff || summary.unifiedDiff === undefined
247 ? (previous.unifiedDiff ?? summary.unifiedDiff)
248 : summary.unifiedDiff;
249235
250 return {236 return {
251 ...summary,237 ...summary,
252 files,238 files,
253 ...(unifiedDiff !== undefined ? { unifiedDiff } : {}),
254 ...(summary.assistantMessageId239 ...(summary.assistantMessageId
255 ? {}240 ? {}
256 : previous.assistantMessageId241 : previous.assistantMessageId
...@@ -261,9 +246,6 @@ function mergeTurnDiffSummaries(...@@ -261,9 +246,6 @@ function mergeTurnDiffSummaries(
261 : typeof previous.checkpointTurnCount === "number"246 : typeof previous.checkpointTurnCount === "number"
262 ? { checkpointTurnCount: previous.checkpointTurnCount }247 ? { checkpointTurnCount: previous.checkpointTurnCount }
263 : {}),248 : {}),
264 ...(summary.checkpointDiffLoaded || previous.checkpointDiffLoaded
265 ? { checkpointDiffLoaded: true }
266 : {}),
267 };249 };
268 });250 });
269251
...@@ -1071,59 +1053,6 @@ export function reducer(state: AppState, action: Action): AppState {...@@ -1071,59 +1053,6 @@ export function reducer(state: AppState, action: Action): AppState {
1071 }),1053 }),
1072 };1054 };
10731055
1074 case "SET_THREAD_TURN_CHECKPOINT_DIFFS":
1075 return {
1076 ...state,
1077 threads: updateThread(state.threads, action.threadId, (t) => {
1078 let hasUpdates = false;
1079 const updatedSummaries = t.turnDiffSummaries.map((summary) => {
1080 const checkpointDiff = action.checkpointDiffByTurnId[summary.turnId];
1081 if (checkpointDiff === undefined) {
1082 return summary;
1083 }
1084
1085 const files = deriveTurnDiffFilesFromUnifiedDiff(checkpointDiff);
1086 const filesUnchanged =
1087 files.length === summary.files.length &&
1088 files.every((file, index) => {
1089 const existing = summary.files[index];
1090 return (
1091 existing !== undefined &&
1092 existing.path === file.path &&
1093 existing.diff === file.diff &&
1094 existing.kind === file.kind &&
1095 existing.additions === file.additions &&
1096 existing.deletions === file.deletions
1097 );
1098 });
1099 if (
1100 summary.unifiedDiff === checkpointDiff &&
1101 filesUnchanged &&
1102 summary.checkpointDiffLoaded
1103 ) {
1104 return summary;
1105 }
1106
1107 hasUpdates = true;
1108 return {
1109 ...summary,
1110 unifiedDiff: checkpointDiff,
1111 files,
1112 checkpointDiffLoaded: true,
1113 };
1114 });
1115
1116 if (!hasUpdates) {
1117 return t;
1118 }
1119
1120 return {
1121 ...t,
1122 turnDiffSummaries: updatedSummaries,
1123 };
1124 }),
1125 };
1126
1127 case "SET_THREAD_BRANCH": {1056 case "SET_THREAD_BRANCH": {
1128 return {1057 return {
1129 ...state,1058 ...state,
apps/web/src/types.ts-3
...@@ -40,7 +40,6 @@ export interface ChatMessage {...@@ -40,7 +40,6 @@ export interface ChatMessage {
40export interface TurnDiffFileChange {40export interface TurnDiffFileChange {
41 path: string;41 path: string;
42 kind?: string | undefined;42 kind?: string | undefined;
43 diff?: string | undefined;
44 additions?: number | undefined;43 additions?: number | undefined;
45 deletions?: number | undefined;44 deletions?: number | undefined;
46}45}
...@@ -50,10 +49,8 @@ export interface TurnDiffSummary {...@@ -50,10 +49,8 @@ export interface TurnDiffSummary {
50 completedAt: string;49 completedAt: string;
51 status?: string | undefined;50 status?: string | undefined;
52 files: TurnDiffFileChange[];51 files: TurnDiffFileChange[];
53 unifiedDiff?: string | undefined;
54 assistantMessageId?: string | undefined;52 assistantMessageId?: string | undefined;
55 checkpointTurnCount?: number | undefined;53 checkpointTurnCount?: number | undefined;
56 checkpointDiffLoaded?: boolean | undefined;
57}54}
5855
59export interface Project {56export interface Project {