From 8c6bd2ad03179f0138e8111c2d71ac217f7f7038 Mon Sep 17 00:00:00 2001 From: 134 Date: Tue, 23 Jun 2026 22:22:05 +0800 Subject: [PATCH] fix: preserve selected canvas slot context --- components/canvas/CanvasChatPanel.tsx | 22 +++++++++++-- lib/canvas/agent-client-intent.ts | 29 ++++++++++++++++ tests/routes/canvas-chat-helpers.test.ts | 42 ++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index d17322f3a..18ecff45c 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -115,7 +115,10 @@ import { shouldDeferCanvasAgentViewOperation, } from "@/lib/canvas/agent-view-interaction-guard"; import { getCanvasAgentOperationErrorMessage } from "@/lib/canvas/agent-operation-errors"; -import { shouldBypassCanvasImagePointsPrecheckForAgentControl } from "@/lib/canvas/agent-client-intent"; +import { + shouldBypassCanvasImagePointsPrecheckForAgentControl, + shouldPreserveCanvasAgentSlotOnlyContext, +} from "@/lib/canvas/agent-client-intent"; import { getCanvasSkillIconComponent } from "@/components/canvas/canvas-skill-icons"; import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar"; import { VideoParamsBar, type VideoGenerationParams } from "@/components/video-params-bar"; @@ -5941,6 +5944,20 @@ export function CanvasChatPanel({ isDetailGenerationMode, agentEnabled: effectiveAgentEnabled, }); + const selectedAgentSlotCountForContext = nodes.filter( + (node) => String(node.type) === "agent-slot" && selectedNodeIdSet.has(node.id) + ).length; + const shouldPreserveSelectedSlotContext = + shouldPreserveCanvasAgentSlotOnlyContext({ + text: input, + activeComposerMode: effectiveComposerMode, + selectedAgentSlotCount: selectedAgentSlotCountForContext, + attachmentCount: attachments.length, + hasSkillTag: Boolean(selectedSkillTag), + hasWorkflowContext, + isDetailGenerationMode, + agentEnabled: effectiveAgentEnabled, + }); const selectedImageNodesForContext = Array.from( new Map( [ @@ -5955,7 +5972,8 @@ export function CanvasChatPanel({ if ( shouldIsolateDirectModeContext && selectedImageNodesForContext.length === 0 && - !shouldPreserveCanvasControlContext + !shouldPreserveCanvasControlContext && + !shouldPreserveSelectedSlotContext ) { generationAnchorNodesRef.current = []; return undefined; diff --git a/lib/canvas/agent-client-intent.ts b/lib/canvas/agent-client-intent.ts index c2be2dc45..0072c5ad5 100644 --- a/lib/canvas/agent-client-intent.ts +++ b/lib/canvas/agent-client-intent.ts @@ -15,6 +15,9 @@ const CANVAS_AGENT_VIEW_CONTROL_REGEX = const CANVAS_AGENT_SLOT_MANAGEMENT_REGEX = /(新建|创建|添加|插入|放置|加一个|建一个).{0,10}(图片|视频|ui|画面|空)?(槽位|slot|空框|占位框|框)|(槽位|slot|空框|占位框).{0,10}(新建|创建|添加|插入|放置)/i; +const CANVAS_AGENT_SLOT_FILL_CONTEXT_REGEX = + /(填充|生成|生图|出图|画一张|来一张|做一张|做图|做视频|视频生成|生成视频|放到|放进|放入|在这里|这个空框|这个槽位|选中槽位|槽位|slot|空框|占位框)/i; + const CANVAS_AGENT_STRUCTURAL_OPERATION_REGEX = /(删除|移除|移动|挪动|排列|对齐|重命名|改名|连线|连接|添加批注|批注|备注|标注|添加文字|文字节点|更新文字|修改文字).{0,14}(画布|节点|对象|元素|图片|图|文本|便签|选中|这些|它们|这个)|(画布|节点|对象|元素|图片|图|文本|便签|选中|这些|它们|这个).{0,14}(删除|移除|移动|挪动|排列|对齐|重命名|改名|连线|连接|添加批注|批注|备注|标注|添加文字|文字节点|更新文字|修改文字)/i; @@ -29,6 +32,12 @@ export function hasCanvasAgentNonGenerativeControlIntent(value: unknown) { ); } +export function hasCanvasAgentSlotFillContextIntent(value: unknown) { + const text = normalizeCanvasAgentClientIntentText(value); + if (!text) return false; + return CANVAS_AGENT_SLOT_FILL_CONTEXT_REGEX.test(text); +} + export function shouldBypassCanvasImagePointsPrecheckForAgentControl(params: { text: unknown; activeComposerMode?: string | null; @@ -46,3 +55,23 @@ export function shouldBypassCanvasImagePointsPrecheckForAgentControl(params: { } return hasCanvasAgentNonGenerativeControlIntent(params.text); } + +export function shouldPreserveCanvasAgentSlotOnlyContext(params: { + text: unknown; + activeComposerMode?: string | null; + selectedAgentSlotCount?: number; + attachmentCount?: number; + hasSkillTag?: boolean; + hasWorkflowContext?: boolean; + isDetailGenerationMode?: boolean; + agentEnabled?: boolean; +}) { + if ((params.selectedAgentSlotCount || 0) <= 0) return false; + if (params.activeComposerMode === "agent") return true; + if (params.agentEnabled === false) return false; + if ((params.attachmentCount || 0) > 0) return false; + if (params.hasSkillTag || params.hasWorkflowContext || params.isDetailGenerationMode) { + return false; + } + return hasCanvasAgentSlotFillContextIntent(params.text); +} diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 97b8bd5ad..bac4cb71b 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -116,7 +116,9 @@ import { } from "@/lib/canvas/canvas-save-request"; import { canvasService } from "@/services/canvas"; import { + hasCanvasAgentSlotFillContextIntent, hasCanvasAgentNonGenerativeControlIntent, + shouldPreserveCanvasAgentSlotOnlyContext, shouldBypassCanvasImagePointsPrecheckForAgentControl, } from "@/lib/canvas/agent-client-intent"; import { @@ -312,6 +314,46 @@ test.describe("canvas chat pure helpers", () => { ).toBe(true); }); + test("preserves selected slot-only canvas context for fill and generation intents", () => { + expect(hasCanvasAgentSlotFillContextIntent("在这里生成一张主图")).toBe(true); + expect(hasCanvasAgentSlotFillContextIntent("填充这个槽位")).toBe(true); + expect(hasCanvasAgentSlotFillContextIntent("普通聊天")).toBe(false); + + expect( + shouldPreserveCanvasAgentSlotOnlyContext({ + text: "在这里生成一张主图", + activeComposerMode: "image", + selectedAgentSlotCount: 1, + agentEnabled: true, + }) + ).toBe(true); + expect( + shouldPreserveCanvasAgentSlotOnlyContext({ + text: "生成一张主图", + activeComposerMode: "image", + selectedAgentSlotCount: 0, + agentEnabled: true, + }) + ).toBe(false); + expect( + shouldPreserveCanvasAgentSlotOnlyContext({ + text: "在这里生成一张主图", + activeComposerMode: "image", + selectedAgentSlotCount: 1, + attachmentCount: 1, + agentEnabled: true, + }) + ).toBe(false); + expect( + shouldPreserveCanvasAgentSlotOnlyContext({ + text: "任意 agent 模式文本", + activeComposerMode: "agent", + selectedAgentSlotCount: 1, + agentEnabled: false, + }) + ).toBe(true); + }); + test("rejects stale autosave bases without blocking fresh or missing revisions", () => { const currentUpdatedAt = new Date("2026-06-22T00:00:05.500Z");