feat: lazy-load diff RPCs for mobile performance

Opening a workspace with a large diff blocked the JS thread because the
full structured diff (hunks, lines, syntax tokens) was sent over the
wire and parsed via JSON.parse + Zod safeParse on the main thread.

Add namespaced checkout RPCs that split diff delivery into two phases:

1. **Metadata-only snapshots** (`checkout/subscribe_diff` →
   `checkout/diff_snapshot`) — lightweight file list with status, path,
   insertions/deletions, and a fingerprint. Sent on every file-system
   change. No hunks, no tokens.

2. **On-demand hunk fetching** (`checkout/get_file_hunks` →
   `checkout/file_hunks_response`) — full structured diff for a single
   file, fetched when the user expands it. Served from an in-memory
   cache with a single-file git fallback.

Feature detection via `server_info.features.lazyDiffRpcs`. New clients
fall back to the V1 full-diff path when talking to older daemons. Old
clients are completely unaffected — all new schema fields are optional
and new message types are additive.

Per-file fingerprints (hash of hunks excluding tokens) drive React Query
cache invalidation so expanded files auto-refresh only when their actual
content changes.
This commit is contained in:
Mohamed Boudra
2026-04-14 08:59:36 +07:00
parent 8d1def4615
commit bf1d5d2ab2
19 changed files with 2071 additions and 28 deletions
+191 -13
View File
@@ -37,6 +37,12 @@ import {
type DiffLine,
type HighlightToken,
} from "@/hooks/use-checkout-diff-query";
import {
useCheckoutDiffMetadataQuery,
type DiffFileMetadata,
} from "@/hooks/use-checkout-diff-metadata-query";
import { useFileHunks } from "@/hooks/use-file-hunks";
import { useSessionForServer } from "@/hooks/use-session-directory";
import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query";
import { useCheckoutPrStatusQuery } from "@/hooks/use-checkout-pr-status-query";
import { useChangesPreferences } from "@/hooks/use-changes-preferences";
@@ -130,8 +136,16 @@ function HighlightedText({ tokens, wrapLines = false }: HighlightedTextProps) {
);
}
interface DiffFileHeaderFields {
path: string;
isNew: boolean;
isDeleted: boolean;
additions: number;
deletions: number;
}
interface DiffFileSectionProps {
file: ParsedDiffFile;
file: DiffFileHeaderFields;
isExpanded: boolean;
onToggle: (path: string) => void;
onHeaderHeightChange?: (path: string, height: number) => void;
@@ -617,6 +631,109 @@ function DiffFileBody({
);
}
interface LazyDiffFileBodyProps {
file: DiffFileMetadata;
serverId: string;
cwd: string;
mode: "uncommitted" | "base";
baseRef?: string;
ignoreWhitespace?: boolean;
layout: "unified" | "split";
wrapLines: boolean;
onBodyHeightChange?: (path: string, height: number) => void;
testID?: string;
}
function LazyDiffFileBody({
file,
serverId,
cwd,
mode,
baseRef,
ignoreWhitespace,
layout,
wrapLines,
onBodyHeightChange,
testID,
}: LazyDiffFileBodyProps) {
const {
file: resolvedFile,
isLoading,
isError,
error,
} = useFileHunks({
serverId,
cwd,
mode,
baseRef,
ignoreWhitespace,
file,
enabled: true,
});
if (file.status === "binary" || file.status === "too_large") {
return (
<View
style={[styles.fileSectionBodyContainer, styles.fileSectionBorder]}
onLayout={(event) => {
onBodyHeightChange?.(file.path, event.nativeEvent.layout.height);
}}
testID={testID}
>
<View style={styles.statusMessageContainer}>
<Text style={styles.statusMessageText}>
{file.status === "binary" ? "Binary file" : "Diff too large to display"}
</Text>
</View>
</View>
);
}
if (isLoading) {
return (
<View
style={[styles.fileSectionBodyContainer, styles.fileSectionBorder]}
onLayout={(event) => {
onBodyHeightChange?.(file.path, event.nativeEvent.layout.height);
}}
testID={testID}
>
<View style={styles.statusMessageContainer}>
<ActivityIndicator size="small" />
</View>
</View>
);
}
if (isError || !resolvedFile) {
return (
<View
style={[styles.fileSectionBodyContainer, styles.fileSectionBorder]}
onLayout={(event) => {
onBodyHeightChange?.(file.path, event.nativeEvent.layout.height);
}}
testID={testID}
>
<View style={styles.statusMessageContainer}>
<Text style={styles.statusMessageText}>
{error instanceof Error ? error.message : "Failed to load diff"}
</Text>
</View>
</View>
);
}
return (
<DiffFileBody
file={resolvedFile}
layout={layout}
wrapLines={wrapLines}
onBodyHeightChange={onBodyHeightChange}
testID={testID}
/>
);
}
interface GitDiffPaneProps {
serverId: string;
workspaceId?: string | null;
@@ -625,8 +742,9 @@ interface GitDiffPaneProps {
}
type DiffFlatItem =
| { type: "header"; file: ParsedDiffFile; fileIndex: number; isExpanded: boolean }
| { type: "body"; file: ParsedDiffFile; fileIndex: number };
| { type: "header"; file: DiffFileHeaderFields; fileIndex: number; isExpanded: boolean }
| { type: "body"; file: ParsedDiffFile; fileIndex: number }
| { type: "lazy-body"; file: DiffFileMetadata; fileIndex: number };
export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDiffPaneProps) {
const { theme } = useUnistyles();
@@ -643,6 +761,13 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
const wrapLines = changesPreferences.wrapLines;
const effectiveLayout = canUseSplitLayout ? changesPreferences.layout : "unified";
// Feature detection: use V2 lazy diff when daemon supports it
const useLazyDiff =
useSessionForServer(
serverId,
(session) => session?.serverInfo?.features?.lazyDiffRpcs === true,
) ?? false;
const handleToggleWrapLines = useCallback(() => {
void updateChangesPreferences({ wrapLines: !wrapLines });
}, [updateChangesPreferences, wrapLines]);
@@ -679,22 +804,50 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
const autoDiffMode = hasUncommittedChanges ? "uncommitted" : "base";
const diffMode = diffModeOverride ?? autoDiffMode;
// V1: full diff (hunks included) — used when daemon doesn't support lazy diff
const {
files,
payloadError: diffPayloadError,
isLoading: isDiffLoading,
isFetching: isDiffFetching,
isError: isDiffError,
error: diffError,
refresh: refreshDiff,
files: v1Files,
payloadError: v1DiffPayloadError,
isLoading: isV1DiffLoading,
isFetching: isV1DiffFetching,
isError: isV1DiffError,
error: v1DiffError,
refresh: refreshV1Diff,
} = useCheckoutDiffQuery({
serverId,
cwd,
mode: diffMode,
baseRef,
ignoreWhitespace: changesPreferences.hideWhitespace,
enabled: isGit,
enabled: isGit && !useLazyDiff,
});
// V2: metadata-only (hunks fetched on demand per file)
const {
files: v2Files,
payloadError: v2DiffPayloadError,
isLoading: isV2DiffLoading,
isFetching: isV2DiffFetching,
isError: isV2DiffError,
error: v2DiffError,
refresh: refreshV2Diff,
} = useCheckoutDiffMetadataQuery({
serverId,
cwd,
mode: diffMode,
baseRef,
ignoreWhitespace: changesPreferences.hideWhitespace,
enabled: isGit && useLazyDiff,
});
// Unified interface for both V1 and V2
const files: DiffFileHeaderFields[] = useLazyDiff ? v2Files : v1Files;
const diffPayloadError = useLazyDiff ? v2DiffPayloadError : v1DiffPayloadError;
const isDiffLoading = useLazyDiff ? isV2DiffLoading : isV1DiffLoading;
const isDiffFetching = useLazyDiff ? isV2DiffFetching : isV1DiffFetching;
const isDiffError = useLazyDiff ? isV2DiffError : isV1DiffError;
const diffError = useLazyDiff ? v2DiffError : v1DiffError;
const refreshDiff = useLazyDiff ? refreshV2Diff : refreshV1Diff;
const {
status: prStatus,
githubFeaturesEnabled,
@@ -788,11 +941,15 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
stickyIndices.push(items.length - 1);
}
if (isExpanded) {
items.push({ type: "body", file, fileIndex: i });
if (useLazyDiff) {
items.push({ type: "lazy-body", file: file as DiffFileMetadata, fileIndex: i });
} else {
items.push({ type: "body", file: file as ParsedDiffFile, fileIndex: i });
}
}
}
return { flatItems: items, stickyHeaderIndices: stickyIndices };
}, [expandedPaths, files]);
}, [expandedPaths, files, useLazyDiff]);
const handleHeaderHeightChange = useCallback((path: string, height: number) => {
if (!Number.isFinite(height) || height <= 0) {
@@ -1067,6 +1224,22 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
/>
);
}
if (item.type === "lazy-body") {
return (
<LazyDiffFileBody
file={item.file}
serverId={serverId}
cwd={cwd}
mode={diffMode}
baseRef={baseRef}
ignoreWhitespace={changesPreferences.hideWhitespace}
layout={effectiveLayout}
wrapLines={wrapLines}
onBodyHeightChange={handleBodyHeightChange}
testID={`diff-file-${item.fileIndex}-body`}
/>
);
}
return (
<DiffFileBody
file={item.file}
@@ -1078,10 +1251,15 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
);
},
[
baseRef,
changesPreferences.hideWhitespace,
cwd,
diffMode,
effectiveLayout,
handleBodyHeightChange,
handleHeaderHeightChange,
handleToggleExpanded,
serverId,
wrapLines,
],
);
@@ -1,6 +1,6 @@
import type { SubscribeCheckoutDiffResponse } from "@server/shared/messages";
type ParsedDiffFile = SubscribeCheckoutDiffResponse["payload"]["files"][number];
interface DiffFileWithPath {
path: string;
}
export function compareCheckoutDiffPaths(left: string, right: string): number {
if (left === right) {
@@ -9,7 +9,7 @@ export function compareCheckoutDiffPaths(left: string, right: string): number {
return left < right ? -1 : 1;
}
export function orderCheckoutDiffFiles(files: ParsedDiffFile[]): ParsedDiffFile[] {
export function orderCheckoutDiffFiles<T extends DiffFileWithPath>(files: T[]): T[] {
if (files.length < 2) {
return files;
}
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import type { DiffFileMetadata } from "./use-checkout-diff-metadata-query";
import { orderCheckoutDiffFiles } from "./checkout-diff-order";
function createMetadata(path: string, fingerprint = "fp1"): DiffFileMetadata {
return {
path,
isNew: false,
isDeleted: false,
additions: 1,
deletions: 0,
fingerprint,
};
}
describe("useCheckoutDiffMetadataQuery — cache replacement behavior", () => {
it("orderCheckoutDiffFiles works with DiffFileMetadata arrays", () => {
const files: DiffFileMetadata[] = [
createMetadata("zeta.ts"),
createMetadata("alpha.ts"),
createMetadata("beta.ts"),
];
const ordered = orderCheckoutDiffFiles(files);
expect(ordered.map((f) => f.path)).toEqual(["alpha.ts", "beta.ts", "zeta.ts"]);
});
it("replaces full list on snapshot — does not merge with previous", () => {
const initial: DiffFileMetadata[] = [
createMetadata("a.ts", "fp1"),
createMetadata("b.ts", "fp2"),
createMetadata("c.ts", "fp3"),
];
// Simulate a new snapshot that removes b.ts and changes c.ts fingerprint
const updated: DiffFileMetadata[] = [
createMetadata("a.ts", "fp1"),
createMetadata("c.ts", "fp4"),
];
// Full replacement: the result is exactly the updated list, not a merge
const result = orderCheckoutDiffFiles(updated);
expect(result).toHaveLength(2);
expect(result.map((f) => f.path)).toEqual(["a.ts", "c.ts"]);
expect(result.find((f) => f.path === "c.ts")?.fingerprint).toBe("fp4");
// b.ts is gone — no remnant from initial
expect(result.find((f) => f.path === "b.ts")).toBeUndefined();
// Initial is unchanged (no mutation)
expect(initial).toHaveLength(3);
});
it("preserves fingerprint and status on metadata items", () => {
const file: DiffFileMetadata = {
path: "large.bin",
isNew: false,
isDeleted: false,
additions: 0,
deletions: 0,
status: "binary",
fingerprint: "abc123",
};
const ordered = orderCheckoutDiffFiles([file]);
expect(ordered[0].fingerprint).toBe("abc123");
expect(ordered[0].status).toBe("binary");
});
});
@@ -0,0 +1,214 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useId, useMemo } from "react";
import { useIsCompactFormFactor } from "@/constants/layout";
import { usePanelStore } from "@/stores/panel-store";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import type { CheckoutDiffSnapshot } from "@server/shared/messages";
import { orderCheckoutDiffFiles } from "./checkout-diff-order";
const CHECKOUT_DIFF_METADATA_STALE_TIME = 30_000;
export type DiffFileMetadata = CheckoutDiffSnapshot["payload"]["files"][number];
type CheckoutDiffMetadataPayload = Omit<CheckoutDiffSnapshot["payload"], "subscriptionId">;
function checkoutDiffMetadataQueryKey(
serverId: string,
cwd: string,
mode: "uncommitted" | "base",
baseRef?: string,
ignoreWhitespace?: boolean,
) {
return [
"checkoutDiffMetadata",
serverId,
cwd,
mode,
baseRef ?? "",
ignoreWhitespace === true,
] as const;
}
interface UseCheckoutDiffMetadataQueryOptions {
serverId: string;
cwd: string;
mode: "uncommitted" | "base";
baseRef?: string;
ignoreWhitespace?: boolean;
enabled?: boolean;
}
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 useCheckoutDiffMetadataQuery({
serverId,
cwd,
mode,
baseRef,
ignoreWhitespace,
enabled = true,
}: UseCheckoutDiffMetadataQueryOptions) {
const queryClient = useQueryClient();
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const isMobile = useIsCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView);
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, ignoreWhitespace }),
[mode, baseRef, ignoreWhitespace],
);
const compareMode = normalizedCompare.mode;
const compareBaseRef = normalizedCompare.baseRef;
const compareIgnoreWhitespace = normalizedCompare.ignoreWhitespace;
const queryKey = useMemo(
() => checkoutDiffMetadataQueryKey(serverId, cwd, mode, baseRef, compareIgnoreWhitespace),
[serverId, cwd, mode, baseRef, compareIgnoreWhitespace],
);
const query = useQuery({
queryKey,
queryFn: async () => {
if (!client) {
throw new Error("Daemon client not available");
}
const payload = await client.getCheckoutDiffMetadata(cwd, {
mode: compareMode,
baseRef: compareBaseRef,
ignoreWhitespace: compareIgnoreWhitespace,
});
return {
cwd: payload.cwd,
files: orderCheckoutDiffFiles(payload.files),
error: payload.error,
requestId: payload.requestId,
};
},
enabled: !!client && isConnected && !!cwd && enabled,
staleTime: CHECKOUT_DIFF_METADATA_STALE_TIME,
});
useEffect(() => {
if (!client || !isConnected || !cwd || !enabled) {
return;
}
if (!isOpen || explorerTab !== "changes") {
return;
}
const subscriptionId = [
"checkoutDiffMetadata",
hookInstanceId,
serverId,
cwd,
compareMode,
compareBaseRef ?? "",
compareIgnoreWhitespace ? "ignore-ws" : "keep-ws",
].join(":");
let cancelled = false;
const unsubscribeSnapshot = client.on("checkout/diff_snapshot", (message) => {
if (message.type !== "checkout/diff_snapshot") {
return;
}
if (message.payload.subscriptionId !== subscriptionId) {
return;
}
queryClient.setQueryData<CheckoutDiffMetadataPayload>(queryKey, {
cwd: message.payload.cwd,
files: orderCheckoutDiffFiles(message.payload.files),
error: message.payload.error,
requestId: message.payload.requestId,
});
});
void client
.subscribeCheckoutDiffLazy(
cwd,
{
mode: compareMode,
baseRef: compareBaseRef,
ignoreWhitespace: compareIgnoreWhitespace,
},
{ subscriptionId },
)
.then((payload) => {
if (cancelled) {
return;
}
queryClient.setQueryData<CheckoutDiffMetadataPayload>(queryKey, {
cwd: payload.cwd,
files: orderCheckoutDiffFiles(payload.files),
error: payload.error,
requestId: payload.requestId,
});
})
.catch((error) => {
if (cancelled) {
return;
}
console.error("[useCheckoutDiffMetadataQuery] subscribeCheckoutDiffLazy failed", {
serverId,
cwd,
error,
});
});
return () => {
cancelled = true;
unsubscribeSnapshot();
try {
client.unsubscribeCheckoutDiffLazy(subscriptionId);
} catch {
// Ignore disconnect race during effect cleanup.
}
};
}, [
client,
isConnected,
cwd,
enabled,
isOpen,
explorerTab,
hookInstanceId,
serverId,
compareMode,
compareBaseRef,
compareIgnoreWhitespace,
queryKey,
queryClient,
]);
const refresh = useCallback(() => {
return query.refetch();
}, [query]);
const payload = query.data ?? null;
const payloadError = payload?.error ?? null;
return {
files: payload?.files ?? [],
payloadError,
isLoading: query.isLoading,
isFetching: query.isFetching,
isError: query.isError || Boolean(payloadError),
error: query.error,
refresh,
};
}
@@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import type { DiffFileMetadata } from "./use-checkout-diff-metadata-query";
/**
* Tests for use-file-hunks hook logic.
*
* The hook itself is a thin React Query wrapper, so we test the key properties:
* 1. Query key includes fingerprint — fingerprint change = automatic refetch
* 2. Metadata fields are passed through to the merged result
*/
function fileHunksQueryKey(
serverId: string,
cwd: string,
mode: "uncommitted" | "base",
baseRef: string | undefined,
ignoreWhitespace: boolean | undefined,
path: string,
fingerprint: string,
) {
return [
"checkoutFileHunks",
serverId,
cwd,
mode,
baseRef ?? "",
ignoreWhitespace === true,
path,
fingerprint,
] as const;
}
describe("use-file-hunks query key", () => {
it("includes fingerprint in query key", () => {
const key = fileHunksQueryKey("s1", "/repo", "base", "main", false, "foo.ts", "fp-abc");
expect(key).toContain("fp-abc");
expect(key).toContain("foo.ts");
});
it("different fingerprints produce different query keys", () => {
const key1 = fileHunksQueryKey("s1", "/repo", "base", "main", false, "foo.ts", "fp-1");
const key2 = fileHunksQueryKey("s1", "/repo", "base", "main", false, "foo.ts", "fp-2");
expect(key1).not.toEqual(key2);
// Only the fingerprint element differs
expect(key1.slice(0, -1)).toEqual(key2.slice(0, -1));
});
it("same fingerprint + same params produces identical key", () => {
const key1 = fileHunksQueryKey("s1", "/repo", "uncommitted", undefined, true, "bar.ts", "fp-x");
const key2 = fileHunksQueryKey("s1", "/repo", "uncommitted", undefined, true, "bar.ts", "fp-x");
expect(key1).toEqual(key2);
});
it("normalizes missing baseRef to empty string", () => {
const key = fileHunksQueryKey("s1", "/repo", "uncommitted", undefined, false, "a.ts", "fp");
expect(key[4]).toBe("");
});
it("normalizes ignoreWhitespace to boolean", () => {
const key = fileHunksQueryKey("s1", "/repo", "base", "main", undefined, "a.ts", "fp");
expect(key[5]).toBe(false);
});
});
describe("use-file-hunks metadata merge", () => {
it("metadata fields carry through to merged ParsedDiffFile shape", () => {
const meta: DiffFileMetadata = {
path: "src/index.ts",
isNew: true,
isDeleted: false,
additions: 10,
deletions: 2,
status: "ok",
fingerprint: "abc",
};
// Simulates merging metadata with fetched hunks
const merged = {
path: meta.path,
isNew: meta.isNew,
isDeleted: meta.isDeleted,
additions: meta.additions,
deletions: meta.deletions,
status: meta.status,
hunks: [],
};
expect(merged.path).toBe("src/index.ts");
expect(merged.isNew).toBe(true);
expect(merged.additions).toBe(10);
expect(merged.hunks).toEqual([]);
});
});
+99
View File
@@ -0,0 +1,99 @@
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import type { ParsedDiffFile } from "@/hooks/use-checkout-diff-query";
import type { DiffFileMetadata } from "@/hooks/use-checkout-diff-metadata-query";
function fileHunksQueryKey(
serverId: string,
cwd: string,
mode: "uncommitted" | "base",
baseRef: string | undefined,
ignoreWhitespace: boolean | undefined,
path: string,
fingerprint: string,
) {
return [
"checkoutFileHunks",
serverId,
cwd,
mode,
baseRef ?? "",
ignoreWhitespace === true,
path,
fingerprint,
] as const;
}
interface UseFileHunksOptions {
serverId: string;
cwd: string;
mode: "uncommitted" | "base";
baseRef?: string;
ignoreWhitespace?: boolean;
file: DiffFileMetadata;
enabled?: boolean;
}
export function useFileHunks({
serverId,
cwd,
mode,
baseRef,
ignoreWhitespace,
file,
enabled = true,
}: UseFileHunksOptions) {
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const queryKey = useMemo(
() =>
fileHunksQueryKey(
serverId,
cwd,
mode,
baseRef,
ignoreWhitespace,
file.path,
file.fingerprint,
),
[serverId, cwd, mode, baseRef, ignoreWhitespace, file.path, file.fingerprint],
);
const query = useQuery({
queryKey,
queryFn: async (): Promise<ParsedDiffFile> => {
if (!client) {
throw new Error("Daemon client not available");
}
const payload = await client.getCheckoutFileHunks(
cwd,
{ mode, baseRef, ignoreWhitespace },
file.path,
);
if (payload.error) {
throw new Error(payload.error.message);
}
return {
path: file.path,
isNew: file.isNew,
isDeleted: file.isDeleted,
additions: file.additions,
deletions: file.deletions,
status: file.status,
hunks: payload.hunks,
};
},
enabled: !!client && isConnected && enabled,
staleTime: 60_000,
});
return {
file: query.data ?? null,
isLoading: query.isLoading,
isFetching: query.isFetching,
isError: query.isError,
error: query.error,
};
}
@@ -5,6 +5,7 @@ import {
getServerCapabilities,
getVoiceReadinessState,
resolveVoiceUnavailableMessage,
supportsLazyDiffRpcs,
} from "./server-info-capabilities";
function buildServerInfo(capabilities?: ServerCapabilities): DaemonServerInfo {
@@ -117,3 +118,42 @@ describe("server-info-capabilities", () => {
).toBeNull();
});
});
describe("supportsLazyDiffRpcs", () => {
it("returns false when serverInfo is null", () => {
expect(supportsLazyDiffRpcs({ serverInfo: null })).toBe(false);
});
it("returns false when serverInfo is undefined", () => {
expect(supportsLazyDiffRpcs({ serverInfo: undefined })).toBe(false);
});
it("returns false when features is absent", () => {
const serverInfo = buildServerInfo();
expect(supportsLazyDiffRpcs({ serverInfo })).toBe(false);
});
it("returns false when lazyDiffRpcs is not set", () => {
const serverInfo: DaemonServerInfo = {
...buildServerInfo(),
features: {},
};
expect(supportsLazyDiffRpcs({ serverInfo })).toBe(false);
});
it("returns true when lazyDiffRpcs is true", () => {
const serverInfo: DaemonServerInfo = {
...buildServerInfo(),
features: { lazyDiffRpcs: true },
};
expect(supportsLazyDiffRpcs({ serverInfo })).toBe(true);
});
it("returns false when lazyDiffRpcs is false", () => {
const serverInfo: DaemonServerInfo = {
...buildServerInfo(),
features: { lazyDiffRpcs: false },
};
expect(supportsLazyDiffRpcs({ serverInfo })).toBe(false);
});
});
@@ -1,6 +1,12 @@
import type { ServerCapabilityState } from "@server/shared/messages";
import type { DaemonServerInfo } from "@/stores/session-store";
export function supportsLazyDiffRpcs(params: {
serverInfo: DaemonServerInfo | null | undefined;
}): boolean {
return params.serverInfo?.features?.lazyDiffRpcs === true;
}
export type VoiceReadinessMode = "dictation" | "voice";
export function getServerCapabilities(params: {