diff --git a/packages/app/src/attachments/composer-workspace-attachments.tsx b/packages/app/src/attachments/composer-workspace-attachments.tsx index 0dcad7d3a..bc4a7fa7c 100644 --- a/packages/app/src/attachments/composer-workspace-attachments.tsx +++ b/packages/app/src/attachments/composer-workspace-attachments.tsx @@ -112,9 +112,12 @@ function useWorkspaceAttachmentBinding({ ); useEffect(() => { - setSuppressedKeys((current) => - current.filter((suppressedKey) => workspaceAttachmentKeys.includes(suppressedKey)), - ); + setSuppressedKeys((current) => { + const next = current.filter((suppressedKey) => + workspaceAttachmentKeys.includes(suppressedKey), + ); + return next.length === current.length ? current : next; + }); }, [workspaceAttachmentKeys]); const buildOutgoingAttachments = useCallback( diff --git a/packages/app/src/components/composer.test.tsx b/packages/app/src/components/composer.test.tsx index 150b6ad39..0aef81cf6 100644 --- a/packages/app/src/components/composer.test.tsx +++ b/packages/app/src/components/composer.test.tsx @@ -9,6 +9,7 @@ import type { ComposerAttachment, UserComposerAttachment, } from "@/attachments/types"; +import { composerWorkspaceAttachment } from "@/attachments/composer-workspace-attachments"; import type { AgentAttachment, GitHubSearchItem } from "@server/shared/messages"; import { Composer } from "./composer"; import { splitComposerAttachmentsForSubmit } from "./composer-attachments"; @@ -569,6 +570,7 @@ let root: Root | null = null; let container: HTMLElement | null = null; let queryClient: QueryClient | null = null; let latestAttachments: ComposerAttachment[] = []; +let workspaceBindingRenderCount = 0; type ReviewComposerAttachment = Extract; type ReviewAttachment = Extract; @@ -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) { addReviewDraftComment({ key, @@ -670,6 +691,7 @@ beforeEach(() => { agentDirectoryStatusMock.mockReset(); agentDirectoryStatusMock.mockReturnValue("ready"); appSendBehavior.current = "interrupt"; + workspaceBindingRenderCount = 0; mockSessionState.sessions.server.serverInfo = { serverId: "server", hostname: "test", @@ -785,6 +807,20 @@ function renderComposer( }); } +function WorkspaceAttachmentBindingHarness({ + workspaceAttachment, +}: { + workspaceAttachment: ReviewComposerAttachment; +}) { + workspaceBindingRenderCount += 1; + const { selectedAttachments } = composerWorkspaceAttachment.useBinding({ + normalAttachments: [], + workspaceAttachments: [workspaceAttachment], + }); + + return
{selectedAttachments.length}
; +} + function click(element: Element) { act(() => { 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(); + }); + await flushAsyncWork(); + + expect(workspaceBindingRenderCount).toBe(1); + expect(queryByTestId("workspace-binding-count")?.textContent).toBe("1"); + + act(() => { + root?.render( + , + ); + }); + await flushAsyncWork(); + + expect(workspaceBindingRenderCount).toBe(2); + expect(queryByTestId("workspace-binding-count")?.textContent).toBe("1"); + }); + it("renders and submits a workspace review attachment pill", async () => { const review = reviewComposerAttachment("Please simplify this."); renderComposer({