fix: retry canvas agent view after refresh

This commit is contained in:
134 2026-06-22 22:42:13 +08:00
parent 41acb006cf
commit dfb8971bcb
3 changed files with 65 additions and 6 deletions

View File

@ -129,6 +129,7 @@ import {
getCanvasMediaPayloadHandledItemIds,
getCanvasOperationHandledMediaItemIds,
type ImageGenerationStatusStreamPayload,
shouldRetryCanvasViewOperationAfterRefresh,
shouldSkipCanvasMediaPayloadForHandledItems,
} from "@/components/canvas/chat-panel/canvas-agent-stream";
import {
@ -1249,6 +1250,8 @@ export function CanvasChatPanel({
const autoContextRef = useRef<string | null>(null);
const contextPayloadRef = useRef<any>(null);
const handledCanvasMediaItemIdsRef = useRef<Set<string>>(new Set());
const pendingCanvasViewOperationRef =
useRef<CanvasViewOperationStreamPayload | null>(null);
const lastWorkflowAutofillSignatureRef = useRef<string | null>(null);
const lastWorkflowAutofillUrlsRef = useRef<string[]>([]);
const pendingLaunchRef = useRef<CanvasLaunchPayload | null>(null);
@ -1826,10 +1829,10 @@ export function CanvasChatPanel({
const applyCanvasViewOperation = useCallback(
(payload: CanvasViewOperationStreamPayload) => {
if (!payload || typeof payload !== "object") return;
if (!payload || typeof payload !== "object") return false;
const targetProjectId = String(payload.projectId || "").trim();
if (targetProjectId && targetProjectId !== canvasId) return;
if (typeof window === "undefined") return;
if (targetProjectId && targetProjectId !== canvasId) return false;
if (typeof window === "undefined") return false;
const store = useCanvasStore.getState();
const resolved = resolveCanvasViewOperation({
@ -1842,7 +1845,7 @@ export function CanvasChatPanel({
height: window.innerHeight,
},
});
if (!resolved) return;
if (!resolved) return false;
window.requestAnimationFrame(() => {
setTransientViewport(resolved.viewport);
@ -1850,6 +1853,7 @@ export function CanvasChatPanel({
if (resolved.targetNodeIds.length > 0) {
store.selectNodes(resolved.targetNodeIds);
}
return true;
},
[canvasId, setTransientViewport]
);
@ -4062,6 +4066,16 @@ export function CanvasChatPanel({
focusCanvasOnNode(focusNode);
}
}
const pendingViewOperation = pendingCanvasViewOperationRef.current;
if (pendingViewOperation) {
const pendingProjectId = String(pendingViewOperation.projectId || "").trim();
if (!pendingProjectId || pendingProjectId === targetProjectId) {
const applied = applyCanvasViewOperation(pendingViewOperation);
if (applied) {
pendingCanvasViewOperationRef.current = null;
}
}
}
return true;
} catch (error) {
@ -4071,7 +4085,7 @@ export function CanvasChatPanel({
return false;
}
},
[canvasId, focusCanvasOnNode, mutate]
[applyCanvasViewOperation, canvasId, focusCanvasOnNode, mutate]
);
const {
@ -4242,7 +4256,10 @@ export function CanvasChatPanel({
try {
const payload: CanvasViewOperationStreamPayload =
typeof dataPart.data === "string" ? JSON.parse(dataPart.data) : dataPart.data;
applyCanvasViewOperation(payload);
const applied = applyCanvasViewOperation(payload);
if (!applied && shouldRetryCanvasViewOperationAfterRefresh(payload)) {
pendingCanvasViewOperationRef.current = payload;
}
} catch (e) {
console.error("[CanvasChat] Failed to parse data-canvasViewOperation:", e);
}

View File

@ -64,6 +64,22 @@ export function shouldSkipCanvasMediaPayloadForHandledItems(params: {
);
}
export function shouldRetryCanvasViewOperationAfterRefresh(
payload: CanvasViewOperationStreamPayload
) {
const operation = String(payload?.operation || "").trim();
if (!operation) return false;
if (normalizeCanvasStreamIdList(payload.nodeIds).length > 0) return true;
if (String(payload.anchorNodeId || "").trim()) return true;
return (
operation === "focus_node" ||
operation === "focus_nodes" ||
operation === "focus_selection" ||
operation === "focus_latest_result" ||
operation === "fit_all"
);
}
export type ImageGenerationStatusStreamPayload = {
phase?: "queued" | "processing" | "finalizing" | "failed" | "completed";
providerType?: string;

View File

@ -9,6 +9,7 @@ import {
import {
getCanvasMediaPayloadHandledItemIds,
getCanvasOperationHandledMediaItemIds,
shouldRetryCanvasViewOperationAfterRefresh,
shouldSkipCanvasMediaPayloadForHandledItems,
} from "@/components/canvas/chat-panel/canvas-agent-stream";
import {
@ -1217,6 +1218,31 @@ test.describe("canvas chat pure helpers", () => {
expect(useCanvasStore.getState().isAgentOperationRefreshActive()).toBe(false);
});
test("queues target-based canvas view operations for post-refresh retry", () => {
expect(
shouldRetryCanvasViewOperationAfterRefresh({
version: 1,
viewOperationId: "view-result",
operation: "focus_latest_result",
nodeIds: ["new-image-1"],
})
).toBe(true);
expect(
shouldRetryCanvasViewOperationAfterRefresh({
version: 1,
viewOperationId: "view-fit-all",
operation: "fit_all",
})
).toBe(true);
expect(
shouldRetryCanvasViewOperationAfterRefresh({
version: 1,
viewOperationId: "view-zoom",
operation: "zoom_in",
})
).toBe(false);
});
test("builds transient view operations for agent writeback results", () => {
expect(
buildCanvasAgentResultViewOperation({