Files
infinite-canvas/web/src/pages/canvas/hooks/use-agent-bridge.ts
T
HouYunFei cf0561d12a refactor(canvas-project): extract agent bridge into useAgentBridge hook
Move the canvas<->local-Agent bridge (agentSnapshot memo, applyAgentOps /
undoAgentOps, agentUndoSnapshot state, and the publish-to-agent-store effect)
out of InfiniteCanvasPage into a useAgentBridge hook. Only applyAgentOps is
consumed outside (by the plugin host); the rest is fully encapsulated.

The hook receives the shared canvas state/refs/setters explicitly and keeps the
exact same dependency arrays, so behavior is unchanged. project.tsx drops the
now-unused agent-ops imports and the setCanvasContext selector.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:14:10 +08:00

98 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState, type Dispatch, type MutableRefObject, type SetStateAction } from "react";
import { useAgentStore } from "@/stores/use-agent-store";
import { applyCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
import type { CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
import type { CanvasConnection, CanvasNodeData, ContextMenuState, ViewportTransform } from "@/types/canvas";
type GenerateNodeRef = MutableRefObject<((nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => Promise<void>) | null>;
type AgentBridgeParams = {
projectId: string;
title: string | undefined;
nodes: CanvasNodeData[];
connections: CanvasConnection[];
selectedNodeIds: Set<string>;
viewport: ViewportTransform;
nodesRef: MutableRefObject<CanvasNodeData[]>;
connectionsRef: MutableRefObject<CanvasConnection[]>;
selectedNodeIdsRef: MutableRefObject<Set<string>>;
viewportRef: MutableRefObject<ViewportTransform>;
generateNodeRef: GenerateNodeRef;
setNodes: Dispatch<SetStateAction<CanvasNodeData[]>>;
setConnections: Dispatch<SetStateAction<CanvasConnection[]>>;
setSelectedNodeIds: Dispatch<SetStateAction<Set<string>>>;
setSelectedConnectionId: Dispatch<SetStateAction<string | null>>;
setViewport: Dispatch<SetStateAction<ViewportTransform>>;
setContextMenu: Dispatch<SetStateAction<ContextMenuState | null>>;
};
/**
* 画布与本地 Agent 的桥接:把当前画布快照与 apply/undo 能力发布到 agent store
* 供本地 Codex 面板读取。除 applyAgentOps(配置节点插件宿主会用到)外均为内部实现。
*/
export function useAgentBridge(params: AgentBridgeParams) {
const { projectId, title, nodes, connections, selectedNodeIds, viewport, nodesRef, connectionsRef, selectedNodeIdsRef, viewportRef, generateNodeRef, setNodes, setConnections, setSelectedNodeIds, setSelectedConnectionId, setViewport, setContextMenu } =
params;
const setAgentCanvasContext = useAgentStore((state) => state.setCanvasContext);
const [agentUndoSnapshot, setAgentUndoSnapshot] = useState<CanvasAgentSnapshot | null>(null);
const projectTitle = title || "未命名画布";
const agentSnapshot = useMemo<CanvasAgentSnapshot>(() => ({ projectId, title: projectTitle, nodes, connections, selectedNodeIds: Array.from(selectedNodeIds), viewport }), [connections, projectTitle, nodes, projectId, selectedNodeIds, viewport]);
const applyAgentOps = useCallback(
(ops?: CanvasAgentOp[]) => {
const safeOps = Array.isArray(ops) ? ops.filter((op) => op?.type) : [];
const before = { projectId, title: projectTitle, nodes: nodesRef.current, connections: connectionsRef.current, selectedNodeIds: Array.from(selectedNodeIdsRef.current), viewport: viewportRef.current };
const generationOps = safeOps.filter((op): op is Extract<CanvasAgentOp, { type: "run_generation" }> => op.type === "run_generation" && Boolean(op.nodeId));
const next = applyCanvasAgentOps(
before,
safeOps.filter((op) => op.type !== "run_generation"),
);
nodesRef.current = next.nodes;
connectionsRef.current = next.connections;
selectedNodeIdsRef.current = new Set(next.selectedNodeIds);
viewportRef.current = next.viewport;
setAgentUndoSnapshot(before);
setNodes(next.nodes);
setConnections(next.connections);
setSelectedNodeIds(new Set(next.selectedNodeIds));
setSelectedConnectionId(null);
setViewport(next.viewport);
setContextMenu(null);
if (generationOps.length) {
queueMicrotask(() =>
generationOps.forEach((op) => {
const target = nodesRef.current.find((node) => node.id === op.nodeId);
const prompt = op.prompt?.trim() ? op.prompt : (target?.metadata?.composerContent ?? target?.metadata?.prompt ?? "");
void generateNodeRef.current?.(op.nodeId, op.mode || target?.metadata?.generationMode || "image", prompt);
}),
);
}
return { ...next, projectId, title: projectTitle };
},
[projectTitle, projectId],
);
const undoAgentOps = useCallback(() => {
if (!agentUndoSnapshot) return null;
nodesRef.current = agentUndoSnapshot.nodes;
connectionsRef.current = agentUndoSnapshot.connections;
selectedNodeIdsRef.current = new Set(agentUndoSnapshot.selectedNodeIds);
viewportRef.current = agentUndoSnapshot.viewport;
setNodes(agentUndoSnapshot.nodes);
setConnections(agentUndoSnapshot.connections);
setSelectedNodeIds(new Set(agentUndoSnapshot.selectedNodeIds));
setSelectedConnectionId(null);
setViewport(agentUndoSnapshot.viewport);
setContextMenu(null);
setAgentUndoSnapshot(null);
return { ...agentUndoSnapshot, projectId, title: projectTitle };
}, [agentUndoSnapshot, projectTitle, projectId]);
useEffect(() => {
setAgentCanvasContext({ snapshot: agentSnapshot, applyOps: applyAgentOps, undoOps: undoAgentOps, canUndo: Boolean(agentUndoSnapshot) });
return () => setAgentCanvasContext(null);
}, [agentSnapshot, applyAgentOps, agentUndoSnapshot, setAgentCanvasContext, undoAgentOps]);
return { applyAgentOps };
}