Push-driven checkout diff subscriptions for changes sidebar (#24)

* Implement push-driven checkout diff subscriptions

* Remove legacy checkout diff polling RPC

* Fix checkout diff watcher coverage and fallback refresh
This commit is contained in:
Mohamed Boudra
2026-02-11 14:11:00 +08:00
committed by GitHub
parent 8e5ba622ee
commit a6f60091f2
9 changed files with 1066 additions and 67 deletions
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import {
compareCheckoutDiffPaths,
orderCheckoutDiffFiles,
} from "./checkout-diff-order";
function createFile(path: string, additions = 0) {
return {
path,
isNew: false,
isDeleted: false,
additions,
deletions: 0,
hunks: [],
};
}
describe("checkout diff ordering", () => {
it("compares paths deterministically", () => {
expect(compareCheckoutDiffPaths("a.ts", "b.ts")).toBeLessThan(0);
expect(compareCheckoutDiffPaths("b.ts", "a.ts")).toBeGreaterThan(0);
expect(compareCheckoutDiffPaths("same.ts", "same.ts")).toBe(0);
});
it("sorts files by path", () => {
const ordered = orderCheckoutDiffFiles([
createFile("zeta.ts"),
createFile("alpha.ts"),
createFile("beta.ts"),
]);
expect(ordered.map((file) => file.path)).toEqual([
"alpha.ts",
"beta.ts",
"zeta.ts",
]);
});
it("preserves relative order for equal paths", () => {
const ordered = orderCheckoutDiffFiles([
createFile("same.ts", 1),
createFile("same.ts", 2),
createFile("same.ts", 3),
]);
expect(ordered.map((file) => file.additions)).toEqual([1, 2, 3]);
});
});
@@ -0,0 +1,21 @@
import type { SubscribeCheckoutDiffResponse } from "@server/shared/messages";
type ParsedDiffFile = SubscribeCheckoutDiffResponse["payload"]["files"][number];
export function compareCheckoutDiffPaths(left: string, right: string): number {
if (left === right) {
return 0;
}
return left < right ? -1 : 1;
}
export function orderCheckoutDiffFiles(
files: ParsedDiffFile[]
): ParsedDiffFile[] {
if (files.length < 2) {
return files;
}
const ordered = [...files];
ordered.sort((a, b) => compareCheckoutDiffPaths(a.path, b.path));
return ordered;
}
+143 -11
View File
@@ -1,9 +1,10 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useId, useMemo } from "react";
import { UnistylesRuntime } from "react-native-unistyles";
import { useSessionStore } from "@/stores/session-store";
import { usePanelStore } from "@/stores/panel-store";
import type { CheckoutDiffResponse } from "@server/shared/messages";
import type { SubscribeCheckoutDiffResponse } from "@server/shared/messages";
import { orderCheckoutDiffFiles } from "./checkout-diff-order";
const CHECKOUT_DIFF_STALE_TIME = 30_000;
@@ -24,11 +25,29 @@ interface UseCheckoutDiffQueryOptions {
enabled?: boolean;
}
export type ParsedDiffFile = CheckoutDiffResponse["payload"]["files"][number];
type CheckoutDiffQueryPayload = Omit<
SubscribeCheckoutDiffResponse["payload"],
"subscriptionId"
>;
export type ParsedDiffFile = CheckoutDiffQueryPayload["files"][number];
export type DiffHunk = ParsedDiffFile["hunks"][number];
export type DiffLine = DiffHunk["lines"][number];
export type HighlightToken = NonNullable<DiffLine["tokens"]>[number];
function normalizeCheckoutDiffCompare(compare: {
mode: "uncommitted" | "base";
baseRef?: string;
}): { mode: "uncommitted" | "base"; baseRef?: string } {
if (compare.mode === "uncommitted") {
return { mode: "uncommitted" };
}
const trimmedBaseRef = compare.baseRef?.trim();
return trimmedBaseRef
? { mode: "base", baseRef: trimmedBaseRef }
: { mode: "base" };
}
export function useCheckoutDiffQuery({
serverId,
cwd,
@@ -49,29 +68,142 @@ export function useCheckoutDiffQuery({
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const explorerTab = usePanelStore((state) => state.explorerTab);
const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
const hookInstanceId = useId();
const normalizedCompare = useMemo(
() => normalizeCheckoutDiffCompare({ mode, baseRef }),
[mode, baseRef]
);
const compareMode = normalizedCompare.mode;
const compareBaseRef = normalizedCompare.baseRef;
const queryKey = useMemo(
() => checkoutDiffQueryKey(serverId, cwd, mode, baseRef),
[serverId, cwd, mode, baseRef]
);
const query = useQuery({
queryKey: checkoutDiffQueryKey(serverId, cwd, mode, baseRef),
queryKey,
queryFn: async () => {
if (!client) {
throw new Error("Daemon client not available");
}
return await client.getCheckoutDiff(cwd, { mode, baseRef });
const payload = await client.getCheckoutDiff(cwd, {
mode: compareMode,
baseRef: compareBaseRef,
});
return {
...payload,
files: orderCheckoutDiffFiles(payload.files),
};
},
enabled: !!client && isConnected && !!cwd && enabled,
staleTime: CHECKOUT_DIFF_STALE_TIME,
refetchInterval: 10_000,
});
// Revalidate when sidebar opens with "changes" tab active
useEffect(() => {
if (!isOpen || explorerTab !== "changes" || !cwd) {
if (!client || !isConnected || !cwd || !enabled) {
return;
}
queryClient.invalidateQueries({
queryKey: checkoutDiffQueryKey(serverId, cwd, mode, baseRef),
if (!isOpen || explorerTab !== "changes") {
return;
}
const subscriptionId = [
"checkoutDiff",
hookInstanceId,
serverId,
cwd,
compareMode,
compareBaseRef ?? "",
].join(":");
let cancelled = false;
const unsubscribeUpdate = client.on("checkout_diff_update", (message) => {
if (message.type !== "checkout_diff_update") {
return;
}
if (message.payload.subscriptionId !== subscriptionId) {
return;
}
queryClient.setQueryData<CheckoutDiffQueryPayload>(queryKey, {
cwd: message.payload.cwd,
files: orderCheckoutDiffFiles(message.payload.files),
error: message.payload.error,
requestId: `subscription:${subscriptionId}`,
});
});
}, [isOpen, explorerTab, serverId, cwd, mode, baseRef, queryClient]);
const unsubscribeSubscribeResponse = client.on(
"subscribe_checkout_diff_response",
(message) => {
if (message.type !== "subscribe_checkout_diff_response") {
return;
}
if (message.payload.subscriptionId !== subscriptionId) {
return;
}
queryClient.setQueryData<CheckoutDiffQueryPayload>(queryKey, {
cwd: message.payload.cwd,
files: orderCheckoutDiffFiles(message.payload.files),
error: message.payload.error,
requestId: message.payload.requestId,
});
}
);
void client
.subscribeCheckoutDiff(
cwd,
{
mode: compareMode,
baseRef: compareBaseRef,
},
{ subscriptionId }
)
.then((payload) => {
if (cancelled) {
return;
}
queryClient.setQueryData<CheckoutDiffQueryPayload>(queryKey, {
cwd: payload.cwd,
files: orderCheckoutDiffFiles(payload.files),
error: payload.error,
requestId: payload.requestId,
});
})
.catch((error) => {
if (cancelled) {
return;
}
console.error("[useCheckoutDiffQuery] subscribeCheckoutDiff failed", {
serverId,
cwd,
error,
});
});
return () => {
cancelled = true;
unsubscribeUpdate();
unsubscribeSubscribeResponse();
try {
client.unsubscribeCheckoutDiff(subscriptionId);
} catch {
// Ignore disconnect race during effect cleanup.
}
};
}, [
client,
isConnected,
cwd,
enabled,
isOpen,
explorerTab,
hookInstanceId,
serverId,
compareMode,
compareBaseRef,
queryKey,
queryClient,
]);
const refresh = useCallback(() => {
return query.refetch();