* fix(workspaces): show isolation without checkout delay * fix(workspaces): trust hydrated project capability * fix(workspaces): resolve branch before creation * fix(workspaces): honor invalidated checkout status * fix(workspaces): preserve isolation while projects load * fix(workspaces): keep provisional project routes usable Route-derived projects do not know their worktree capability. Represent that uncertainty at the placement boundary so remembered worktree intent survives hydration without disabling the New Workspace screen indefinitely.
113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
import type { QueryClient } from "@tanstack/react-query";
|
|
import type { CheckoutStatusResponse, CheckoutStatusUpdate } from "@getpaseo/protocol/messages";
|
|
import equal from "fast-deep-equal/es6";
|
|
import {
|
|
checkoutCommitsQueryKey,
|
|
checkoutPrStatusQueryKey,
|
|
checkoutStatusQueryKey,
|
|
invalidatePrPaneTimelineForCheckout,
|
|
} from "@/git/query-keys";
|
|
import { type CheckoutPrStatusPayload, normalizeCheckoutPrStatusPayload } from "@/git/pr-status";
|
|
import { expireStaleDiffModeOverrides } from "@/review/store";
|
|
|
|
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
|
|
export type { CheckoutPrStatusPayload } from "@/git/pr-status";
|
|
|
|
export interface CheckoutStatusClient {
|
|
getCheckoutStatus: (cwd: string) => Promise<CheckoutStatusPayload>;
|
|
}
|
|
|
|
// 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,
|
|
}: {
|
|
client: CheckoutStatusClient;
|
|
serverId: string;
|
|
cwd: string;
|
|
}): Promise<CheckoutStatusPayload> {
|
|
const payload = await client.getCheckoutStatus(cwd);
|
|
expireStaleDiffModeOverrides({ serverId, cwd, isDirty: payload.isGit && payload.isDirty });
|
|
return payload;
|
|
}
|
|
|
|
export async function ensureCheckoutStatus({
|
|
queryClient,
|
|
client,
|
|
serverId,
|
|
cwd,
|
|
}: {
|
|
queryClient: QueryClient;
|
|
client: CheckoutStatusClient;
|
|
serverId: string;
|
|
cwd: string;
|
|
}): Promise<CheckoutStatusPayload> {
|
|
return await queryClient.fetchQuery({
|
|
queryKey: checkoutStatusQueryKey(serverId, cwd),
|
|
queryFn: () => fetchCheckoutStatus({ client, serverId, cwd }),
|
|
staleTime: Infinity,
|
|
});
|
|
}
|
|
|
|
export function applyCheckoutStatusUpdateFromEvent({
|
|
queryClient,
|
|
serverId,
|
|
message,
|
|
}: {
|
|
queryClient: QueryClient;
|
|
serverId: string;
|
|
message: CheckoutStatusUpdate;
|
|
}): void {
|
|
const { payload } = message;
|
|
const prStatus = payload.prStatus
|
|
? normalizeCheckoutPrStatusPayload(payload.prStatus)
|
|
: undefined;
|
|
const cachePayload = prStatus ? { ...payload, prStatus } : payload;
|
|
queryClient.setQueryData(checkoutStatusQueryKey(serverId, payload.cwd), cachePayload);
|
|
void queryClient.invalidateQueries({
|
|
queryKey: checkoutCommitsQueryKey(serverId, payload.cwd),
|
|
});
|
|
expireStaleDiffModeOverrides({
|
|
serverId,
|
|
cwd: payload.cwd,
|
|
isDirty: payload.isGit && payload.isDirty,
|
|
});
|
|
|
|
if (!prStatus) {
|
|
return;
|
|
}
|
|
|
|
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));
|
|
}
|