feat(canvas): Agent引用与局部重绘 - 完善附件预览多图展开并修复取消清理

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-08 22:59:49 +08:00
parent 897981b893
commit 6c1d17dbf6
26 changed files with 999 additions and 236 deletions

View File

@@ -1,4 +1,91 @@
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeMetadata } from "@/types/canvas";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata } from "@/types/canvas";
export function liveImageBatchChildren(root: CanvasNodeData, nodes: CanvasNodeData[]) {
if (root.type !== CanvasNodeType.Image) return [];
const listed = new Set(root.metadata?.batchChildIds || []);
return nodes.filter((node) => node.id !== root.id && (node.metadata?.batchRootId === root.id || listed.has(node.id)));
}
export function retireImageBatchChildren(root: CanvasNodeData, nodes: CanvasNodeData[], connections: CanvasConnection[]) {
const children = liveImageBatchChildren(root, nodes);
if (!children.length) return { nodes, connections, removedIds: [] as string[] };
const removedIds = children.filter((node) => !node.metadata?.content).map((node) => node.id);
const removed = new Set(removedIds);
const kept = children.filter((node) => Boolean(node.metadata?.content));
const nextNodes = nodes
.filter((node) => !removed.has(node.id))
.map((node) => {
if (node.id === root.id) {
const metadata = { ...node.metadata, imageBatchExpanded: true };
delete metadata.batchChildIds;
delete metadata.primaryImageId;
delete metadata.batchFailedCount;
if (!metadata.content) metadata.status = "idle";
return { ...node, metadata };
}
const index = kept.findIndex((item) => item.id === node.id);
if (index < 0) return node;
const metadata = { ...node.metadata };
delete metadata.batchRootId;
return {
...node,
position: { x: root.position.x - node.width - 48, y: root.position.y + index * (node.height + 24) },
metadata,
};
});
return {
nodes: nextNodes,
connections: connections.filter((connection) => !removed.has(connection.fromNodeId) && !removed.has(connection.toNodeId)),
removedIds,
};
}
export function cancelIncompleteImageBatch(rootId: string, childIds: string[], nodes: CanvasNodeData[], connections: CanvasConnection[]) {
const childIdSet = new Set(childIds);
const removedIds = nodes.filter((node) => childIdSet.has(node.id) && !node.metadata?.content).map((node) => node.id);
const removed = new Set(removedIds);
const nextNodes = nodes
.filter((node) => !removed.has(node.id))
.map((node) => {
if (node.id !== rootId) return node;
const remaining = (node.metadata?.batchChildIds || []).filter((id) => !removed.has(id) && nodes.some((item) => item.id === id && item.metadata?.content));
const hasContent = Boolean(node.metadata?.content);
const metadata = {
...node.metadata,
batchChildIds: remaining.length ? remaining : undefined,
isBatchRoot: remaining.length > 1 ? true : undefined,
imageBatchExpanded: remaining.length > 1 ? true : undefined,
status: hasContent ? node.metadata?.status : "idle",
};
if (!hasContent) {
delete metadata.errorDetails;
delete metadata.generationErrorCode;
delete metadata.failedPromptFingerprint;
}
if (remaining.length <= 1) {
delete metadata.batchFailedCount;
delete metadata.primaryImageId;
delete metadata.isBatchRoot;
delete metadata.imageBatchExpanded;
delete metadata.batchChildIds;
}
return { ...node, metadata };
});
const remainingIds = new Set(
nextNodes.find((node) => node.id === rootId)?.metadata?.batchChildIds || [],
);
const detached = nextNodes.map((node) => {
if (!childIdSet.has(node.id) || remainingIds.has(node.id)) return node;
const metadata = { ...node.metadata };
delete metadata.batchRootId;
return { ...node, metadata };
});
return {
nodes: detached,
connections: connections.filter((connection) => !removed.has(connection.fromNodeId) && !removed.has(connection.toNodeId)),
removedIds,
};
}
export function failedImageBatchChildren(root: CanvasNodeData, nodes: CanvasNodeData[]) {
if (root.type !== CanvasNodeType.Image || !root.metadata?.isBatchRoot) return [];

View File

@@ -226,6 +226,14 @@ export function buildOrderedCanvasResourceReferences(nodes: CanvasNodeData[], ac
return labelResourceNodes(nodes.filter(isResourceNode), active);
}
export function imageGenerationReferenceConnections(sourceNodeId: string, targetNodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[], createId: () => string): CanvasConnection[] {
if (!sourceNodeId || sourceNodeId === targetNodeId) return [];
const existing = new Set(connections.filter((connection) => connection.toNodeId === targetNodeId).map((connection) => connection.fromNodeId));
return getMentionResourceNodes(sourceNodeId, nodes, connections)
.filter((node) => node.id !== sourceNodeId && node.id !== targetNodeId && !existing.has(node.id))
.map((node) => ({ id: createId(), fromNodeId: node.id, toNodeId: targetNodeId }));
}
export function getMentionResourceNodes(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]) {
const configInputs = getConnectedConfigResourceNodes(nodeId, nodes, connections);
if (configInputs.length) return configInputs;