mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* Extract client SDK package * Polish SDK client identity defaults * Build client before dependent CI jobs * Restore daemon client server export * Extract protocol and client SDK packages * Fix provider override schema validation * Fix app test daemon client imports * Simplify workspace build targets * Fix CLI test server build bootstrap * Run SDK package tests in CI * Fix rebase package split drift * Restore lockfile registry metadata * Update SDK config test for prompt default * Move terminal stream router test to client package * Fix rebase drift for protocol imports * Fix SDK agent capability fixture * Restore legacy server client exports * Fix server export compatibility test * Advertise custom mode icon client capability * Remove server daemon-client exports * Format rebased mode control import * Fix rebase drift for protocol imports Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split shared/ paths that this branch moves into @getpaseo/protocol. Redirect those imports to the protocol package so typecheck stays green after the rebase.
180 lines
5.2 KiB
TypeScript
180 lines
5.2 KiB
TypeScript
import { skipToken, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { useEffect, useId, useMemo } from "react";
|
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
|
import type { SubscribeCheckoutDiffResponse } from "@getpaseo/protocol/messages";
|
|
import { orderCheckoutDiffFiles } from "@/git/diff-order";
|
|
import { checkoutDiffQueryKey } from "@/git/query-keys";
|
|
|
|
interface UseCheckoutDiffQueryOptions {
|
|
serverId: string;
|
|
cwd: string;
|
|
mode: "uncommitted" | "base";
|
|
baseRef?: string;
|
|
ignoreWhitespace?: boolean;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
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;
|
|
ignoreWhitespace?: boolean;
|
|
}): { mode: "uncommitted" | "base"; baseRef?: string; ignoreWhitespace?: boolean } {
|
|
const ignoreWhitespace = compare.ignoreWhitespace === true;
|
|
if (compare.mode === "uncommitted") {
|
|
return { mode: "uncommitted", ignoreWhitespace };
|
|
}
|
|
const trimmedBaseRef = compare.baseRef?.trim();
|
|
return trimmedBaseRef
|
|
? { mode: "base", baseRef: trimmedBaseRef, ignoreWhitespace }
|
|
: { mode: "base", ignoreWhitespace };
|
|
}
|
|
|
|
export function useCheckoutDiffQuery({
|
|
serverId,
|
|
cwd,
|
|
mode,
|
|
baseRef,
|
|
ignoreWhitespace,
|
|
enabled = true,
|
|
}: UseCheckoutDiffQueryOptions) {
|
|
const queryClient = useQueryClient();
|
|
const client = useHostRuntimeClient(serverId);
|
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
|
const hookInstanceId = useId();
|
|
const normalizedCompare = useMemo(
|
|
() => normalizeCheckoutDiffCompare({ mode, baseRef, ignoreWhitespace }),
|
|
[mode, baseRef, ignoreWhitespace],
|
|
);
|
|
const compareMode = normalizedCompare.mode;
|
|
const compareBaseRef = normalizedCompare.baseRef;
|
|
const compareIgnoreWhitespace = normalizedCompare.ignoreWhitespace;
|
|
const queryKey = useMemo(
|
|
() => checkoutDiffQueryKey(serverId, cwd, mode, baseRef, compareIgnoreWhitespace),
|
|
[serverId, cwd, mode, baseRef, compareIgnoreWhitespace],
|
|
);
|
|
|
|
const query = useQuery<CheckoutDiffQueryPayload>({
|
|
queryKey,
|
|
queryFn: skipToken,
|
|
staleTime: Infinity,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!client || !isConnected || !cwd || !enabled) {
|
|
return;
|
|
}
|
|
|
|
const subscriptionId = [
|
|
"checkoutDiff",
|
|
hookInstanceId,
|
|
serverId,
|
|
cwd,
|
|
compareMode,
|
|
compareBaseRef ?? "",
|
|
compareIgnoreWhitespace ? "ignore-ws" : "keep-ws",
|
|
].join(":");
|
|
let cancelled = false;
|
|
|
|
const unsubscribeUpdate = client.on("checkout_diff_update", (message) => {
|
|
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}`,
|
|
});
|
|
});
|
|
const unsubscribeSubscribeResponse = client.on(
|
|
"subscribe_checkout_diff_response",
|
|
(message) => {
|
|
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,
|
|
ignoreWhitespace: compareIgnoreWhitespace,
|
|
},
|
|
{ subscriptionId },
|
|
)
|
|
.then((payload) => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
queryClient.setQueryData<CheckoutDiffQueryPayload>(queryKey, {
|
|
cwd: payload.cwd,
|
|
files: orderCheckoutDiffFiles(payload.files),
|
|
error: payload.error,
|
|
requestId: payload.requestId,
|
|
});
|
|
return;
|
|
})
|
|
.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,
|
|
hookInstanceId,
|
|
serverId,
|
|
compareMode,
|
|
compareBaseRef,
|
|
compareIgnoreWhitespace,
|
|
queryKey,
|
|
queryClient,
|
|
]);
|
|
|
|
const payload = query.data ?? null;
|
|
const payloadError = payload?.error ?? null;
|
|
|
|
return {
|
|
files: payload?.files ?? [],
|
|
payloadError,
|
|
isLoading: payload === null && enabled && isConnected,
|
|
isFetching: false,
|
|
isError: Boolean(payloadError),
|
|
error: null,
|
|
};
|
|
}
|