fix: focus canvas after agent writeback

This commit is contained in:
134 2026-06-22 22:24:59 +08:00
parent 5e82adb7eb
commit ad6b09d1b2
3 changed files with 140 additions and 0 deletions

View File

@ -73,6 +73,7 @@ import {
resolveCanvasAgentSlotCreationIntent,
type CanvasAgentModelPlannerOutcome,
} from "@/lib/canvas/agent-model-planner";
import { buildCanvasAgentResultViewOperation } from "@/lib/canvas/agent-result-view";
import {
getCanvasAgentOperationErrorMessage,
normalizeCanvasAgentOperationErrorCode,
@ -3056,6 +3057,23 @@ export async function POST(request: Request) {
},
`${params.operationLabel}:canvas-operation`,
);
if (canvasAgentFeatures.viewOperationEnabled) {
const viewOperation = buildCanvasAgentResultViewOperation({
projectId,
operationId,
mutationOperation: operation,
createdNodeIds: result.createdNodeIds,
updatedNodeIds: result.updatedNodeIds,
anchorNodeId: result.createdNodeIds[0] || result.updatedNodeIds[0] || null,
});
if (viewOperation) {
writeCanvasViewOperationChunk(
params.writer,
viewOperation,
`${params.operationLabel}:canvas-view-operation`,
);
}
}
return {
mediaItemIds: handledCanvasMediaItemIds,
handledCanvasMediaItemIds,

View File

@ -0,0 +1,68 @@
import type {
CanvasOperationKind,
CanvasViewOperationPlan,
} from "@/types/canvas/agent-protocol";
function normalizeNodeIds(nodeIds?: string[]) {
return Array.from(
new Set(
(nodeIds || [])
.map((nodeId) => String(nodeId || "").trim())
.filter(Boolean)
)
);
}
export function buildCanvasAgentResultViewOperation(params: {
projectId?: string | null;
operationId: string;
mutationOperation?: CanvasOperationKind;
createdNodeIds?: string[];
updatedNodeIds?: string[];
anchorNodeId?: string | null;
padding?: number;
durationMs?: number;
reason?: string;
}): CanvasViewOperationPlan | null {
const projectId = String(params.projectId || "").trim();
const operationId = String(params.operationId || "").trim();
if (!projectId || !operationId) return null;
const createdNodeIds = normalizeNodeIds(params.createdNodeIds);
const updatedNodeIds = normalizeNodeIds(params.updatedNodeIds).filter(
(nodeId) => !createdNodeIds.includes(nodeId)
);
const nodeIds = createdNodeIds.length > 0 ? createdNodeIds : updatedNodeIds;
if (nodeIds.length === 0) return null;
const requestedAnchorNodeId = String(params.anchorNodeId || "").trim();
const anchorNodeId = nodeIds.includes(requestedAnchorNodeId)
? requestedAnchorNodeId
: nodeIds[0];
const hasCreatedResult = createdNodeIds.length > 0;
const operation =
nodeIds.length > 1
? "focus_nodes"
: hasCreatedResult
? "focus_latest_result"
: "focus_node";
return {
version: 1,
viewOperationId: `${operationId}:view`,
operation,
projectId,
nodeIds,
anchorNodeId,
padding: params.padding ?? 120,
durationMs: params.durationMs ?? 360,
persist: false,
reason:
params.reason ||
(params.mutationOperation === "replace_node_media"
? "focus_canvas_agent_replaced_nodes"
: hasCreatedResult
? "focus_canvas_agent_writeback_result"
: "focus_canvas_agent_updated_nodes"),
};
}

View File

@ -48,6 +48,7 @@ import {
shouldConsiderCanvasAgentModelPlanner,
} from "@/lib/canvas/agent-model-planner";
import { resolveCanvasAgentMediaPlacements } from "@/lib/canvas/agent-placement";
import { buildCanvasAgentResultViewOperation } from "@/lib/canvas/agent-result-view";
import {
computeCanvasAgentPanViewport,
resolveCanvasViewOperation,
@ -1205,6 +1206,59 @@ test.describe("canvas chat pure helpers", () => {
expect(useCanvasStore.getState().isAgentOperationRefreshActive()).toBe(false);
});
test("builds transient view operations for agent writeback results", () => {
expect(
buildCanvasAgentResultViewOperation({
projectId: plannerContext.projectId,
operationId: "operation-1",
mutationOperation: "insert_beside",
createdNodeIds: ["new-image-1"],
})
).toMatchObject({
version: 1,
viewOperationId: "operation-1:view",
operation: "focus_latest_result",
nodeIds: ["new-image-1"],
anchorNodeId: "new-image-1",
persist: false,
reason: "focus_canvas_agent_writeback_result",
});
expect(
buildCanvasAgentResultViewOperation({
projectId: plannerContext.projectId,
operationId: "operation-2",
mutationOperation: "append_revision",
createdNodeIds: ["new-image-1", "new-image-2", "new-image-1"],
updatedNodeIds: ["slot-1"],
})
).toMatchObject({
operation: "focus_nodes",
nodeIds: ["new-image-1", "new-image-2"],
anchorNodeId: "new-image-1",
});
expect(
buildCanvasAgentResultViewOperation({
projectId: plannerContext.projectId,
operationId: "operation-3",
mutationOperation: "replace_node_media",
updatedNodeIds: ["image-1"],
})
).toMatchObject({
operation: "focus_node",
nodeIds: ["image-1"],
reason: "focus_canvas_agent_replaced_nodes",
});
expect(
buildCanvasAgentResultViewOperation({
projectId: plannerContext.projectId,
operationId: "operation-4",
})
).toBeNull();
});
test("resolves latest-result and pan canvas view operations", () => {
expect(
resolveCanvasViewOperationTargetNodeIds({