refactor: better resume two-way sync in case of MCP/API updates

This commit is contained in:
Amruth Pillai
2026-05-26 12:05:38 +02:00
parent 19b412d84d
commit dd1e37e579
11 changed files with 548 additions and 93 deletions
+2
View File
@@ -57,6 +57,7 @@ import { getOrpcErrorMessage } from "@/libs/error-message";
import { client, orpc, streamClient } from "@/libs/orpc/client";
import { AgentThreadSidebar } from "./-components/thread-sidebar";
import { attachmentIdsFromTransportBody, buildAgentChatSubmission } from "./-helpers/chat-attachments";
import { useAgentResumeUpdateSubscription } from "./-hooks/use-agent-resume-updates";
type AgentThreadDetail = RouterOutput["agent"]["threads"]["get"];
type AgentAction = AgentThreadDetail["actions"][number];
@@ -1218,6 +1219,7 @@ function RouteComponent() {
const [isThreadsCollapsed, setIsThreadsCollapsed] = useState(false);
const [isResumeCollapsed, setIsResumeCollapsed] = useState(false);
const { data, isLoading, error } = useQuery(orpc.agent.threads.get.queryOptions({ input: { id: threadId } }));
useAgentResumeUpdateSubscription({ resumeId: data?.resume?.id, threadId });
const toggleThreadsPanel = useCallback(() => {
const panel = threadsPanelRef.current;
@@ -0,0 +1,64 @@
// @vitest-environment happy-dom
import { renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useAgentResumeUpdateSubscription } from "./use-agent-resume-updates";
const subscriptionMock = vi.hoisted(() => vi.fn());
const queryClientMock = vi.hoisted(() => ({
invalidateQueries: vi.fn(),
}));
vi.mock("@tanstack/react-query", () => ({
useQueryClient: () => queryClientMock,
}));
vi.mock("@/features/resume/builder/draft", () => ({
useResumeUpdateSubscription: subscriptionMock,
}));
vi.mock("@/libs/orpc/client", () => ({
orpc: {
agent: {
threads: {
get: {
queryKey: ({ input }: { input: { id: string } }) => ["agent", "threads", "get", input.id],
},
list: {
queryKey: () => ["agent", "threads", "list"],
},
},
},
},
}));
describe("useAgentResumeUpdateSubscription", () => {
beforeEach(() => {
subscriptionMock.mockReset();
queryClientMock.invalidateQueries.mockClear();
});
it("invalidates the active thread and thread list when the working resume changes", async () => {
renderHook(() =>
useAgentResumeUpdateSubscription({
resumeId: "resume-1",
threadId: "thread-1",
}),
);
const options = subscriptionMock.mock.calls[0]?.[0] as
| { resumeId?: string; onUpdate: () => Promise<void> }
| undefined;
expect(options?.resumeId).toBe("resume-1");
await options?.onUpdate();
expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({
queryKey: ["agent", "threads", "list"],
});
expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({
queryKey: ["agent", "threads", "get", "thread-1"],
});
});
});
@@ -0,0 +1,28 @@
import { useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { useResumeUpdateSubscription } from "@/features/resume/builder/draft";
import { orpc } from "@/libs/orpc/client";
type UseAgentResumeUpdateSubscriptionInput = {
resumeId?: string;
threadId: string;
};
export function useAgentResumeUpdateSubscription({ resumeId, threadId }: UseAgentResumeUpdateSubscriptionInput) {
const queryClient = useQueryClient();
const onUpdate = useCallback(async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: orpc.agent.threads.list.queryKey() }),
queryClient.invalidateQueries({ queryKey: orpc.agent.threads.get.queryKey({ input: { id: threadId } }) }),
]);
}, [queryClient, threadId]);
useResumeUpdateSubscription({
resumeId,
onUpdate,
onError: useCallback((error: unknown) => {
console.warn("Agent resume update stream failed, reconnecting:", error);
}, []),
});
}
@@ -8,11 +8,11 @@ import { useEffect, useRef } from "react";
import { usePanelRef } from "react-resizable-panels";
import { ResizableGroup, ResizablePanel, ResizableSeparator } from "@reactive-resume/ui/components/resizable";
import {
useBuilderResumeUpdateSubscription,
useInitializeResumeStore,
useMergeResumeMetadata,
useResumeCleanup,
useResumeStore,
useResumeUpdateSubscription,
} from "@/features/resume/builder/draft";
import { useIsMobile } from "@/hooks/use-mobile";
import { orpc } from "@/libs/orpc/client";
@@ -62,7 +62,7 @@ function RouteComponent() {
const isInitialized = isReady && initializedResumeId === resumeId;
useResumeCleanup();
useResumeUpdateSubscription();
useBuilderResumeUpdateSubscription();
useEffect(() => {
if (isInitialized) return;