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

View File

@@ -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(

View File

@@ -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<ComposerAttachment, { kind: "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) {
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 <div data-testid="workspace-binding-count">{selectedAttachments.length}</div>;
}
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(<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 () => {
const review = reviewComposerAttachment("Please simplify this.");
renderComposer({