From cb44b2237fcf19553b53fb955432bb41a20ce9aa Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 23 Jun 2026 19:09:07 +0800 Subject: [PATCH] fix: gate canvas agent view operations on client --- components/canvas/CanvasChatPanel.tsx | 6 ++++++ .../canvas/chat-panel/canvas-agent-stream.ts | 16 ++++++++++++++++ tests/routes/canvas-chat-helpers.test.ts | 15 +++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index de8cc5224..398226bf9 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -139,6 +139,7 @@ import { getCanvasMediaPayloadHandledItemIds, getCanvasOperationHandledMediaItemIds, type ImageGenerationStatusStreamPayload, + isCanvasAgentViewOperationClientEnabled, shouldRetryCanvasViewOperationAfterRefresh, shouldSkipCanvasMediaPayloadForHandledItems, } from "@/components/canvas/chat-panel/canvas-agent-stream"; @@ -482,6 +483,10 @@ const CANVAS_IMAGE_TASK_TIMEOUT_MS = 10 * 60 * 1000; const CANVAS_GENERATION_NODE_GAP = 56; const CANVAS_GENERATION_STACK_GAP = 28; const CANVAS_GENERATION_COLLISION_GAP = 32; +const CANVAS_AGENT_VIEW_OPERATION_CLIENT_ENABLED = + isCanvasAgentViewOperationClientEnabled( + process.env.NEXT_PUBLIC_CANVAS_AGENT_VIEW_OPERATION_ENABLED + ); function hashCanvasChatValue(value: string) { let hash = 0; @@ -1978,6 +1983,7 @@ export function CanvasChatPanel({ const applyCanvasViewOperation = useCallback( (payload: CanvasViewOperationStreamPayload) => { + if (!CANVAS_AGENT_VIEW_OPERATION_CLIENT_ENABLED) return false; if (!payload || typeof payload !== "object") return false; const targetProjectId = String(payload.projectId || "").trim(); if (targetProjectId && targetProjectId !== canvasId) return false; diff --git a/components/canvas/chat-panel/canvas-agent-stream.ts b/components/canvas/chat-panel/canvas-agent-stream.ts index a949a7094..0caf7e19c 100644 --- a/components/canvas/chat-panel/canvas-agent-stream.ts +++ b/components/canvas/chat-panel/canvas-agent-stream.ts @@ -42,6 +42,22 @@ export function normalizeCanvasStreamIdList(value: unknown) { : []; } +export function normalizeCanvasAgentClientBoolean( + value: unknown, + fallback: boolean +) { + if (typeof value !== "string") return fallback; + const normalized = value.trim().toLowerCase(); + if (!normalized) return fallback; + if (["1", "true", "yes", "on", "enabled"].includes(normalized)) return true; + if (["0", "false", "no", "off", "disabled"].includes(normalized)) return false; + return fallback; +} + +export function isCanvasAgentViewOperationClientEnabled(value: unknown) { + return normalizeCanvasAgentClientBoolean(value, true); +} + export function getCanvasOperationHandledMediaItemIds( payload: CanvasOperationStreamPayload ) { diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index ea712f1f3..30bf1ca18 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -9,6 +9,8 @@ import { import { getCanvasMediaPayloadHandledItemIds, getCanvasOperationHandledMediaItemIds, + isCanvasAgentViewOperationClientEnabled, + normalizeCanvasAgentClientBoolean, shouldRetryCanvasViewOperationAfterRefresh, shouldSkipCanvasMediaPayloadForHandledItems, } from "@/components/canvas/chat-panel/canvas-agent-stream"; @@ -207,6 +209,19 @@ test.describe("canvas chat pure helpers", () => { expect(isAgentModeScopedChatId(canvasChatId)).toBe(false); }); + test("normalizes client-side canvas agent feature flags", () => { + expect(normalizeCanvasAgentClientBoolean(undefined, true)).toBe(true); + expect(normalizeCanvasAgentClientBoolean("", false)).toBe(false); + expect(normalizeCanvasAgentClientBoolean("enabled", false)).toBe(true); + expect(normalizeCanvasAgentClientBoolean("off", true)).toBe(false); + expect(normalizeCanvasAgentClientBoolean("disabled", true)).toBe(false); + expect(normalizeCanvasAgentClientBoolean("unexpected", true)).toBe(true); + + expect(isCanvasAgentViewOperationClientEnabled(undefined)).toBe(true); + expect(isCanvasAgentViewOperationClientEnabled("false")).toBe(false); + expect(isCanvasAgentViewOperationClientEnabled("1")).toBe(true); + }); + test("rejects stale autosave bases without blocking fresh or missing revisions", () => { const currentUpdatedAt = new Date("2026-06-22T00:00:05.500Z");