| author | |
| committer | |
| log | 3afbf93d5e54fec6674c62a5521f78c0ec2cf846 |
| tree | 83fcd8cb80bd6ce1ceb0458d4f8f4ee9dc011f92 |
| parent | ec5c3c32cd2acaec16f0b6b26d2f0e6bb3030e3c |
- add shared provider checkpoint diff query options and keys
- refactor `ChatView` and `DiffPanel` to fetch diffs via React Query
- persist selected-turn checkpoint diffs back into thread state for reuse9 files changed, 258 insertions(+), 194 deletions(-)
apps/server/src/filesystemCheckpointStore.ts+30-8| ... | @@ -38,7 +38,11 @@ export class FilesystemCheckpointStore { | ... | @@ -38,7 +38,11 @@ export class FilesystemCheckpointStore { |
| 38 | return result.code === 0 && result.stdout.trim() === "true"; | 38 | return result.code === 0 && result.stdout.trim() === "true"; |
| 39 | } | 39 | } |
| 40 | 40 | ||
| 41 | async captureCheckpoint(input: { cwd: string; threadId: string; turnCount: number }): Promise<void> { | 41 | async captureCheckpoint(input: { |
| 42 | cwd: string; | ||
| 43 | threadId: string; | ||
| 44 | turnCount: number; | ||
| 45 | }): Promise<void> { | ||
| 42 | const { cwd, threadId, turnCount } = input; | 46 | const { cwd, threadId, turnCount } = input; |
| 43 | const ref = checkpointRefForThreadTurn(threadId, turnCount); | 47 | const ref = checkpointRefForThreadTurn(threadId, turnCount); |
| 44 | 48 | ||
| ... | @@ -84,7 +88,11 @@ export class FilesystemCheckpointStore { | ... | @@ -84,7 +88,11 @@ export class FilesystemCheckpointStore { |
| 84 | } | 88 | } |
| 85 | } | 89 | } |
| 86 | 90 | ||
| 87 | async hasCheckpoint(input: { cwd: string; threadId: string; turnCount: number }): Promise<boolean> { | 91 | async hasCheckpoint(input: { |
| 92 | cwd: string; | ||
| 93 | threadId: string; | ||
| 94 | turnCount: number; | ||
| 95 | }): Promise<boolean> { | ||
| 88 | const { cwd, threadId, turnCount } = input; | 96 | const { cwd, threadId, turnCount } = input; |
| 89 | const ref = checkpointRefForThreadTurn(threadId, turnCount); | 97 | const ref = checkpointRefForThreadTurn(threadId, turnCount); |
| 90 | const commit = await this.resolveCheckpointCommit(cwd, ref); | 98 | const commit = await this.resolveCheckpointCommit(cwd, ref); |
| ... | @@ -108,7 +116,11 @@ export class FilesystemCheckpointStore { | ... | @@ -108,7 +116,11 @@ export class FilesystemCheckpointStore { |
| 108 | return true; | 116 | return true; |
| 109 | } | 117 | } |
| 110 | 118 | ||
| 111 | async restoreCheckpoint(input: { cwd: string; threadId: string; turnCount: number }): Promise<boolean> { | 119 | async restoreCheckpoint(input: { |
| 120 | cwd: string; | ||
| 121 | threadId: string; | ||
| 122 | turnCount: number; | ||
| 123 | }): Promise<boolean> { | ||
| 112 | const { cwd, threadId, turnCount } = input; | 124 | const { cwd, threadId, turnCount } = input; |
| 113 | const ref = checkpointRefForThreadTurn(threadId, turnCount); | 125 | const ref = checkpointRefForThreadTurn(threadId, turnCount); |
| 114 | let commitOid = await this.resolveCheckpointCommit(cwd, ref); | 126 | let commitOid = await this.resolveCheckpointCommit(cwd, ref); |
| ... | @@ -158,7 +170,9 @@ export class FilesystemCheckpointStore { | ... | @@ -158,7 +170,9 @@ export class FilesystemCheckpointStore { |
| 158 | ); | 170 | ); |
| 159 | } | 171 | } |
| 160 | if (!toCommitOid) { | 172 | if (!toCommitOid) { |
| 161 | throw new Error(`Filesystem checkpoint is unavailable for turn ${toTurnCount} in thread ${threadId}.`); | 173 | throw new Error( |
| 174 | `Filesystem checkpoint is unavailable for turn ${toTurnCount} in thread ${threadId}.`, | ||
| 175 | ); | ||
| 162 | } | 176 | } |
| 163 | 177 | ||
| 164 | const result = await this.runGit(cwd, [ | 178 | const result = await this.runGit(cwd, [ |
| ... | @@ -183,16 +197,24 @@ export class FilesystemCheckpointStore { | ... | @@ -183,16 +197,24 @@ export class FilesystemCheckpointStore { |
| 183 | return commit.length > 0 ? commit : null; | 197 | return commit.length > 0 ? commit : null; |
| 184 | } | 198 | } |
| 185 | 199 | ||
| 186 | async pruneAfterTurn(input: { cwd: string; threadId: string; maxTurnCount: number }): Promise<void> { | 200 | async pruneAfterTurn(input: { |
| 201 | cwd: string; | ||
| 202 | threadId: string; | ||
| 203 | maxTurnCount: number; | ||
| 204 | }): Promise<void> { | ||
| 187 | const { cwd, threadId, maxTurnCount } = input; | 205 | const { cwd, threadId, maxTurnCount } = input; |
| 188 | if (!Number.isInteger(maxTurnCount) || maxTurnCount < 0) { | 206 | if (!Number.isInteger(maxTurnCount) || maxTurnCount < 0) { |
| 189 | throw new Error(`Invalid max turn count: ${maxTurnCount}`); | 207 | throw new Error(`Invalid max turn count: ${maxTurnCount}`); |
| 190 | } | 208 | } |
| 191 | 209 | ||
| 192 | const threadRefPrefix = `${CHECKPOINT_REFS_PREFIX}/${checkpointRefThreadSegment(threadId)}/turn/`; | 210 | const threadRefPrefix = `${CHECKPOINT_REFS_PREFIX}/${checkpointRefThreadSegment(threadId)}/turn/`; |
| 193 | const result = await this.runGit(cwd, ["for-each-ref", "--format=%(refname)", threadRefPrefix], { | 211 | const result = await this.runGit( |
| 194 | allowNonZeroExit: true, | 212 | cwd, |
| 195 | }); | 213 | ["for-each-ref", "--format=%(refname)", threadRefPrefix], |
| 214 | { | ||
| 215 | allowNonZeroExit: true, | ||
| 216 | }, | ||
| 217 | ); | ||
| 196 | if (result.code !== 0) { | 218 | if (result.code !== 0) { |
| 197 | return; | 219 | return; |
| 198 | } | 220 | } |
apps/server/src/providerManager.test.ts+49-11| ... | @@ -159,7 +159,10 @@ describe("ProviderManager", () => { | ... | @@ -159,7 +159,10 @@ describe("ProviderManager", () => { |
| 159 | threadId: string; | 159 | threadId: string; |
| 160 | turns: Array<{ id: string; items: unknown[] }>; | 160 | turns: Array<{ id: string; items: unknown[] }>; |
| 161 | }>; | 161 | }>; |
| 162 | rollbackThread: (sessionId: string, numTurns: number) => Promise<{ | 162 | rollbackThread: ( |
| 163 | sessionId: string, | ||
| 164 | numTurns: number, | ||
| 165 | ) => Promise<{ | ||
| 163 | threadId: string; | 166 | threadId: string; |
| 164 | turns: Array<{ id: string; items: unknown[] }>; | 167 | turns: Array<{ id: string; items: unknown[] }>; |
| 165 | }>; | 168 | }>; |
| ... | @@ -253,7 +256,10 @@ describe("ProviderManager", () => { | ... | @@ -253,7 +256,10 @@ describe("ProviderManager", () => { |
| 253 | internals.codex.hasSession = () => true; | 256 | internals.codex.hasSession = () => true; |
| 254 | internals.codex.readThread = async () => ({ | 257 | internals.codex.readThread = async () => ({ |
| 255 | threadId: "thr_1", | 258 | threadId: "thr_1", |
| 256 | turns: [{ id: "turn_1", items: [] }, { id: "turn_2", items: [] }], | 259 | turns: [ |
| 260 | { id: "turn_1", items: [] }, | ||
| 261 | { id: "turn_2", items: [] }, | ||
| 262 | ], | ||
| 257 | }); | 263 | }); |
| 258 | const diffCheckpoints = vi.fn(async () => "diff --git a/a.ts b/a.ts"); | 264 | const diffCheckpoints = vi.fn(async () => "diff --git a/a.ts b/a.ts"); |
| 259 | internals.filesystemCheckpointStore.diffCheckpoints = diffCheckpoints; | 265 | internals.filesystemCheckpointStore.diffCheckpoints = diffCheckpoints; |
| ... | @@ -330,7 +336,10 @@ describe("ProviderManager", () => { | ... | @@ -330,7 +336,10 @@ describe("ProviderManager", () => { |
| 330 | ]; | 336 | ]; |
| 331 | internals.codex.readThread = async () => ({ | 337 | internals.codex.readThread = async () => ({ |
| 332 | threadId: "thr_1", | 338 | threadId: "thr_1", |
| 333 | turns: [{ id: "turn_1", items: [] }, { id: "turn_2", items: [] }], | 339 | turns: [ |
| 340 | { id: "turn_1", items: [] }, | ||
| 341 | { id: "turn_2", items: [] }, | ||
| 342 | ], | ||
| 334 | }); | 343 | }); |
| 335 | const isGitRepository = vi.fn(async () => true); | 344 | const isGitRepository = vi.fn(async () => true); |
| 336 | const ensureRootCheckpoint = vi.fn(async () => true); | 345 | const ensureRootCheckpoint = vi.fn(async () => true); |
| ... | @@ -471,19 +480,30 @@ describe("ProviderManager", () => { | ... | @@ -471,19 +480,30 @@ describe("ProviderManager", () => { |
| 471 | threadId: string; | 480 | threadId: string; |
| 472 | turns: Array<{ id: string; items: unknown[] }>; | 481 | turns: Array<{ id: string; items: unknown[] }>; |
| 473 | }>; | 482 | }>; |
| 474 | rollbackThread: (sessionId: string, numTurns: number) => Promise<{ | 483 | rollbackThread: ( |
| 484 | sessionId: string, | ||
| 485 | numTurns: number, | ||
| 486 | ) => Promise<{ | ||
| 475 | threadId: string; | 487 | threadId: string; |
| 476 | turns: Array<{ id: string; items: unknown[] }>; | 488 | turns: Array<{ id: string; items: unknown[] }>; |
| 477 | }>; | 489 | }>; |
| 478 | }; | 490 | }; |
| 479 | filesystemCheckpointStore: { | 491 | filesystemCheckpointStore: { |
| 480 | hasCheckpoint: (input: { cwd: string; threadId: string; turnCount: number }) => Promise<boolean>; | 492 | hasCheckpoint: (input: { |
| 493 | cwd: string; | ||
| 494 | threadId: string; | ||
| 495 | turnCount: number; | ||
| 496 | }) => Promise<boolean>; | ||
| 481 | restoreCheckpoint: (input: { | 497 | restoreCheckpoint: (input: { |
| 482 | cwd: string; | 498 | cwd: string; |
| 483 | threadId: string; | 499 | threadId: string; |
| 484 | turnCount: number; | 500 | turnCount: number; |
| 485 | }) => Promise<boolean>; | 501 | }) => Promise<boolean>; |
| 486 | pruneAfterTurn: (input: { cwd: string; threadId: string; maxTurnCount: number }) => Promise<void>; | 502 | pruneAfterTurn: (input: { |
| 503 | cwd: string; | ||
| 504 | threadId: string; | ||
| 505 | maxTurnCount: number; | ||
| 506 | }) => Promise<void>; | ||
| 487 | }; | 507 | }; |
| 488 | sessionCheckpointCwds: Map<string, string>; | 508 | sessionCheckpointCwds: Map<string, string>; |
| 489 | }; | 509 | }; |
| ... | @@ -542,19 +562,30 @@ describe("ProviderManager", () => { | ... | @@ -542,19 +562,30 @@ describe("ProviderManager", () => { |
| 542 | threadId: string; | 562 | threadId: string; |
| 543 | turns: Array<{ id: string; items: unknown[] }>; | 563 | turns: Array<{ id: string; items: unknown[] }>; |
| 544 | }>; | 564 | }>; |
| 545 | rollbackThread: (sessionId: string, numTurns: number) => Promise<{ | 565 | rollbackThread: ( |
| 566 | sessionId: string, | ||
| 567 | numTurns: number, | ||
| 568 | ) => Promise<{ | ||
| 546 | threadId: string; | 569 | threadId: string; |
| 547 | turns: Array<{ id: string; items: unknown[] }>; | 570 | turns: Array<{ id: string; items: unknown[] }>; |
| 548 | }>; | 571 | }>; |
| 549 | }; | 572 | }; |
| 550 | filesystemCheckpointStore: { | 573 | filesystemCheckpointStore: { |
| 551 | hasCheckpoint: (input: { cwd: string; threadId: string; turnCount: number }) => Promise<boolean>; | 574 | hasCheckpoint: (input: { |
| 575 | cwd: string; | ||
| 576 | threadId: string; | ||
| 577 | turnCount: number; | ||
| 578 | }) => Promise<boolean>; | ||
| 552 | restoreCheckpoint: (input: { | 579 | restoreCheckpoint: (input: { |
| 553 | cwd: string; | 580 | cwd: string; |
| 554 | threadId: string; | 581 | threadId: string; |
| 555 | turnCount: number; | 582 | turnCount: number; |
| 556 | }) => Promise<boolean>; | 583 | }) => Promise<boolean>; |
| 557 | pruneAfterTurn: (input: { cwd: string; threadId: string; maxTurnCount: number }) => Promise<void>; | 584 | pruneAfterTurn: (input: { |
| 585 | cwd: string; | ||
| 586 | threadId: string; | ||
| 587 | maxTurnCount: number; | ||
| 588 | }) => Promise<void>; | ||
| 558 | }; | 589 | }; |
| 559 | sessionCheckpointCwds: Map<string, string>; | 590 | sessionCheckpointCwds: Map<string, string>; |
| 560 | }; | 591 | }; |
| ... | @@ -609,13 +640,20 @@ describe("ProviderManager", () => { | ... | @@ -609,13 +640,20 @@ describe("ProviderManager", () => { |
| 609 | threadId: string; | 640 | threadId: string; |
| 610 | turns: Array<{ id: string; items: unknown[] }>; | 641 | turns: Array<{ id: string; items: unknown[] }>; |
| 611 | }>; | 642 | }>; |
| 612 | rollbackThread: (sessionId: string, numTurns: number) => Promise<{ | 643 | rollbackThread: ( |
| 644 | sessionId: string, | ||
| 645 | numTurns: number, | ||
| 646 | ) => Promise<{ | ||
| 613 | threadId: string; | 647 | threadId: string; |
| 614 | turns: Array<{ id: string; items: unknown[] }>; | 648 | turns: Array<{ id: string; items: unknown[] }>; |
| 615 | }>; | 649 | }>; |
| 616 | }; | 650 | }; |
| 617 | filesystemCheckpointStore: { | 651 | filesystemCheckpointStore: { |
| 618 | hasCheckpoint: (input: { cwd: string; threadId: string; turnCount: number }) => Promise<boolean>; | 652 | hasCheckpoint: (input: { |
| 653 | cwd: string; | ||
| 654 | threadId: string; | ||
| 655 | turnCount: number; | ||
| 656 | }) => Promise<boolean>; | ||
| 619 | }; | 657 | }; |
| 620 | sessionCheckpointCwds: Map<string, string>; | 658 | sessionCheckpointCwds: Map<string, string>; |
| 621 | }; | 659 | }; |
apps/server/src/providerManager.ts+3-1| ... | @@ -425,7 +425,9 @@ export class ProviderManager extends EventEmitter<ProviderManagerEvents> { | ... | @@ -425,7 +425,9 @@ export class ProviderManager extends EventEmitter<ProviderManagerEvents> { |
| 425 | return existingCwd; | 425 | return existingCwd; |
| 426 | } | 426 | } |
| 427 | 427 | ||
| 428 | const session = this.codex.listSessions().find((candidate) => candidate.sessionId === sessionId); | 428 | const session = this.codex |
| 429 | .listSessions() | ||
| 430 | .find((candidate) => candidate.sessionId === sessionId); | ||
| 429 | const candidateCwds = Array.from( | 431 | const candidateCwds = Array.from( |
| 430 | new Set([session?.cwd, process.cwd()].filter((value): value is string => Boolean(value))), | 432 | new Set([session?.cwd, process.cwd()].filter((value): value is string => Boolean(value))), |
| 431 | ); | 433 | ); |
apps/web/src/components/ChatView.tsx+52-52| ... | @@ -27,10 +27,11 @@ import { | ... | @@ -27,10 +27,11 @@ import { |
| 27 | useRef, | 27 | useRef, |
| 28 | useState, | 28 | useState, |
| 29 | } from "react"; | 29 | } from "react"; |
| 30 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | 30 | import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 31 | import { useDebouncedValue } from "@tanstack/react-pacer/debouncer"; | 31 | import { useDebouncedValue } from "@tanstack/react-pacer/debouncer"; |
| 32 | import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery"; | 32 | import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery"; |
| 33 | import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery"; | 33 | import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery"; |
| 34 | import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery"; | ||
| 34 | import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery"; | 35 | import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery"; |
| 35 | 36 | ||
| 36 | import { isElectron } from "../env"; | 37 | import { isElectron } from "../env"; |
| ... | @@ -373,12 +374,11 @@ export default function ChatView() { | ... | @@ -373,12 +374,11 @@ export default function ChatView() { |
| 373 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); | 374 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| 374 | const dragDepthRef = useRef(0); | 375 | const dragDepthRef = useRef(0); |
| 375 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); | 376 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); |
| 376 | const checkpointDiffRequestsRef = useRef(new Set<string>()); | ||
| 377 | const checkpointHydrationSessionRequestRef = useRef(new Set<string>()); | 377 | const checkpointHydrationSessionRequestRef = useRef(new Set<string>()); |
| 378 | 378 | ||
| 379 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); | 379 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); |
| 380 | const activeThreadId = activeThread?.id ?? null; | 380 | const activeThreadId = activeThread?.id ?? null; |
| 381 | const activeSessionId = activeThread?.session?.sessionId; | 381 | const activeSessionId = activeThread?.session?.sessionId ?? null; |
| 382 | const activeThreadRuntimeId = | 382 | const activeThreadRuntimeId = |
| 383 | activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; | 383 | activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; |
| 384 | const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); | 384 | const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); |
| ... | @@ -433,6 +433,7 @@ export default function ChatView() { | ... | @@ -433,6 +433,7 @@ export default function ChatView() { |
| 433 | () => inferCheckpointTurnCountByTurnId(turnDiffSummaries), | 433 | () => inferCheckpointTurnCountByTurnId(turnDiffSummaries), |
| 434 | [turnDiffSummaries], | 434 | [turnDiffSummaries], |
| 435 | ); | 435 | ); |
| 436 | |||
| 436 | const revertTurnCountByUserMessageId = useMemo(() => { | 437 | const revertTurnCountByUserMessageId = useMemo(() => { |
| 437 | const byUserMessageId = new Map<string, number>(); | 438 | const byUserMessageId = new Map<string, number>(); |
| 438 | for (let index = 0; index < timelineEntries.length; index += 1) { | 439 | for (let index = 0; index < timelineEntries.length; index += 1) { |
| ... | @@ -496,68 +497,67 @@ export default function ChatView() { | ... | @@ -496,68 +497,67 @@ export default function ChatView() { |
| 496 | }); | 497 | }); |
| 497 | }, [activeThreadId, dispatch, inferredCheckpointTurnCountByTurnId, turnDiffSummaries]); | 498 | }, [activeThreadId, dispatch, inferredCheckpointTurnCountByTurnId, turnDiffSummaries]); |
| 498 | 499 | ||
| 499 | useEffect(() => { | 500 | const checkpointDiffHydrationTargets = useMemo(() => { |
| 500 | if (!api || !activeThreadId || !activeSessionId || turnDiffSummaries.length === 0) { | 501 | if (!api || !activeThreadId || !activeSessionId || turnDiffSummaries.length === 0) { |
| 501 | return; | 502 | return []; |
| 502 | } | 503 | } |
| 503 | 504 | return turnDiffSummaries.flatMap((summary) => { | |
| 504 | const turnSummariesNeedingDiff = turnDiffSummaries.filter( | 505 | const turnCount = inferredCheckpointTurnCountByTurnId[summary.turnId]; |
| 505 | (summary) => | 506 | if (summary.checkpointDiffLoaded || typeof turnCount !== "number") { |
| 506 | !summary.checkpointDiffLoaded && inferredCheckpointTurnCountByTurnId[summary.turnId], | 507 | return []; |
| 507 | ); | ||
| 508 | const requestedSummaries = turnSummariesNeedingDiff.filter((summary) => { | ||
| 509 | const requestKey = `${activeThreadId}:${summary.turnId}`; | ||
| 510 | if (checkpointDiffRequestsRef.current.has(requestKey)) { | ||
| 511 | return false; | ||
| 512 | } | 508 | } |
| 513 | checkpointDiffRequestsRef.current.add(requestKey); | 509 | return [ |
| 514 | return true; | 510 | { |
| 515 | }); | 511 | turnId: summary.turnId, |
| 516 | if (requestedSummaries.length === 0) { | ||
| 517 | return; | ||
| 518 | } | ||
| 519 | |||
| 520 | let cancelled = false; | ||
| 521 | void Promise.all( | ||
| 522 | requestedSummaries.map(async (summary) => { | ||
| 523 | const turnCount = inferredCheckpointTurnCountByTurnId[summary.turnId] ?? 0; | ||
| 524 | const result = await api.providers.getCheckpointDiff({ | ||
| 525 | sessionId: activeSessionId, | ||
| 526 | fromTurnCount: Math.max(0, turnCount - 1), | 512 | fromTurnCount: Math.max(0, turnCount - 1), |
| 527 | toTurnCount: turnCount, | 513 | toTurnCount: turnCount, |
| 528 | }); | 514 | }, |
| 529 | return [summary.turnId, result.diff] as const; | 515 | ]; |
| 530 | }), | 516 | }); |
| 531 | ) | ||
| 532 | .then((entries) => { | ||
| 533 | if (cancelled) return; | ||
| 534 | dispatch({ | ||
| 535 | type: "SET_THREAD_TURN_CHECKPOINT_DIFFS", | ||
| 536 | threadId: activeThreadId, | ||
| 537 | checkpointDiffByTurnId: Object.fromEntries(entries), | ||
| 538 | }); | ||
| 539 | }) | ||
| 540 | .catch(() => { | ||
| 541 | if (cancelled) return; | ||
| 542 | }) | ||
| 543 | .finally(() => { | ||
| 544 | for (const summary of requestedSummaries) { | ||
| 545 | checkpointDiffRequestsRef.current.delete(`${activeThreadId}:${summary.turnId}`); | ||
| 546 | } | ||
| 547 | }); | ||
| 548 | |||
| 549 | return () => { | ||
| 550 | cancelled = true; | ||
| 551 | }; | ||
| 552 | }, [ | 517 | }, [ |
| 553 | activeSessionId, | 518 | activeSessionId, |
| 554 | activeThreadId, | 519 | activeThreadId, |
| 555 | api, | 520 | api, |
| 556 | dispatch, | ||
| 557 | inferredCheckpointTurnCountByTurnId, | 521 | inferredCheckpointTurnCountByTurnId, |
| 558 | turnDiffSummaries, | 522 | turnDiffSummaries, |
| 559 | ]); | 523 | ]); |
| 560 | 524 | ||
| 525 | const checkpointDiffHydrationQueries = useQueries({ | ||
| 526 | queries: checkpointDiffHydrationTargets.map((target) => | ||
| 527 | checkpointDiffQueryOptions(api, { | ||
| 528 | sessionId: activeSessionId, | ||
| 529 | threadRuntimeId: activeThreadRuntimeId, | ||
| 530 | fromTurnCount: target.fromTurnCount, | ||
| 531 | toTurnCount: target.toTurnCount, | ||
| 532 | }), | ||
| 533 | ), | ||
| 534 | }); | ||
| 535 | |||
| 536 | useEffect(() => { | ||
| 537 | if (!activeThreadId || checkpointDiffHydrationTargets.length === 0) { | ||
| 538 | return; | ||
| 539 | } | ||
| 540 | |||
| 541 | const checkpointDiffByTurnId: Record<string, string> = {}; | ||
| 542 | for (let index = 0; index < checkpointDiffHydrationTargets.length; index += 1) { | ||
| 543 | const target = checkpointDiffHydrationTargets[index]; | ||
| 544 | const query = checkpointDiffHydrationQueries[index]; | ||
| 545 | const diff = query?.data?.diff; | ||
| 546 | if (!target || !diff) { | ||
| 547 | continue; | ||
| 548 | } | ||
| 549 | checkpointDiffByTurnId[target.turnId] = diff; | ||
| 550 | } | ||
| 551 | if (Object.keys(checkpointDiffByTurnId).length === 0) { | ||
| 552 | return; | ||
| 553 | } | ||
| 554 | dispatch({ | ||
| 555 | type: "SET_THREAD_TURN_CHECKPOINT_DIFFS", | ||
| 556 | threadId: activeThreadId, | ||
| 557 | checkpointDiffByTurnId, | ||
| 558 | }); | ||
| 559 | }, [activeThreadId, checkpointDiffHydrationQueries, checkpointDiffHydrationTargets, dispatch]); | ||
| 560 | |||
| 561 | const completionSummary = useMemo(() => { | 561 | const completionSummary = useMemo(() => { |
| 562 | if (!activeThread?.latestTurnStartedAt) return null; | 562 | if (!activeThread?.latestTurnStartedAt) return null; |
| 563 | if (!activeThread.latestTurnCompletedAt) return null; | 563 | if (!activeThread.latestTurnCompletedAt) return null; |
apps/web/src/components/DiffPanel.tsx+58-119| ... | @@ -1,7 +1,10 @@ | ... | @@ -1,7 +1,10 @@ |
| 1 | import { parsePatchFiles } from "@pierre/diffs"; | 1 | import { parsePatchFiles } from "@pierre/diffs"; |
| 2 | import { FileDiff, type FileDiffMetadata } from "@pierre/diffs/react"; | 2 | import { FileDiff, type FileDiffMetadata } from "@pierre/diffs/react"; |
| 3 | import { Columns2Icon, Rows3Icon, XIcon } from "lucide-react"; | 3 | import { useQuery, useQueryClient } from "@tanstack/react-query"; |
| 4 | import { Columns2Icon, Rows3Icon } from "lucide-react"; | ||
| 4 | import { useEffect, useMemo, useRef, useState } from "react"; | 5 | import { useEffect, useMemo, useRef, useState } from "react"; |
| 6 | import { checkpointDiffQueryOptions, providerQueryKeys } from "~/lib/providerReactQuery"; | ||
| 7 | import { cn } from "~/lib/utils"; | ||
| 5 | import { isElectron } from "../env"; | 8 | import { isElectron } from "../env"; |
| 6 | import { useNativeApi } from "../hooks/useNativeApi"; | 9 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 7 | import { useTheme } from "../hooks/useTheme"; | 10 | import { useTheme } from "../hooks/useTheme"; |
| ... | @@ -11,9 +14,6 @@ import { | ... | @@ -11,9 +14,6 @@ import { |
| 11 | inferCheckpointTurnCountByTurnId, | 14 | inferCheckpointTurnCountByTurnId, |
| 12 | } from "../session-logic"; | 15 | } from "../session-logic"; |
| 13 | import { useStore } from "../store"; | 16 | import { useStore } from "../store"; |
| 14 | import { Button } from "./ui/button"; | ||
| 15 | import { Popover, PopoverPopup, PopoverTrigger } from "./ui/popover"; | ||
| 16 | import { cn } from "~/lib/utils"; | ||
| 17 | import { ToggleGroup, Toggle } from "./ui/toggle-group"; | 17 | import { ToggleGroup, Toggle } from "./ui/toggle-group"; |
| 18 | 18 | ||
| 19 | type DiffRenderMode = "stacked" | "split"; | 19 | type DiffRenderMode = "stacked" | "split"; |
| ... | @@ -63,14 +63,6 @@ function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { | ... | @@ -63,14 +63,6 @@ function resolveFileDiffPath(fileDiff: FileDiffMetadata): string { |
| 63 | return raw; | 63 | return raw; |
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | function buildCheckpointDiffKey( | ||
| 67 | sessionId: string, | ||
| 68 | fromTurnCount: number, | ||
| 69 | toTurnCount: number, | ||
| 70 | ): string { | ||
| 71 | return `${sessionId}:${fromTurnCount}-${toTurnCount}`; | ||
| 72 | } | ||
| 73 | |||
| 74 | interface DiffPanelProps { | 66 | interface DiffPanelProps { |
| 75 | mode?: "inline" | "sheet"; | 67 | mode?: "inline" | "sheet"; |
| 76 | } | 68 | } |
| ... | @@ -79,12 +71,13 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -79,12 +71,13 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 79 | const api = useNativeApi(); | 71 | const api = useNativeApi(); |
| 80 | const { resolvedTheme } = useTheme(); | 72 | const { resolvedTheme } = useTheme(); |
| 81 | const { state, dispatch } = useStore(); | 73 | const { state, dispatch } = useStore(); |
| 82 | const [checkpointDiffByKey, setCheckpointDiffByKey] = useState<Record<string, string>>({}); | 74 | const queryClient = useQueryClient(); |
| 83 | const [isLoadingCheckpointDiff, setIsLoadingCheckpointDiff] = useState(false); | ||
| 84 | const [checkpointDiffError, setCheckpointDiffError] = useState<string | null>(null); | ||
| 85 | const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked"); | 75 | const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked"); |
| 86 | const patchViewportRef = useRef<HTMLDivElement>(null); | 76 | const patchViewportRef = useRef<HTMLDivElement>(null); |
| 87 | const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); | 77 | const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); |
| 78 | const activeThreadRuntimeId = | ||
| 79 | activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; | ||
| 80 | const activeSessionId = activeThread?.session?.sessionId ?? null; | ||
| 88 | const turnDiffSummaries = useMemo( | 81 | const turnDiffSummaries = useMemo( |
| 89 | () => | 82 | () => |
| 90 | activeThread?.turnDiffSummaries.length | 83 | activeThread?.turnDiffSummaries.length |
| ... | @@ -97,11 +90,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -97,11 +90,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 97 | [turnDiffSummaries], | 90 | [turnDiffSummaries], |
| 98 | ); | 91 | ); |
| 99 | 92 | ||
| 100 | useEffect(() => { | ||
| 101 | setCheckpointDiffByKey({}); | ||
| 102 | setCheckpointDiffError(null); | ||
| 103 | }, [activeThread?.id]); | ||
| 104 | |||
| 105 | const canApplyStoredTarget = Boolean(activeThread && state.diffThreadId === activeThread.id); | 93 | const canApplyStoredTarget = Boolean(activeThread && state.diffThreadId === activeThread.id); |
| 106 | const selectedTurnId = canApplyStoredTarget ? state.diffTurnId : null; | 94 | const selectedTurnId = canApplyStoredTarget ? state.diffTurnId : null; |
| 107 | const selectedFilePath = | 95 | const selectedFilePath = |
| ... | @@ -147,41 +135,57 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -147,41 +135,57 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 147 | : null, | 135 | : null, |
| 148 | [conversationCheckpointTurnCount, selectedTurn], | 136 | [conversationCheckpointTurnCount, selectedTurn], |
| 149 | ); | 137 | ); |
| 150 | const selectedCheckpointDiffKey = | 138 | const activeCheckpointRange = selectedTurn |
| 151 | activeThread?.session?.sessionId && selectedCheckpointRange | 139 | ? selectedCheckpointRange |
| 152 | ? buildCheckpointDiffKey( | 140 | : conversationCheckpointRange; |
| 153 | activeThread.session.sessionId, | 141 | const activeCheckpointDiffQuery = useQuery( |
| 154 | selectedCheckpointRange.fromTurnCount, | 142 | checkpointDiffQueryOptions(api, { |
| 155 | selectedCheckpointRange.toTurnCount, | 143 | sessionId: activeSessionId, |
| 156 | ) | 144 | threadRuntimeId: activeThreadRuntimeId, |
| 157 | : null; | 145 | fromTurnCount: activeCheckpointRange?.fromTurnCount ?? null, |
| 158 | const conversationCheckpointDiffKey = | 146 | toTurnCount: activeCheckpointRange?.toTurnCount ?? null, |
| 159 | activeThread?.session?.sessionId && conversationCheckpointRange | 147 | }), |
| 160 | ? buildCheckpointDiffKey( | 148 | ); |
| 161 | activeThread.session.sessionId, | 149 | const selectedTurnCheckpointDiff = selectedTurn |
| 162 | conversationCheckpointRange.fromTurnCount, | 150 | ? activeCheckpointDiffQuery.data?.diff |
| 163 | conversationCheckpointRange.toTurnCount, | 151 | : undefined; |
| 164 | ) | 152 | const conversationCheckpointDiff = selectedTurn |
| 165 | : null; | 153 | ? undefined |
| 166 | const selectedTurnCheckpointDiff = | 154 | : activeCheckpointDiffQuery.data?.diff; |
| 167 | selectedCheckpointDiffKey === null ? undefined : checkpointDiffByKey[selectedCheckpointDiffKey]; | 155 | const isLoadingCheckpointDiff = activeCheckpointDiffQuery.isPending; |
| 168 | const conversationCheckpointDiff = | 156 | const checkpointDiffError = |
| 169 | conversationCheckpointDiffKey === null | 157 | activeCheckpointDiffQuery.error instanceof Error |
| 170 | ? undefined | 158 | ? activeCheckpointDiffQuery.error.message |
| 171 | : checkpointDiffByKey[conversationCheckpointDiffKey]; | 159 | : activeCheckpointDiffQuery.error |
| 160 | ? "Failed to load checkpoint diff." | ||
| 161 | : null; | ||
| 162 | |||
| 163 | useEffect(() => { | ||
| 164 | if (!activeThread?.id || !selectedTurn || !selectedTurnCheckpointDiff) { | ||
| 165 | return; | ||
| 166 | } | ||
| 167 | dispatch({ | ||
| 168 | type: "SET_THREAD_TURN_CHECKPOINT_DIFFS", | ||
| 169 | threadId: activeThread.id, | ||
| 170 | checkpointDiffByTurnId: { | ||
| 171 | [selectedTurn.turnId]: selectedTurnCheckpointDiff, | ||
| 172 | }, | ||
| 173 | }); | ||
| 174 | }, [activeThread?.id, dispatch, selectedTurn, selectedTurnCheckpointDiff]); | ||
| 175 | |||
| 172 | const selectedPatch = useMemo(() => { | 176 | const selectedPatch = useMemo(() => { |
| 173 | const patchForSummary = (summary: (typeof turnDiffSummaries)[number]): string | undefined => { | 177 | const patchForSummary = (summary: (typeof turnDiffSummaries)[number]): string | undefined => { |
| 174 | const checkpointTurnCount = | 178 | const checkpointTurnCount = |
| 175 | summary.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[summary.turnId]; | 179 | summary.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[summary.turnId]; |
| 176 | if (activeThread?.session?.sessionId && typeof checkpointTurnCount === "number") { | 180 | if (activeSessionId && typeof checkpointTurnCount === "number") { |
| 177 | const checkpointPatch = | 181 | const checkpointPatch = queryClient.getQueryData<{ diff: string }>( |
| 178 | checkpointDiffByKey[ | 182 | providerQueryKeys.checkpointDiff({ |
| 179 | buildCheckpointDiffKey( | 183 | sessionId: activeSessionId, |
| 180 | activeThread.session.sessionId, | 184 | threadRuntimeId: activeThreadRuntimeId, |
| 181 | Math.max(0, checkpointTurnCount - 1), | 185 | fromTurnCount: Math.max(0, checkpointTurnCount - 1), |
| 182 | checkpointTurnCount, | 186 | toTurnCount: checkpointTurnCount, |
| 183 | ) | 187 | }), |
| 184 | ]; | 188 | )?.diff; |
| 185 | if (checkpointPatch) { | 189 | if (checkpointPatch) { |
| 186 | return checkpointPatch; | 190 | return checkpointPatch; |
| 187 | } | 191 | } |
| ... | @@ -239,10 +243,11 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -239,10 +243,11 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 239 | } | 243 | } |
| 240 | return patches.join("\n\n"); | 244 | return patches.join("\n\n"); |
| 241 | }, [ | 245 | }, [ |
| 242 | activeThread?.session?.sessionId, | 246 | activeSessionId, |
| 243 | checkpointDiffByKey, | 247 | activeThreadRuntimeId, |
| 244 | conversationCheckpointDiff, | 248 | conversationCheckpointDiff, |
| 245 | inferredCheckpointTurnCountByTurnId, | 249 | inferredCheckpointTurnCountByTurnId, |
| 250 | queryClient, | ||
| 246 | selectedTurn, | 251 | selectedTurn, |
| 247 | selectedTurnCheckpointDiff, | 252 | selectedTurnCheckpointDiff, |
| 248 | turnDiffSummaries, | 253 | turnDiffSummaries, |
| ... | @@ -260,72 +265,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { | ... | @@ -260,72 +265,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 260 | ); | 265 | ); |
| 261 | }, [renderablePatch]); | 266 | }, [renderablePatch]); |
| 262 | 267 | ||
| 263 | useEffect(() => { | ||
| 264 | const checkpointFetchRange = selectedTurn | ||
| 265 | ? selectedCheckpointRange | ||
| 266 | : conversationCheckpointRange; | ||
| 267 | const checkpointFetchKey = selectedTurn | ||
| 268 | ? selectedCheckpointDiffKey | ||
| 269 | : conversationCheckpointDiffKey; | ||
| 270 | if (!api || !activeThread?.session?.sessionId || !checkpointFetchRange || !checkpointFetchKey) { | ||
| 271 | return; | ||
| 272 | } | ||
| 273 | if (checkpointDiffByKey[checkpointFetchKey] !== undefined) { | ||
| 274 | return; | ||
| 275 | } | ||
| 276 | |||
| 277 | let cancelled = false; | ||
| 278 | setIsLoadingCheckpointDiff(true); | ||
| 279 | setCheckpointDiffError(null); | ||
| 280 | void api.providers | ||
| 281 | .getCheckpointDiff({ | ||
| 282 | sessionId: activeThread.session.sessionId, | ||
| 283 | fromTurnCount: checkpointFetchRange.fromTurnCount, | ||
| 284 | toTurnCount: checkpointFetchRange.toTurnCount, | ||
| 285 | }) | ||
| 286 | .then((result) => { | ||
| 287 | if (cancelled) return; | ||
| 288 | setCheckpointDiffByKey((previous) => ({ | ||
| 289 | ...previous, | ||
| 290 | [checkpointFetchKey]: result.diff, | ||
| 291 | })); | ||
| 292 | if (selectedTurn) { | ||
| 293 | dispatch({ | ||
| 294 | type: "SET_THREAD_TURN_CHECKPOINT_DIFFS", | ||
| 295 | threadId: activeThread.id, | ||
| 296 | checkpointDiffByTurnId: { | ||
| 297 | [selectedTurn.turnId]: result.diff, | ||
| 298 | }, | ||
| 299 | }); | ||
| 300 | } | ||
| 301 | }) | ||
| 302 | .catch((error) => { | ||
| 303 | if (cancelled) return; | ||
| 304 | setCheckpointDiffError( | ||
| 305 | error instanceof Error ? error.message : "Failed to load checkpoint diff.", | ||
| 306 | ); | ||
| 307 | }) | ||
| 308 | .finally(() => { | ||
| 309 | if (cancelled) return; | ||
| 310 | setIsLoadingCheckpointDiff(false); | ||
| 311 | }); | ||
| 312 | |||
| 313 | return () => { | ||
| 314 | cancelled = true; | ||
| 315 | }; | ||
| 316 | }, [ | ||
| 317 | activeThread?.id, | ||
| 318 | activeThread?.session?.sessionId, | ||
| 319 | api, | ||
| 320 | checkpointDiffByKey, | ||
| 321 | conversationCheckpointDiffKey, | ||
| 322 | conversationCheckpointRange, | ||
| 323 | dispatch, | ||
| 324 | selectedCheckpointDiffKey, | ||
| 325 | selectedCheckpointRange, | ||
| 326 | selectedTurn, | ||
| 327 | ]); | ||
| 328 | |||
| 329 | useEffect(() => { | 268 | useEffect(() => { |
| 330 | if (!selectedFilePath || !patchViewportRef.current) { | 269 | if (!selectedFilePath || !patchViewportRef.current) { |
| 331 | return; | 270 | return; |
apps/web/src/lib/providerReactQuery.ts created+52| ... | @@ -0,0 +1,52 @@ | ||
| 1 | import type { NativeApi } from "@t3tools/contracts"; | ||
| 2 | import { queryOptions } from "@tanstack/react-query"; | ||
| 3 | |||
| 4 | export interface CheckpointDiffQueryInput { | ||
| 5 | sessionId: string | null; | ||
| 6 | threadRuntimeId: string | null; | ||
| 7 | fromTurnCount: number | null; | ||
| 8 | toTurnCount: number | null; | ||
| 9 | } | ||
| 10 | |||
| 11 | export const providerQueryKeys = { | ||
| 12 | all: ["providers"] as const, | ||
| 13 | checkpointDiff: (input: CheckpointDiffQueryInput) => | ||
| 14 | [ | ||
| 15 | "providers", | ||
| 16 | "checkpointDiff", | ||
| 17 | input.sessionId, | ||
| 18 | input.threadRuntimeId, | ||
| 19 | input.fromTurnCount, | ||
| 20 | input.toTurnCount, | ||
| 21 | ] as const, | ||
| 22 | }; | ||
| 23 | |||
| 24 | export function checkpointDiffQueryOptions( | ||
| 25 | api: NativeApi | undefined, | ||
| 26 | input: CheckpointDiffQueryInput, | ||
| 27 | ) { | ||
| 28 | const hasValidRange = | ||
| 29 | typeof input.fromTurnCount === "number" && typeof input.toTurnCount === "number"; | ||
| 30 | |||
| 31 | return queryOptions({ | ||
| 32 | queryKey: providerQueryKeys.checkpointDiff(input), | ||
| 33 | queryFn: async () => { | ||
| 34 | if (!api || !input.sessionId || !hasValidRange) { | ||
| 35 | throw new Error("Checkpoint diff is unavailable."); | ||
| 36 | } | ||
| 37 | const { fromTurnCount, toTurnCount } = input; | ||
| 38 | if (typeof fromTurnCount !== "number" || typeof toTurnCount !== "number") { | ||
| 39 | throw new Error("Checkpoint diff range is invalid."); | ||
| 40 | } | ||
| 41 | return api.providers.getCheckpointDiff({ | ||
| 42 | sessionId: input.sessionId, | ||
| 43 | fromTurnCount, | ||
| 44 | toTurnCount, | ||
| 45 | }); | ||
| 46 | }, | ||
| 47 | enabled: !!api && !!input.sessionId && hasValidRange, | ||
| 48 | staleTime: Infinity, | ||
| 49 | retry: 8, | ||
| 50 | retryDelay: (attempt) => Math.min(2_000, 150 * 2 ** (attempt - 1)), | ||
| 51 | }); | ||
| 52 | } | ||
apps/web/src/store.test.ts+6-1| ... | @@ -880,7 +880,12 @@ describe("store reducer thread continuity", () => { | ... | @@ -880,7 +880,12 @@ describe("store reducer thread continuity", () => { |
| 880 | { | 880 | { |
| 881 | turnId: "turn-1", | 881 | turnId: "turn-1", |
| 882 | completedAt: "2026-02-09T00:00:03.000Z", | 882 | completedAt: "2026-02-09T00:00:03.000Z", |
| 883 | files: [{ path: "src/from-checkpoint.ts", diff: "diff --git a/src/from-checkpoint.ts b/src/from-checkpoint.ts" }], | 883 | files: [ |
| 884 | { | ||
| 885 | path: "src/from-checkpoint.ts", | ||
| 886 | diff: "diff --git a/src/from-checkpoint.ts b/src/from-checkpoint.ts", | ||
| 887 | }, | ||
| 888 | ], | ||
| 884 | unifiedDiff: "diff --git a/src/from-checkpoint.ts b/src/from-checkpoint.ts", | 889 | unifiedDiff: "diff --git a/src/from-checkpoint.ts b/src/from-checkpoint.ts", |
| 885 | checkpointDiffLoaded: true, | 890 | checkpointDiffLoaded: true, |
| 886 | }, | 891 | }, |
apps/web/src/store.ts+5-1| ... | @@ -1075,7 +1075,11 @@ export function reducer(state: AppState, action: Action): AppState { | ... | @@ -1075,7 +1075,11 @@ export function reducer(state: AppState, action: Action): AppState { |
| 1075 | existing.deletions === file.deletions | 1075 | existing.deletions === file.deletions |
| 1076 | ); | 1076 | ); |
| 1077 | }); | 1077 | }); |
| 1078 | if (summary.unifiedDiff === checkpointDiff && filesUnchanged && summary.checkpointDiffLoaded) { | 1078 | if ( |
| 1079 | summary.unifiedDiff === checkpointDiff && | ||
| 1080 | filesUnchanged && | ||
| 1081 | summary.checkpointDiffLoaded | ||
| 1082 | ) { | ||
| 1079 | return summary; | 1083 | return summary; |
| 1080 | } | 1084 | } |
| 1081 | 1085 |
packages/contracts/src/ipc.ts+3-1| ... | @@ -95,7 +95,9 @@ export interface NativeApi { | ... | @@ -95,7 +95,9 @@ export interface NativeApi { |
| 95 | respondToRequest: (input: ProviderRespondToRequestInput) => Promise<void>; | 95 | respondToRequest: (input: ProviderRespondToRequestInput) => Promise<void>; |
| 96 | stopSession: (input: ProviderStopSessionInput) => Promise<void>; | 96 | stopSession: (input: ProviderStopSessionInput) => Promise<void>; |
| 97 | listSessions: () => Promise<ProviderSession[]>; | 97 | listSessions: () => Promise<ProviderSession[]>; |
| 98 | listCheckpoints: (input: ProviderListCheckpointsInput) => Promise<ProviderListCheckpointsResult>; | 98 | listCheckpoints: ( |
| 99 | input: ProviderListCheckpointsInput, | ||
| 100 | ) => Promise<ProviderListCheckpointsResult>; | ||
| 99 | getCheckpointDiff: ( | 101 | getCheckpointDiff: ( |
| 100 | input: ProviderGetCheckpointDiffInput, | 102 | input: ProviderGetCheckpointDiffInput, |
| 101 | ) => Promise<ProviderGetCheckpointDiffResult>; | 103 | ) => Promise<ProviderGetCheckpointDiffResult>; |