From b7ff4e68ebc94750ada363d44a92a3bf77b86142 Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 23 Jun 2026 17:52:00 +0800 Subject: [PATCH] fix: centralize canvas agent pending requests --- components/canvas/CanvasChatPanel.tsx | 62 ++++++-------- components/canvas/CanvasWorkspace.tsx | 30 +++---- components/canvas/nodes/ImageNode.tsx | 30 +++---- lib/canvas/agent-pending-requests.ts | 103 +++++++++++++++++++++++ tests/routes/canvas-chat-helpers.test.ts | 54 ++++++++++++ 5 files changed, 208 insertions(+), 71 deletions(-) create mode 100644 lib/canvas/agent-pending-requests.ts diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index d860ccfdf..7dcee33b1 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -136,6 +136,17 @@ import { mergePendingCanvasAgentTargetNodeIds, resolvePendingCanvasAgentTargetNode, } from "@/components/canvas/chat-panel/canvas-agent-targets"; +import { + CANVAS_AGENT_ANNOTATION_EDIT_EVENT, + CANVAS_AGENT_SLOT_FILL_EVENT, + consumeCanvasAgentAnnotationEditRequest, + consumeCanvasAgentSlotFillRequest, + getPendingCanvasAgentAnnotationEditRequest, + getPendingCanvasAgentSlotFillRequest, + type CanvasAgentPendingRequestWindow, + type PendingCanvasAgentAnnotationEditRequest, + type PendingCanvasAgentSlotFillRequest, +} from "@/lib/canvas/agent-pending-requests"; import { type DetailArchive, type DetailBriefForm, @@ -3594,19 +3605,7 @@ export function CanvasChatPanel({ useEffect(() => { if (!effectiveAgentEnabled || typeof window === "undefined") return; - type PendingCanvasAgentSlotFillRequest = { - nodeId?: string; - label?: string; - }; - type PendingCanvasAgentAnnotationEditRequest = { - nodeId?: string; - title?: string; - annotationCount?: number; - }; - const targetWindow = window as Window & { - __canvasAgentPendingSlotFillRequest?: PendingCanvasAgentSlotFillRequest; - __canvasAgentPendingAnnotationEditRequest?: PendingCanvasAgentAnnotationEditRequest; - }; + const targetWindow = window as CanvasAgentPendingRequestWindow; const focusComposerWithPrompt = (prompt: string) => { handleSelectGenerationMode("agent"); @@ -3630,11 +3629,8 @@ export function CanvasChatPanel({ }; const handleAgentSlotFillRequest = (event: Event) => { - const detail = (event as CustomEvent<{ nodeId?: string; label?: string }>).detail; - const pending = targetWindow.__canvasAgentPendingSlotFillRequest; - if (!pending?.nodeId || pending.nodeId === detail?.nodeId) { - delete targetWindow.__canvasAgentPendingSlotFillRequest; - } + const detail = (event as CustomEvent).detail; + consumeCanvasAgentSlotFillRequest(targetWindow, detail); const label = String(detail?.label || "这个槽位").trim(); const prompt = `在${label === "这个槽位" ? label : `「${label}」`}生成一张图`; @@ -3644,17 +3640,9 @@ export function CanvasChatPanel({ }; const handleAgentAnnotationEditRequest = (event: Event) => { - const detail = ( - event as CustomEvent<{ - nodeId?: string; - title?: string; - annotationCount?: number; - }> - ).detail; - const pending = targetWindow.__canvasAgentPendingAnnotationEditRequest; - if (!pending?.nodeId || pending.nodeId === detail?.nodeId) { - delete targetWindow.__canvasAgentPendingAnnotationEditRequest; - } + const detail = (event as CustomEvent) + .detail; + consumeCanvasAgentAnnotationEditRequest(targetWindow, detail); const nodeId = String(detail?.nodeId || "").trim(); if (nodeId) { pendingCanvasAgentAnnotationEditTargetRef.current = { @@ -3674,37 +3662,37 @@ export function CanvasChatPanel({ }; window.addEventListener( - "canvas-agent-slot-fill-request", + CANVAS_AGENT_SLOT_FILL_EVENT, handleAgentSlotFillRequest ); window.addEventListener( - "canvas-agent-annotation-edit-request", + CANVAS_AGENT_ANNOTATION_EDIT_EVENT, handleAgentAnnotationEditRequest ); - const pendingSlotFillRequest = targetWindow.__canvasAgentPendingSlotFillRequest; + const pendingSlotFillRequest = getPendingCanvasAgentSlotFillRequest(targetWindow); if (pendingSlotFillRequest) { handleAgentSlotFillRequest( - new CustomEvent("canvas-agent-slot-fill-request", { + new CustomEvent(CANVAS_AGENT_SLOT_FILL_EVENT, { detail: pendingSlotFillRequest, }) ); } const pendingAnnotationEditRequest = - targetWindow.__canvasAgentPendingAnnotationEditRequest; + getPendingCanvasAgentAnnotationEditRequest(targetWindow); if (pendingAnnotationEditRequest) { handleAgentAnnotationEditRequest( - new CustomEvent("canvas-agent-annotation-edit-request", { + new CustomEvent(CANVAS_AGENT_ANNOTATION_EDIT_EVENT, { detail: pendingAnnotationEditRequest, }) ); } return () => { window.removeEventListener( - "canvas-agent-slot-fill-request", + CANVAS_AGENT_SLOT_FILL_EVENT, handleAgentSlotFillRequest ); window.removeEventListener( - "canvas-agent-annotation-edit-request", + CANVAS_AGENT_ANNOTATION_EDIT_EVENT, handleAgentAnnotationEditRequest ); }; diff --git a/components/canvas/CanvasWorkspace.tsx b/components/canvas/CanvasWorkspace.tsx index fc46ce170..efd936e5d 100644 --- a/components/canvas/CanvasWorkspace.tsx +++ b/components/canvas/CanvasWorkspace.tsx @@ -60,6 +60,12 @@ import { LOCAL_UPLOAD_PENDING_METADATA_KEY, LOCAL_UPLOAD_PREVIEW_METADATA_KEY, } from "@/lib/canvas/local-upload-state"; +import { + dispatchCanvasAgentOpenChatPanelRequest, + dispatchCanvasAgentSlotFillRequest, + storeCanvasAgentSlotFillRequest, + type CanvasAgentPendingRequestWindow, +} from "@/lib/canvas/agent-pending-requests"; import { inferOriginalImageUrl } from "@/lib/image-url"; const CANVAS_NO_DRAG_SELECTOR = @@ -691,24 +697,14 @@ export function CanvasWorkspace({ nodeId: agentSlotContextMenu.nodeId, label: agentSlotContextMenu.label, }; - const targetWindow = window as Window & { - __canvasAgentPendingSlotFillRequest?: typeof detail; - }; + const targetWindow = window as CanvasAgentPendingRequestWindow; selectNode(agentSlotContextMenu.nodeId, false); - targetWindow.__canvasAgentPendingSlotFillRequest = detail; - targetWindow.dispatchEvent( - new CustomEvent("canvas-open-chat-panel-request", { - detail: { - reason: "agent-slot-fill", - nodeId: detail.nodeId, - }, - }) - ); - targetWindow.dispatchEvent( - new CustomEvent("canvas-agent-slot-fill-request", { - detail, - }) - ); + storeCanvasAgentSlotFillRequest(targetWindow, detail); + dispatchCanvasAgentOpenChatPanelRequest(targetWindow, { + reason: "agent-slot-fill", + nodeId: detail.nodeId, + }); + dispatchCanvasAgentSlotFillRequest(targetWindow, detail); setAgentSlotContextMenu(null); }, [agentSlotContextMenu, selectNode]); diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 1e5f955c3..4931f098a 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -125,6 +125,12 @@ import { syncCanvasLocalEditRegionAnnotationsMetadata, syncCanvasNodeNoteAnnotationMetadata, } from "@/lib/canvas/agent-annotation-metadata"; +import { + dispatchCanvasAgentAnnotationEditRequest, + dispatchCanvasAgentOpenChatPanelRequest, + storeCanvasAgentAnnotationEditRequest, + type CanvasAgentPendingRequestWindow, +} from "@/lib/canvas/agent-pending-requests"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -6685,25 +6691,15 @@ export function ImageNode({ title: displayTitle, annotationCount: canvasAgentAnnotations.length, }; - const targetWindow = window as Window & { - __canvasAgentPendingAnnotationEditRequest?: typeof detail; - }; + const targetWindow = window as CanvasAgentPendingRequestWindow; selectNode(node.id, false); - targetWindow.__canvasAgentPendingAnnotationEditRequest = detail; - targetWindow.dispatchEvent( - new CustomEvent("canvas-open-chat-panel-request", { - detail: { - reason: "agent-annotation-edit", - nodeId: detail.nodeId, - }, - }) - ); - targetWindow.dispatchEvent( - new CustomEvent("canvas-agent-annotation-edit-request", { - detail, - }) - ); + storeCanvasAgentAnnotationEditRequest(targetWindow, detail); + dispatchCanvasAgentOpenChatPanelRequest(targetWindow, { + reason: "agent-annotation-edit", + nodeId: detail.nodeId, + }); + dispatchCanvasAgentAnnotationEditRequest(targetWindow, detail); }, [ canvasAgentAnnotations.length, displayTitle, diff --git a/lib/canvas/agent-pending-requests.ts b/lib/canvas/agent-pending-requests.ts new file mode 100644 index 000000000..cf2fe9c47 --- /dev/null +++ b/lib/canvas/agent-pending-requests.ts @@ -0,0 +1,103 @@ +export type PendingCanvasAgentSlotFillRequest = { + nodeId?: string; + label?: string; +}; + +export type PendingCanvasAgentAnnotationEditRequest = { + nodeId?: string; + title?: string; + annotationCount?: number; +}; + +export type CanvasAgentPendingRequestWindow = Window & { + __canvasAgentPendingSlotFillRequest?: PendingCanvasAgentSlotFillRequest; + __canvasAgentPendingAnnotationEditRequest?: PendingCanvasAgentAnnotationEditRequest; +}; + +export const CANVAS_AGENT_OPEN_CHAT_PANEL_EVENT = "canvas-open-chat-panel-request"; +export const CANVAS_AGENT_SLOT_FILL_EVENT = "canvas-agent-slot-fill-request"; +export const CANVAS_AGENT_ANNOTATION_EDIT_EVENT = + "canvas-agent-annotation-edit-request"; + +function normalizeNodeId(value: unknown) { + return String(value || "").trim(); +} + +export function storeCanvasAgentSlotFillRequest( + targetWindow: CanvasAgentPendingRequestWindow, + request: PendingCanvasAgentSlotFillRequest +) { + targetWindow.__canvasAgentPendingSlotFillRequest = { ...request }; +} + +export function getPendingCanvasAgentSlotFillRequest( + targetWindow: CanvasAgentPendingRequestWindow +) { + return targetWindow.__canvasAgentPendingSlotFillRequest || null; +} + +export function consumeCanvasAgentSlotFillRequest( + targetWindow: CanvasAgentPendingRequestWindow, + detail?: PendingCanvasAgentSlotFillRequest | null +) { + const pending = getPendingCanvasAgentSlotFillRequest(targetWindow); + if (!pending) return null; + const pendingNodeId = normalizeNodeId(pending.nodeId); + const detailNodeId = normalizeNodeId(detail?.nodeId); + if (!pendingNodeId || pendingNodeId === detailNodeId) { + delete targetWindow.__canvasAgentPendingSlotFillRequest; + } + return pending; +} + +export function storeCanvasAgentAnnotationEditRequest( + targetWindow: CanvasAgentPendingRequestWindow, + request: PendingCanvasAgentAnnotationEditRequest +) { + targetWindow.__canvasAgentPendingAnnotationEditRequest = { ...request }; +} + +export function getPendingCanvasAgentAnnotationEditRequest( + targetWindow: CanvasAgentPendingRequestWindow +) { + return targetWindow.__canvasAgentPendingAnnotationEditRequest || null; +} + +export function consumeCanvasAgentAnnotationEditRequest( + targetWindow: CanvasAgentPendingRequestWindow, + detail?: PendingCanvasAgentAnnotationEditRequest | null +) { + const pending = getPendingCanvasAgentAnnotationEditRequest(targetWindow); + if (!pending) return null; + const pendingNodeId = normalizeNodeId(pending.nodeId); + const detailNodeId = normalizeNodeId(detail?.nodeId); + if (!pendingNodeId || pendingNodeId === detailNodeId) { + delete targetWindow.__canvasAgentPendingAnnotationEditRequest; + } + return pending; +} + +export function dispatchCanvasAgentOpenChatPanelRequest( + targetWindow: Window, + detail: { reason: "agent-slot-fill" | "agent-annotation-edit"; nodeId?: string } +) { + targetWindow.dispatchEvent( + new CustomEvent(CANVAS_AGENT_OPEN_CHAT_PANEL_EVENT, { detail }) + ); +} + +export function dispatchCanvasAgentSlotFillRequest( + targetWindow: Window, + detail: PendingCanvasAgentSlotFillRequest +) { + targetWindow.dispatchEvent(new CustomEvent(CANVAS_AGENT_SLOT_FILL_EVENT, { detail })); +} + +export function dispatchCanvasAgentAnnotationEditRequest( + targetWindow: Window, + detail: PendingCanvasAgentAnnotationEditRequest +) { + targetWindow.dispatchEvent( + new CustomEvent(CANVAS_AGENT_ANNOTATION_EDIT_EVENT, { detail }) + ); +} diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 1b647852a..55292f8a2 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -16,6 +16,15 @@ import { mergePendingCanvasAgentTargetNodeIds, resolvePendingCanvasAgentTargetNode, } from "@/components/canvas/chat-panel/canvas-agent-targets"; +import { + consumeCanvasAgentAnnotationEditRequest, + consumeCanvasAgentSlotFillRequest, + getPendingCanvasAgentAnnotationEditRequest, + getPendingCanvasAgentSlotFillRequest, + storeCanvasAgentAnnotationEditRequest, + storeCanvasAgentSlotFillRequest, + type CanvasAgentPendingRequestWindow, +} from "@/lib/canvas/agent-pending-requests"; import { writeCanvasMediaChunk, writeCanvasOperationChunk, @@ -2675,6 +2684,51 @@ test.describe("canvas chat pure helpers", () => { ]); }); + test("keeps pending canvas agent UI requests until the matching panel event consumes them", () => { + const targetWindow = {} as CanvasAgentPendingRequestWindow; + const slotRequest = { + nodeId: "slot-1", + label: "主图槽", + }; + const annotationRequest = { + nodeId: "image-annotated", + title: "白底主图", + annotationCount: 2, + }; + + storeCanvasAgentSlotFillRequest(targetWindow, slotRequest); + storeCanvasAgentAnnotationEditRequest(targetWindow, annotationRequest); + + expect(getPendingCanvasAgentSlotFillRequest(targetWindow)).toEqual(slotRequest); + expect(getPendingCanvasAgentAnnotationEditRequest(targetWindow)).toEqual( + annotationRequest + ); + + expect( + consumeCanvasAgentSlotFillRequest(targetWindow, { nodeId: "other-slot" }) + ).toEqual(slotRequest); + expect( + consumeCanvasAgentAnnotationEditRequest(targetWindow, { + nodeId: "other-image", + }) + ).toEqual(annotationRequest); + expect(getPendingCanvasAgentSlotFillRequest(targetWindow)).toEqual(slotRequest); + expect(getPendingCanvasAgentAnnotationEditRequest(targetWindow)).toEqual( + annotationRequest + ); + + expect( + consumeCanvasAgentSlotFillRequest(targetWindow, { nodeId: "slot-1" }) + ).toEqual(slotRequest); + expect( + consumeCanvasAgentAnnotationEditRequest(targetWindow, { + nodeId: "image-annotated", + }) + ).toEqual(annotationRequest); + expect(getPendingCanvasAgentSlotFillRequest(targetWindow)).toBeNull(); + expect(getPendingCanvasAgentAnnotationEditRequest(targetWindow)).toBeNull(); + }); + test("keeps pending annotation edit targets eligible for the next canvas context", () => { const nodes = [ {