Fix composer workspace attachment update loop

This commit is contained in:
Mohamed Boudra
2026-05-01 19:51:58 +07:00
parent 68d32eb183
commit 1fdfefc4de
2 changed files with 66 additions and 3 deletions
@@ -112,9 +112,12 @@ function useWorkspaceAttachmentBinding({
); );
useEffect(() => { useEffect(() => {
setSuppressedKeys((current) => setSuppressedKeys((current) => {
current.filter((suppressedKey) => workspaceAttachmentKeys.includes(suppressedKey)), const next = current.filter((suppressedKey) =>
); workspaceAttachmentKeys.includes(suppressedKey),
);
return next.length === current.length ? current : next;
});
}, [workspaceAttachmentKeys]); }, [workspaceAttachmentKeys]);
const buildOutgoingAttachments = useCallback( const buildOutgoingAttachments = useCallback(
@@ -9,6 +9,7 @@ import type {
ComposerAttachment, ComposerAttachment,
UserComposerAttachment, UserComposerAttachment,
} from "@/attachments/types"; } from "@/attachments/types";
import { composerWorkspaceAttachment } from "@/attachments/composer-workspace-attachments";
import type { AgentAttachment, GitHubSearchItem } from "@server/shared/messages"; import type { AgentAttachment, GitHubSearchItem } from "@server/shared/messages";
import { Composer } from "./composer"; import { Composer } from "./composer";
import { splitComposerAttachmentsForSubmit } from "./composer-attachments"; import { splitComposerAttachmentsForSubmit } from "./composer-attachments";
@@ -569,6 +570,7 @@ let root: Root | null = null;
let container: HTMLElement | null = null; let container: HTMLElement | null = null;
let queryClient: QueryClient | null = null; let queryClient: QueryClient | null = null;
let latestAttachments: ComposerAttachment[] = []; let latestAttachments: ComposerAttachment[] = [];
let workspaceBindingRenderCount = 0;
type ReviewComposerAttachment = Extract<ComposerAttachment, { kind: "review" }>; type ReviewComposerAttachment = Extract<ComposerAttachment, { kind: "review" }>;
type ReviewAttachment = Extract<AgentAttachment, { type: "review" }>; type ReviewAttachment = Extract<AgentAttachment, { type: "review" }>;
@@ -617,6 +619,25 @@ function reviewComposerAttachment(body: string): ReviewComposerAttachment {
}; };
} }
function cloneReviewComposerAttachment(
attachment: ReviewComposerAttachment,
): ReviewComposerAttachment {
return {
...attachment,
attachment: {
...attachment.attachment,
comments: attachment.attachment.comments.map((comment) => ({
...comment,
context: {
...comment.context,
targetLine: { ...comment.context.targetLine },
lines: comment.context.lines.map((line) => ({ ...line })),
},
})),
},
};
}
function seedReviewDraft(key: string) { function seedReviewDraft(key: string) {
addReviewDraftComment({ addReviewDraftComment({
key, key,
@@ -670,6 +691,7 @@ beforeEach(() => {
agentDirectoryStatusMock.mockReset(); agentDirectoryStatusMock.mockReset();
agentDirectoryStatusMock.mockReturnValue("ready"); agentDirectoryStatusMock.mockReturnValue("ready");
appSendBehavior.current = "interrupt"; appSendBehavior.current = "interrupt";
workspaceBindingRenderCount = 0;
mockSessionState.sessions.server.serverInfo = { mockSessionState.sessions.server.serverInfo = {
serverId: "server", serverId: "server",
hostname: "test", hostname: "test",
@@ -785,6 +807,20 @@ function renderComposer(
}); });
} }
function WorkspaceAttachmentBindingHarness({
workspaceAttachment,
}: {
workspaceAttachment: ReviewComposerAttachment;
}) {
workspaceBindingRenderCount += 1;
const { selectedAttachments } = composerWorkspaceAttachment.useBinding({
normalAttachments: [],
workspaceAttachments: [workspaceAttachment],
});
return <div data-testid="workspace-binding-count">{selectedAttachments.length}</div>;
}
function click(element: Element) { function click(element: Element) {
act(() => { act(() => {
element.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); element.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
@@ -1007,6 +1043,30 @@ describe("Composer attachments", () => {
}); });
}); });
it("does not enqueue redundant binding renders for equivalent workspace attachments", async () => {
const review = reviewComposerAttachment("Stable workspace review.");
act(() => {
root?.render(<WorkspaceAttachmentBindingHarness workspaceAttachment={review} />);
});
await flushAsyncWork();
expect(workspaceBindingRenderCount).toBe(1);
expect(queryByTestId("workspace-binding-count")?.textContent).toBe("1");
act(() => {
root?.render(
<WorkspaceAttachmentBindingHarness
workspaceAttachment={cloneReviewComposerAttachment(review)}
/>,
);
});
await flushAsyncWork();
expect(workspaceBindingRenderCount).toBe(2);
expect(queryByTestId("workspace-binding-count")?.textContent).toBe("1");
});
it("renders and submits a workspace review attachment pill", async () => { it("renders and submits a workspace review attachment pill", async () => {
const review = reviewComposerAttachment("Please simplify this."); const review = reviewComposerAttachment("Please simplify this.");
renderComposer({ renderComposer({