From d548e31063a727fdc3c7476c68c7ae8fabac87be Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 23 Jun 2026 22:07:38 +0800 Subject: [PATCH] fix: honor canvas agent view rollout on client --- .../settings/admin-settings-client.tsx | 45 +++---------------- app/api/site-settings/route.ts | 2 + .../[id]/CanvasEditorLegacyPageClient.tsx | 5 +++ components/canvas/CanvasChatPanel.tsx | 26 ++++++++++- lib/canvas/agent-feature-flags.ts | 38 ++++++++++++++++ tests/routes/canvas-chat-helpers.test.ts | 16 +++++++ 6 files changed, 92 insertions(+), 40 deletions(-) diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 615d45e19..57ed39331 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -204,7 +204,11 @@ import { getDefaultCanvasToolbarItem, normalizeCanvasToolbarConfig, } from "@/lib/canvas-toolbar-config"; -import type { CanvasAgentFeatureSettings } from "@/lib/canvas/agent-feature-flags"; +import { + defaultCanvasAgentFeatureSettings, + normalizeCanvasAgentFeatureSettings, + type CanvasAgentFeatureSettings, +} from "@/lib/canvas/agent-feature-flags"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { AI_WASH_CONFIG_KEY, @@ -1227,43 +1231,6 @@ type HomeAnimationConfig = { updatedAt: string | null; }; -const defaultCanvasAgentFeatureSettings: CanvasAgentFeatureSettings = { - operationProtocolEnabled: true, - writebackMode: "operation_authoritative", - slotEnabled: true, - viewOperationEnabled: true, - annotationEditEnabled: true, -}; - -function normalizeCanvasAgentFeatureSettingsForClient( - value: unknown -): CanvasAgentFeatureSettings { - const raw = - value && typeof value === "object" && !Array.isArray(value) - ? (value as Partial) - : {}; - const writebackMode = - raw.writebackMode === "legacy_canvas_media" || - raw.writebackMode === "operation_shadow" || - raw.writebackMode === "operation_authoritative" - ? raw.writebackMode - : defaultCanvasAgentFeatureSettings.writebackMode; - - return { - operationProtocolEnabled: - raw.operationProtocolEnabled ?? - defaultCanvasAgentFeatureSettings.operationProtocolEnabled, - writebackMode, - slotEnabled: raw.slotEnabled ?? defaultCanvasAgentFeatureSettings.slotEnabled, - viewOperationEnabled: - raw.viewOperationEnabled ?? - defaultCanvasAgentFeatureSettings.viewOperationEnabled, - annotationEditEnabled: - raw.annotationEditEnabled ?? - defaultCanvasAgentFeatureSettings.annotationEditEnabled, - }; -} - const defaultLovartPromptTagVisibility: LovartPromptTagVisibility = { nanoBananaPro: true, design: true, @@ -2263,7 +2230,7 @@ export function AdminSettingsClient() { agentEnabled: (siteSettingsData as any).agentEnabled ?? true, canvasAgentModeEnabled: (siteSettingsData as any).canvasAgentModeEnabled ?? true, - canvasAgentFeatureSettings: normalizeCanvasAgentFeatureSettingsForClient( + canvasAgentFeatureSettings: normalizeCanvasAgentFeatureSettings( (siteSettingsData as any).canvasAgentFeatureSettings ), videoCanvasEnabled: diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 7b7a7e3c2..8720029e7 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -886,6 +886,7 @@ async function getScopedPublicSiteSettings( LOVART_COMPOSER_PREVIEW_TEXTS_KEY, LOVART_COMPOSER_PREFIX_KEY, CANVAS_TOOLBAR_CONFIG_SETTING_KEY, + ...CANVAS_AGENT_FEATURE_SETTING_KEYS, OPENCLAW_CHAT_CONFIG_KEY, ]); const typedSettings = settings as Record | undefined; @@ -903,6 +904,7 @@ async function getScopedPublicSiteSettings( valueByKey.get(LOVART_COMPOSER_PREVIEW_TEXTS_KEY) ), canvasEnabled: typedSettings?.canvasEnabled ?? true, + canvasAgentFeatureSettings: buildCanvasAgentFeatureSettings(valueByKey), aiDetailGenerationEnabled: Boolean( valueByKey.get(AI_DETAIL_GENERATOR_ENABLED_KEY) ), diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index b75967053..4d41b05b0 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -172,6 +172,7 @@ import { type CanvasToolbarItemConfig, type CanvasToolbarItemKey, } from "@/lib/canvas-toolbar-config"; +import type { CanvasAgentFeatureSettings } from "@/lib/canvas/agent-feature-flags"; import { buildAgentWorkflowComposerContext, } from "@/lib/agents/workflow-composer"; @@ -2060,6 +2061,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { siteLogoDark?: string | null; agentEnabled?: boolean; canvasAgentModeEnabled?: boolean; + canvasAgentFeatureSettings?: CanvasAgentFeatureSettings; videoCanvasSidebarEnabled?: boolean; layerSplitEnabled?: boolean; angleEditEnabled?: boolean; @@ -10454,6 +10456,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
normalizeCanvasToolbarConfig(rawCanvasToolbarConfig), [rawCanvasToolbarConfig] ); + const canvasAgentFeatureSettingsRef = useRef( + normalizeCanvasAgentFeatureSettings(propCanvasAgentFeatureSettings) + ); const effectiveAgentEnabled = agentEnabled && !isVideoWorkspace; const { data: session } = useSession(); const [input, setInput] = useState(""); @@ -1988,7 +1998,12 @@ export function CanvasChatPanel({ const applyCanvasViewOperation = useCallback( (payload: CanvasViewOperationStreamPayload) => { - if (!CANVAS_AGENT_VIEW_OPERATION_CLIENT_ENABLED) return false; + if ( + !CANVAS_AGENT_VIEW_OPERATION_CLIENT_ENABLED || + !canvasAgentFeatureSettingsRef.current.viewOperationEnabled + ) { + return false; + } if (!payload || typeof payload !== "object") return false; const targetProjectId = String(payload.projectId || "").trim(); if (targetProjectId && targetProjectId !== canvasId) return false; @@ -2580,11 +2595,20 @@ export function CanvasChatPanel({ canvasAutoModeDefaultModelId?: string | null; canvasDetailDefaultModelId?: string | null; canvasToolbarConfig?: CanvasToolbarConfig; + canvasAgentFeatureSettings?: CanvasAgentFeatureSettings; openClawChatConfig?: { enabled?: boolean; baseUrl?: string; }; }>("/api/site-settings?scope=canvas-chat", fetcher); + useEffect(() => { + canvasAgentFeatureSettingsRef.current = + normalizeCanvasAgentFeatureSettings( + propCanvasAgentFeatureSettings || + siteSettings?.canvasAgentFeatureSettings || + defaultCanvasAgentFeatureSettings + ); + }, [propCanvasAgentFeatureSettings, siteSettings?.canvasAgentFeatureSettings]); const { data: canvasSkillTagsData } = useSWR<{ enabled?: boolean; title?: string; diff --git a/lib/canvas/agent-feature-flags.ts b/lib/canvas/agent-feature-flags.ts index 118a4bb53..892dac5de 100644 --- a/lib/canvas/agent-feature-flags.ts +++ b/lib/canvas/agent-feature-flags.ts @@ -13,6 +13,44 @@ export type CanvasAgentFeatureFlags = { export type CanvasAgentFeatureSettings = CanvasAgentFeatureFlags; +export const defaultCanvasAgentFeatureSettings: CanvasAgentFeatureSettings = { + operationProtocolEnabled: true, + writebackMode: "operation_authoritative", + slotEnabled: true, + viewOperationEnabled: true, + annotationEditEnabled: true, +}; + +export function normalizeCanvasAgentFeatureSettings( + value: unknown, + fallback: CanvasAgentFeatureSettings = defaultCanvasAgentFeatureSettings +): CanvasAgentFeatureSettings { + const raw = + value && typeof value === "object" && !Array.isArray(value) + ? (value as Partial) + : {}; + + return { + operationProtocolEnabled: normalizeCanvasAgentBoolean( + raw.operationProtocolEnabled, + fallback.operationProtocolEnabled + ), + writebackMode: normalizeCanvasAgentWritebackMode( + raw.writebackMode, + fallback.writebackMode + ), + slotEnabled: normalizeCanvasAgentBoolean(raw.slotEnabled, fallback.slotEnabled), + viewOperationEnabled: normalizeCanvasAgentBoolean( + raw.viewOperationEnabled, + fallback.viewOperationEnabled + ), + annotationEditEnabled: normalizeCanvasAgentBoolean( + raw.annotationEditEnabled, + fallback.annotationEditEnabled + ), + }; +} + export const CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY = "canvas_agent_operation_protocol_enabled"; export const CANVAS_AGENT_WRITEBACK_MODE_KEY = "canvas_agent_writeback_mode"; diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 8581d0e88..97b8bd5ad 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -138,6 +138,7 @@ import { canvasAgentFeatureSettingsToSystemSettings, canApplyCanvasAgentMutations, getCanvasAgentFeatureFlags, + normalizeCanvasAgentFeatureSettings, normalizeCanvasAgentBoolean, normalizeCanvasAgentWritebackMode, shouldStreamCanvasAgentShadowOperations, @@ -1857,6 +1858,21 @@ test.describe("canvas chat pure helpers", () => { [CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY]: true, [CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY]: false, }); + expect( + normalizeCanvasAgentFeatureSettings({ + operationProtocolEnabled: "off", + writebackMode: "invalid-mode", + slotEnabled: 0, + viewOperationEnabled: "on", + annotationEditEnabled: false, + }) + ).toEqual({ + operationProtocolEnabled: false, + writebackMode: "operation_authoritative", + slotEnabled: false, + viewOperationEnabled: true, + annotationEditEnabled: false, + }); const invalidSettingsFallback = getCanvasAgentFeatureFlags( { CANVAS_AGENT_WRITEBACK_MODE: "operation_shadow" },