fix: honor canvas agent view rollout on client

This commit is contained in:
134 2026-06-23 22:07:38 +08:00
parent d434485987
commit d548e31063
6 changed files with 92 additions and 40 deletions

View File

@ -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<CanvasAgentFeatureSettings>)
: {};
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:

View File

@ -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<string, unknown> | 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)
),

View File

@ -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) {
<div className="flex-1 overflow-hidden" key={`chat-${id}`}>
<CanvasChatPanel
agentEnabled={isCanvasAgentModeEnabled}
canvasAgentFeatureSettings={
siteSettingsData?.canvasAgentFeatureSettings
}
canvasToolbarConfig={canvasToolbarConfig}
chatId={`canvas-${id}`}
chatScope="canvas"

View File

@ -96,6 +96,11 @@ import {
isCanvasImageReadyForAi,
isCanvasImageUploadPending,
} from "@/lib/canvas/local-upload-state";
import {
defaultCanvasAgentFeatureSettings,
normalizeCanvasAgentFeatureSettings,
type CanvasAgentFeatureSettings,
} from "@/lib/canvas/agent-feature-flags";
import {
resolveCanvasViewOperation,
} from "@/lib/canvas/agent-view-operations";
@ -297,6 +302,7 @@ interface CanvasChatPanelProps {
agentEnabled?: boolean;
workspaceVariant?: "canvas" | "video";
canvasToolbarConfig?: CanvasToolbarConfig;
canvasAgentFeatureSettings?: CanvasAgentFeatureSettings;
}
type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd";
@ -1191,6 +1197,7 @@ export function CanvasChatPanel({
agentEnabled = true,
workspaceVariant = "canvas",
canvasToolbarConfig: rawCanvasToolbarConfig,
canvasAgentFeatureSettings: propCanvasAgentFeatureSettings,
}: CanvasChatPanelProps) {
const isWorkspacePresentation = presentation === "workspace";
const isVideoWorkspace = workspaceVariant === "video";
@ -1198,6 +1205,9 @@ export function CanvasChatPanel({
() => normalizeCanvasToolbarConfig(rawCanvasToolbarConfig),
[rawCanvasToolbarConfig]
);
const canvasAgentFeatureSettingsRef = useRef<CanvasAgentFeatureSettings>(
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;

View File

@ -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<CanvasAgentFeatureSettings>)
: {};
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";

View File

@ -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" },