Keep git review state fresh after reconnect (#1491)

This commit is contained in:
Mohamed Boudra
2026-06-12 16:55:18 +00:00
committed by GitHub
parent 8cd51c8a47
commit b0ca08a452
16 changed files with 592 additions and 234 deletions
+56 -18
View File
@@ -1,48 +1,86 @@
import type { QueryClient } from "@tanstack/react-query";
import type { CheckoutStatusResponse, CheckoutStatusUpdate } from "@getpaseo/protocol/messages";
import { checkoutStatusQueryKey } from "@/git/query-keys";
import equal from "fast-deep-equal/es6";
import {
checkoutPrStatusQueryKey,
checkoutStatusQueryKey,
invalidatePrPaneTimelineForCheckout,
} from "@/git/query-keys";
import { expireStaleDiffModeOverrides } from "@/review/store";
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
export type CheckoutPrStatusPayload = NonNullable<CheckoutStatusUpdate["payload"]["prStatus"]>;
export interface CheckoutStatusClient {
getCheckoutStatus: (cwd: string) => Promise<CheckoutStatusPayload>;
}
export async function peekOrFetchCheckoutStatus({
queryClient,
// Checkout status enters the app through exactly two doors: daemon pushes
// (applyCheckoutStatusUpdateFromEvent) and query fetches (fetchCheckoutStatus). Both run
// the dirty-state reactions, so they hold regardless of which screens are mounted.
export async function fetchCheckoutStatus({
client,
serverId,
cwd,
}: {
queryClient: QueryClient;
client: CheckoutStatusClient;
serverId: string;
cwd: string;
}): Promise<CheckoutStatusPayload> {
const queryKey = checkoutStatusQueryKey(serverId, cwd);
const cached = queryClient.getQueryData<CheckoutStatusPayload>(queryKey);
if (cached) {
return cached;
}
const snapshot = await client.getCheckoutStatus(cwd);
queryClient.setQueryData(queryKey, snapshot);
return snapshot;
const payload = await client.getCheckoutStatus(cwd);
expireStaleDiffModeOverrides({ serverId, cwd, isDirty: payload.isGit && payload.isDirty });
return payload;
}
export function applyCheckoutStatusUpdate({
export function applyCheckoutStatusUpdateFromEvent({
queryClient,
serverId,
cwd,
message,
}: {
queryClient: QueryClient;
serverId: string;
cwd: string;
message: CheckoutStatusUpdate;
}): void {
if (message.payload.cwd !== cwd) {
const { payload } = message;
queryClient.setQueryData(checkoutStatusQueryKey(serverId, payload.cwd), payload);
expireStaleDiffModeOverrides({
serverId,
cwd: payload.cwd,
isDirty: payload.isGit && payload.isDirty,
});
const prStatus = payload.prStatus;
if (!prStatus) {
return;
}
queryClient.setQueryData(checkoutStatusQueryKey(serverId, cwd), message.payload);
const previous = queryClient.getQueryData<CheckoutPrStatusPayload>(
checkoutPrStatusQueryKey(serverId, prStatus.cwd),
);
queryClient.setQueryData(checkoutPrStatusQueryKey(serverId, prStatus.cwd), prStatus);
// The PR activity timeline has no push channel; mark it stale when the pushed PR status
// meaningfully changed. Active panes refetch immediately, evicted ones on next mount.
if (hasPrStatusChanged(previous, prStatus)) {
void invalidatePrPaneTimelineForCheckout(queryClient, { serverId, cwd: prStatus.cwd });
}
}
// requestId changes on every emission and carries no PR state.
function prStatusWithoutVolatileFields(
prStatus: CheckoutPrStatusPayload,
): Omit<CheckoutPrStatusPayload, "requestId"> {
const { requestId: _requestId, ...rest } = prStatus;
return rest;
}
function hasPrStatusChanged(
previous: CheckoutPrStatusPayload | undefined,
next: CheckoutPrStatusPayload,
): boolean {
if (!previous) {
return true;
}
return !equal(prStatusWithoutVolatileFields(previous), prStatusWithoutVolatileFields(next));
}