Skip to content

Commit ec1b2f7

Browse files
move some handlers to useAgentSelectionSync, remove useDefaultSubAgentNodeIdRef (#2855)
* upd * upd * upd * upd * upd * upd * upd * move handleNavigateToNode * upd * upd * upd * upd * upd * style: auto-format with biome * polish * Update page.client.tsx * Add changeset for agents-manage-ui Co-authored-by: Dimitri POSTOLOV <dimaMachina@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Dimitri POSTOLOV <dimaMachina@users.noreply.github.com>
1 parent cad67b9 commit ec1b2f7

File tree

6 files changed

+138
-121
lines changed

6 files changed

+138
-121
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inkeep/agents-manage-ui": patch
3+
---
4+
5+
Extract selection-sync logic into useAgentSelectionSync hook and remove useDefaultSubAgentNodeIdRef

agents-manage-ui/src/app/[tenantId]/projects/[projectId]/agents/[agentId]/page.client.tsx

Lines changed: 9 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ import {
66
Controls,
77
type Edge,
88
type Node,
9-
type OnSelectionChangeFunc,
109
Panel,
1110
ReactFlow,
1211
type ReactFlowProps,
13-
useOnSelectionChange,
1412
useReactFlow,
1513
} from '@xyflow/react';
1614
import dynamic from 'next/dynamic';
@@ -41,9 +39,9 @@ import { EditorLoadingSkeleton } from '@/components/agent/sidepane/editor-loadin
4139
import { SidePane } from '@/components/agent/sidepane/sidepane';
4240
import { Toolbar } from '@/components/agent/toolbar';
4341
import { UnsavedChangesDialog } from '@/components/agent/unsaved-changes-dialog';
42+
import { useAgentSelectionSync } from '@/components/agent/use-agent-selection-sync';
4443
import { useAgentShortcuts } from '@/components/agent/use-agent-shortcuts';
4544
import { useAnimateGraph } from '@/components/agent/use-animate-graph';
46-
import { useDefaultSubAgentNodeIdRef } from '@/components/agent/use-default-sub-agent-id-ref';
4745
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
4846
import { useCopilotContext } from '@/contexts/copilot';
4947
import { useFullAgentFormContext } from '@/contexts/full-agent-form';
@@ -57,9 +55,6 @@ import {
5755
createMcpRelationFormInput,
5856
createSubAgentFormInput,
5957
editorToPayload,
60-
findEdgeByGraphKey,
61-
findNodeByGraphKey,
62-
getEdgeGraphKey,
6358
getFunctionToolRelationFormKey,
6459
getMcpRelationFormKey,
6560
getNodeGraphKey,
@@ -155,7 +150,6 @@ export const Agent: FC<AgentProps> = ({ agent }) => {
155150
}));
156151
const {
157152
setNodes,
158-
setEdges,
159153
onNodesChange,
160154
onEdgesChange: onEdgesChangeAction,
161155
setInitial,
@@ -164,6 +158,13 @@ export const Agent: FC<AgentProps> = ({ agent }) => {
164158
markUnsaved,
165159
reset,
166160
} = useAgentActions();
161+
const { backToAgent, closeSidePane, selectedEdge, selectedNode } = useAgentSelectionSync({
162+
nodes,
163+
edges,
164+
isOpen,
165+
nodeId,
166+
edgeId,
167+
});
167168

168169
function onAddInitialNode(): void {
169170
const newNode = {
@@ -479,48 +480,8 @@ export const Agent: FC<AgentProps> = ({ agent }) => {
479480
commandManager.execute(new AddNodeCommand(newNode));
480481
};
481482

482-
const onSelectionChange: OnSelectionChangeFunc = ({ nodes, edges }) => {
483-
const node = nodes.length === 1 ? nodes[0] : null;
484-
const edge =
485-
edges.length === 1 &&
486-
(edges[0]?.type === EdgeType.A2A || edges[0]?.type === EdgeType.SelfLoop)
487-
? edges[0]
488-
: null;
489-
const defaultPane = isOpen ? 'agent' : null;
490-
setQueryState(
491-
{
492-
pane: node ? 'node' : edge ? 'edge' : defaultPane,
493-
nodeId: node ? getNodeGraphKey(node) : null,
494-
edgeId: edge ? getEdgeGraphKey(edge, nodes) : null,
495-
},
496-
{ history: 'replace' }
497-
);
498-
};
499-
500-
useOnSelectionChange({ onChange: onSelectionChange });
501-
502483
useAgentShortcuts();
503484

504-
const closeSidePane = () => {
505-
setEdges((edges) => edges.map((edge) => ({ ...edge, selected: false })));
506-
setNodes((nodes) => nodes.map((node) => ({ ...node, selected: false })));
507-
setQueryState({
508-
pane: null,
509-
nodeId: null,
510-
edgeId: null,
511-
});
512-
};
513-
514-
const backToAgent = () => {
515-
setEdges((edges) => edges.map((edge) => ({ ...edge, selected: false })));
516-
setNodes((nodes) => nodes.map((node) => ({ ...node, selected: false })));
517-
setQueryState({
518-
pane: 'agent',
519-
nodeId: null,
520-
edgeId: null,
521-
});
522-
};
523-
524485
const onSubmit = form.handleSubmit(
525486
async ({ mcpRelations, defaultSubAgentNodeId, ...data }): Promise<void> => {
526487
const serializedData = editorToPayload(nodes, edges, {
@@ -590,47 +551,6 @@ export const Agent: FC<AgentProps> = ({ agent }) => {
590551
console.error
591552
);
592553

593-
// React Flow selection can stay the same while a selected node/edge gets a new graph key,
594-
// so mirror the current canvas selection back into query state here.
595-
useEffect(() => {
596-
const selectedCanvasNode = nodes.filter((node) => node.selected);
597-
const selectedCanvasEdge = edges.filter((edge) => edge.selected);
598-
const singleSelectedNode = selectedCanvasNode.length === 1 ? selectedCanvasNode[0] : null;
599-
const singleSelectedEdge = selectedCanvasEdge.length === 1 ? selectedCanvasEdge[0] : null;
600-
601-
if (singleSelectedNode) {
602-
const nextNodeId = getNodeGraphKey(singleSelectedNode);
603-
if (nextNodeId && nextNodeId !== nodeId) {
604-
setQueryState(
605-
{
606-
pane: 'node',
607-
nodeId: nextNodeId,
608-
edgeId: null,
609-
},
610-
{ history: 'replace' }
611-
);
612-
}
613-
return;
614-
}
615-
616-
if (
617-
singleSelectedEdge &&
618-
(singleSelectedEdge.type === EdgeType.A2A || singleSelectedEdge.type === EdgeType.SelfLoop)
619-
) {
620-
const nextEdgeId = getEdgeGraphKey(singleSelectedEdge, nodes);
621-
if (nextEdgeId && nextEdgeId !== edgeId) {
622-
setQueryState(
623-
{
624-
pane: 'edge',
625-
nodeId: null,
626-
edgeId: nextEdgeId,
627-
},
628-
{ history: 'replace' }
629-
);
630-
}
631-
}
632-
}, [edgeId, nodeId, nodes, edges, setQueryState]);
633-
634554
useAnimateGraph();
635555

636556
const onNodeClick: ReactFlowProps['onNodeClick'] = (_, node) => {
@@ -663,9 +583,6 @@ export const Agent: FC<AgentProps> = ({ agent }) => {
663583
isMounted &&
664584
!showEmptyState;
665585

666-
const defaultSubAgentNodeIdRef = useDefaultSubAgentNodeIdRef();
667-
const selectedNode = findNodeByGraphKey(nodes, nodeId);
668-
const selectedEdge = findEdgeByGraphKey(edges, nodes, edgeId);
669586
return (
670587
<ResizablePanelGroup
671588
// Note: Without a specified `id`, Cypress tests may become flaky and fail with the error: `No group found for id '...'`
@@ -725,7 +642,7 @@ export const Agent: FC<AgentProps> = ({ agent }) => {
725642
setNodes(resolveCollisions);
726643
}}
727644
onBeforeDelete={async (state) => {
728-
const defaultSubAgentNodeId = defaultSubAgentNodeIdRef.current;
645+
const defaultSubAgentNodeId = form.getValues('defaultSubAgentNodeId');
729646
const hasDefaultNode = state.nodes.some((node) => node.id === defaultSubAgentNodeId);
730647
if (hasDefaultNode) {
731648
toast.error(`Cannot delete default subagent "${defaultSubAgentNodeId}"`);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { type Edge, type Node, useOnSelectionChange } from '@xyflow/react';
2+
import { useEffect } from 'react';
3+
import { EdgeType } from '@/components/agent/configuration/edge-types';
4+
import {
5+
findEdgeByGraphKey,
6+
findNodeByGraphKey,
7+
getEdgeGraphKey,
8+
getNodeGraphKey,
9+
} from '@/features/agent/domain';
10+
import { useAgentActions } from '@/features/agent/state/use-agent-store';
11+
import { useSidePane } from '@/hooks/use-side-pane';
12+
13+
interface UseAgentSelectionSyncParams {
14+
nodes: Node[];
15+
edges: Edge[];
16+
isOpen: boolean;
17+
nodeId: string | null;
18+
edgeId: string | null;
19+
}
20+
21+
export function useAgentSelectionSync({
22+
nodes,
23+
edges,
24+
isOpen,
25+
nodeId,
26+
edgeId,
27+
}: UseAgentSelectionSyncParams) {
28+
'use memo';
29+
const { setNodes, setEdges } = useAgentActions();
30+
const { setQueryState } = useSidePane();
31+
32+
useOnSelectionChange({
33+
onChange({ nodes: selectedNodes, edges: selectedEdges }) {
34+
const node = selectedNodes.length === 1 ? selectedNodes[0] : null;
35+
const edge =
36+
selectedEdges.length === 1 &&
37+
(selectedEdges[0]?.type === EdgeType.A2A || selectedEdges[0]?.type === EdgeType.SelfLoop)
38+
? selectedEdges[0]
39+
: null;
40+
const defaultPane = isOpen ? 'agent' : null;
41+
42+
setQueryState(
43+
{
44+
pane: node ? 'node' : edge ? 'edge' : defaultPane,
45+
nodeId: node ? getNodeGraphKey(node) : null,
46+
edgeId: edge ? getEdgeGraphKey(edge, selectedNodes) : null,
47+
},
48+
{ history: 'replace' }
49+
);
50+
},
51+
});
52+
53+
useEffect(() => {
54+
const selectedCanvasNode = nodes.filter((node) => node.selected);
55+
const selectedCanvasEdge = edges.filter((edge) => edge.selected);
56+
const singleSelectedNode = selectedCanvasNode.length === 1 ? selectedCanvasNode[0] : null;
57+
const singleSelectedEdge = selectedCanvasEdge.length === 1 ? selectedCanvasEdge[0] : null;
58+
59+
if (singleSelectedNode) {
60+
const nextNodeId = getNodeGraphKey(singleSelectedNode);
61+
if (nextNodeId && nextNodeId !== nodeId) {
62+
setQueryState(
63+
{
64+
pane: 'node',
65+
nodeId: nextNodeId,
66+
edgeId: null,
67+
},
68+
{ history: 'replace' }
69+
);
70+
}
71+
return;
72+
}
73+
74+
if (
75+
singleSelectedEdge &&
76+
(singleSelectedEdge.type === EdgeType.A2A || singleSelectedEdge.type === EdgeType.SelfLoop)
77+
) {
78+
const nextEdgeId = getEdgeGraphKey(singleSelectedEdge, nodes);
79+
if (nextEdgeId && nextEdgeId !== edgeId) {
80+
setQueryState(
81+
{
82+
pane: 'edge',
83+
nodeId: null,
84+
edgeId: nextEdgeId,
85+
},
86+
{ history: 'replace' }
87+
);
88+
}
89+
}
90+
}, [edgeId, nodeId, nodes, edges]);
91+
92+
function clearCanvasSelection() {
93+
setEdges((prevEdges) => prevEdges.map((edge) => ({ ...edge, selected: false })));
94+
setNodes((prevNodes) => prevNodes.map((node) => ({ ...node, selected: false })));
95+
}
96+
97+
return {
98+
selectedNode: findNodeByGraphKey(nodes, nodeId),
99+
selectedEdge: findEdgeByGraphKey(edges, nodes, edgeId),
100+
closeSidePane() {
101+
clearCanvasSelection();
102+
setQueryState({
103+
pane: null,
104+
nodeId: null,
105+
edgeId: null,
106+
});
107+
},
108+
backToAgent() {
109+
clearCanvasSelection();
110+
setQueryState({
111+
pane: 'agent',
112+
nodeId: null,
113+
edgeId: null,
114+
});
115+
},
116+
};
117+
}

agents-manage-ui/src/components/agent/use-animate-graph.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
NodeType,
1111
setNodeStatus,
1212
} from '@/components/agent/configuration/node-types';
13-
import { useDefaultSubAgentNodeIdRef } from '@/components/agent/use-default-sub-agent-id-ref';
1413
import { useFullAgentFormContext } from '@/contexts/full-agent-form';
1514
import {
1615
findSubAgentNodeId,
@@ -24,7 +23,6 @@ import { sentry } from '@/lib/sentry';
2423

2524
export function useAnimateGraph(): void {
2625
const form = useFullAgentFormContext();
27-
const defaultSubAgentNodeIdRef = useDefaultSubAgentNodeIdRef();
2826

2927
// biome-ignore lint/correctness/useExhaustiveDependencies: subscribe once and read latest form values imperatively
3028
useEffect(() => {
@@ -37,6 +35,8 @@ export function useAnimateGraph(): void {
3735
return;
3836
}
3937

38+
const defaultSubAgentNodeId = form.getValues('defaultSubAgentNodeId');
39+
4040
agentStore.setState((state) => {
4141
const { edges: prevEdges, nodes: prevNodes } = state;
4242
const subAgentFormData = form.getValues('subAgents');
@@ -103,7 +103,7 @@ export function useAnimateGraph(): void {
103103
if (data?.details?.agentId !== /* agentId */ location.pathname.split('/')[5]) {
104104
return null;
105105
}
106-
if (node.id === defaultSubAgentNodeIdRef.current) {
106+
if (node.id === defaultSubAgentNodeId) {
107107
return 'delegating';
108108
}
109109
return getCurrentStatus(node);

agents-manage-ui/src/components/agent/use-default-sub-agent-id-ref.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

biome.jsonc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,14 @@
123123
{ "name": "useAgentActions", "stableResult": true },
124124
{ "name": "useMonacoActions", "stableResult": true },
125125
{ "name": "useProjectActions", "stableResult": true },
126-
{ "name": "useDefaultSubAgentNodeIdRef", "stableResult": true },
127126
{ "name": "useTheme", "stableResult": ["setTheme"] },
128127
{
129128
"name": "useCopilotContext",
130129
"stableResult": ["setIsOpen", "setIsStreaming", "openCopilot", "setDynamicHeaders"]
130+
},
131+
{
132+
"name": "useSidePane",
133+
"stableResult": ["setQueryState", "openAgentPane"]
131134
}
132135
]
133136
}

0 commit comments

Comments
 (0)