diff --git a/frontend/src/components/Flow/FlowEditor.tsx b/frontend/src/components/Flow/FlowEditor.tsx
index 879a732..b9dd994 100644
--- a/frontend/src/components/Flow/FlowEditor.tsx
+++ b/frontend/src/components/Flow/FlowEditor.tsx
@@ -50,6 +50,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
+import { FluksioLoader } from "@/components/ui/fluksio-loader"
import useAuth from "@/hooks/useAuth"
import useCustomToast from "@/hooks/useCustomToast"
import { useIsMobile } from "@/hooks/useMobile"
@@ -110,10 +111,10 @@ const nodeTypes = { flow: FlowNode, [ENDPOINT_TYPE]: EndpointNode }
* Where every node sits and how every edge runs, once the engine has said so.
*
* The layout is a chunk of its own and answers asynchronously, so a canvas has
- * a moment before it knows where anything goes. It draws nothing at all until
- * then — a route already shows a skeleton for that — and an *edit* keeps the
- * arrangement it had, so only the node that changed lands a frame late rather
- * than the whole graph flickering through the corner.
+ * a moment before it knows where anything goes. It draws no node until then,
+ * only a loader, and an *edit* keeps the arrangement it had, so only the node
+ * that changed lands a frame late rather than the whole graph flickering
+ * through the corner.
*/
function useLayout(
ids: string[],
@@ -1565,6 +1566,17 @@ function FlowEditorInner({
/>
+ {/* On a slow link the layout engine arrives well after the document,
+ and a bare canvas until then reads as a flow with no nodes. */}
+ {definitions.length > 0 && !positions ? (
+
+
+
+ ) : null}
+
{definitions.length === 0 ? (
{/* Where the node lives: the flow is the namespace of its
messages, so it reads like the folder holding them. */}
-
+
/{flow}/
)
diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx
index 3c98759..c1c24e9 100644
--- a/frontend/src/components/ui/select.tsx
+++ b/frontend/src/components/ui/select.tsx
@@ -60,7 +60,7 @@ function SelectContent({