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

View File

@ -9,6 +9,7 @@ import {
import { import {
getCanvasMediaPayloadHandledItemIds, getCanvasMediaPayloadHandledItemIds,
getCanvasOperationHandledMediaItemIds, getCanvasOperationHandledMediaItemIds,
shouldRetryCanvasViewOperationAfterRefresh,
shouldSkipCanvasMediaPayloadForHandledItems, shouldSkipCanvasMediaPayloadForHandledItems,
} from "@/components/canvas/chat-panel/canvas-agent-stream"; } from "@/components/canvas/chat-panel/canvas-agent-stream";
import { import {
@ -1217,6 +1218,31 @@ test.describe("canvas chat pure helpers", () => {
expect(useCanvasStore.getState().isAgentOperationRefreshActive()).toBe(false); 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", () => { test("builds transient view operations for agent writeback results", () => {
expect( expect(
buildCanvasAgentResultViewOperation({ buildCanvasAgentResultViewOperation({