@burdenoff/fluidgrids-fe-sdk 2026.1111.12 → 2026.1111.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,7 @@ import { Link, useNavigate, useParams } from "react-router";
21
21
  import { useMutation, useQuery } from "@apollo/client/react";
22
22
  import { gql } from "@apollo/client/core";
23
23
  import { create } from "zustand";
24
- import { Background, Controls, EdgeLabelRenderer, Handle, MarkerType, MiniMap, Panel, Position, ReactFlow, ReactFlowProvider, addEdge, getBezierPath, useEdgesState, useNodes, useNodesState, useOnSelectionChange, useReactFlow, useStore } from "@xyflow/react";
24
+ import { Background, Controls, EdgeLabelRenderer, Handle, MarkerType, MiniMap, NodeToolbar, Panel, Position, ReactFlow, ReactFlowProvider, ViewportPortal, addEdge, getBezierPath, useEdgesState, useInternalNode, useNodes, useNodesState, useOnSelectionChange, useReactFlow, useStore } from "@xyflow/react";
25
25
  import { Icon } from "@iconify/react";
26
26
  import ReactMarkdown from "react-markdown";
27
27
  import { cn } from "@burdenoff/fe-libs/utils";
@@ -10582,7 +10582,7 @@ function buildNodeCheck(requiredNodeTypes, installedAppIds) {
10582
10582
  }
10583
10583
 
10584
10584
  // src/hooks/store/useTemplateImportFlow.tsx
10585
- const LazyTemplateConfigurationModal = lazy(() => import("./TemplateConfigurationModal-BimyvVH6.js").then((m) => ({ default: m.TemplateConfigurationModal })));
10585
+ const LazyTemplateConfigurationModal = lazy(() => import("./TemplateConfigurationModal-ChQsXf0-.js").then((m) => ({ default: m.TemplateConfigurationModal })));
10586
10586
  /**
10587
10587
  * "Use this template", the same everywhere it is offered: the builder's
10588
10588
  * template panel, the Templates page and the template preview page
@@ -12759,7 +12759,7 @@ function TemplatesGrid({ templates, loading, hasMore, onLoadMore, onTemplateClic
12759
12759
  }
12760
12760
 
12761
12761
  // src/builder/components/flow-builder/components/templates/TemplatesModal.tsx
12762
- const LazyTemplateDetailPanel = lazy(() => import("./TemplateDetailPanel-CkwB6vrW.js").then((m) => ({ default: m.TemplateDetailPanel })));
12762
+ const LazyTemplateDetailPanel = lazy(() => import("./TemplateDetailPanel-C0YiVdub.js").then((m) => ({ default: m.TemplateDetailPanel })));
12763
12763
  /**
12764
12764
  * Modal for browsing and selecting workflow templates
12765
12765
  * Features:
@@ -20702,6 +20702,448 @@ function WebhookRunFollower() {
20702
20702
  return null;
20703
20703
  }
20704
20704
 
20705
+ // src/agentMarks/plainText.ts
20706
+ /**
20707
+ * ★ BOFF-11584: the text of a callout or note is model output, and the model
20708
+ * has read workflow content people wrote. It is shown as plain text only:
20709
+ * React renders it as a text node (never `dangerouslySetInnerHTML`, never a
20710
+ * markdown renderer), so markup, links and images in it stay inert characters.
20711
+ *
20712
+ * This keeps it short and printable as well: control characters (other than
20713
+ * line breaks) and bidirectional controls, which can disguise what a line
20714
+ * says, are removed; runs of blank lines collapse; the result is clamped.
20715
+ */
20716
+ const BIDI_CONTROLS = /* @__PURE__ */ new Set([
20717
+ 1564,
20718
+ 8206,
20719
+ 8207,
20720
+ 8234,
20721
+ 8235,
20722
+ 8236,
20723
+ 8237,
20724
+ 8238,
20725
+ 8294,
20726
+ 8295,
20727
+ 8296,
20728
+ 8297
20729
+ ]);
20730
+ function isDroppedCharacter(code) {
20731
+ if (code === 10) return false;
20732
+ if (code <= 31) return true;
20733
+ if (code >= 127 && code <= 159) return true;
20734
+ return BIDI_CONTROLS.has(code);
20735
+ }
20736
+ function toPlainText(text, max) {
20737
+ let kept = "";
20738
+ for (const character of text.replace(/\r\n?/g, "\n").replace(/\t/g, " ")) if (!isDroppedCharacter(character.codePointAt(0) ?? 0)) kept += character;
20739
+ const cleaned = kept.replace(/ {2,}/g, " ").replace(/ *\n */g, "\n").replace(/\n{3,}/g, "\n\n").trim();
20740
+ if (cleaned.length <= max) return cleaned;
20741
+ return `${cleaned.slice(0, max - 1).trimEnd()}…`;
20742
+ }
20743
+
20744
+ // src/agentMarks/marksStore.ts
20745
+ /** The text of a mark is model output: kept as plain text, never markup. */
20746
+ const CALLOUT_LABEL_MAX = 200;
20747
+ const NOTE_MESSAGE_MAX = 280;
20748
+ /** A workflow holds at most this many notes, resolved ones included. */
20749
+ const MAX_NOTES_PER_WORKFLOW = 20;
20750
+ let sequence = 0;
20751
+ const nextId = (prefix) => {
20752
+ sequence += 1;
20753
+ return `${prefix}${sequence}`;
20754
+ };
20755
+ const useAgentMarksStore = create()((set, get) => ({
20756
+ callouts: [],
20757
+ notes: [],
20758
+ nextNumber: {},
20759
+ openNoteId: null,
20760
+ flash: null,
20761
+ actions: {
20762
+ addCallout: ({ label, target, element, route }) => {
20763
+ const callout = {
20764
+ id: nextId("c"),
20765
+ label: toPlainText(label, 200),
20766
+ target,
20767
+ route,
20768
+ element: element ? new WeakRef(element) : null
20769
+ };
20770
+ set((state) => ({ callouts: [...state.callouts, callout].slice(-3) }));
20771
+ return callout;
20772
+ },
20773
+ rebindCallout: (id, element) => set((state) => ({ callouts: state.callouts.map((callout) => callout.id === id ? {
20774
+ ...callout,
20775
+ element: new WeakRef(element)
20776
+ } : callout) })),
20777
+ removeCallout: (id) => set((state) => ({ callouts: state.callouts.filter((callout) => callout.id !== id) })),
20778
+ clearCallouts: () => {
20779
+ const count = get().callouts.length;
20780
+ if (count > 0) set({ callouts: [] });
20781
+ return count;
20782
+ },
20783
+ dropCalloutsOffRoute: (route) => {
20784
+ const { callouts } = get();
20785
+ const kept = callouts.filter((callout) => callout.route === route);
20786
+ if (kept.length !== callouts.length) set({ callouts: kept });
20787
+ return callouts.length - kept.length;
20788
+ },
20789
+ addNote: (input) => {
20790
+ const { notes, nextNumber } = get();
20791
+ if (notes.filter((note) => note.workflowKey === input.workflowKey).length >= 20) return null;
20792
+ const number = nextNumber[input.workflowKey] ?? 1;
20793
+ const note = {
20794
+ id: nextId("n"),
20795
+ number,
20796
+ workflowKey: input.workflowKey,
20797
+ nodeId: input.nodeId,
20798
+ nodeLabel: toPlainText(input.nodeLabel, 80),
20799
+ paramKey: input.paramKey ?? null,
20800
+ kind: input.kind,
20801
+ message: toPlainText(input.message, 280),
20802
+ resolved: false
20803
+ };
20804
+ set({
20805
+ notes: [...notes, note],
20806
+ nextNumber: {
20807
+ ...nextNumber,
20808
+ [input.workflowKey]: number + 1
20809
+ }
20810
+ });
20811
+ return note;
20812
+ },
20813
+ setResolved: (id, resolved) => set((state) => ({ notes: state.notes.map((note) => note.id === id ? {
20814
+ ...note,
20815
+ resolved
20816
+ } : note) })),
20817
+ removeNote: (id) => set((state) => ({
20818
+ notes: state.notes.filter((note) => note.id !== id),
20819
+ openNoteId: state.openNoteId === id ? null : state.openNoteId
20820
+ })),
20821
+ clearNotes: (workflowKey) => {
20822
+ const { notes, openNoteId } = get();
20823
+ const kept = workflowKey === void 0 ? [] : notes.filter((note) => note.workflowKey !== workflowKey);
20824
+ const removed = notes.length - kept.length;
20825
+ if (removed > 0) set({
20826
+ notes: kept,
20827
+ openNoteId: kept.some((note) => note.id === openNoteId) ? openNoteId : null
20828
+ });
20829
+ return removed;
20830
+ },
20831
+ openNote: (id) => set({ openNoteId: id }),
20832
+ flashNode: (nodeId) => {
20833
+ sequence += 1;
20834
+ set({ flash: {
20835
+ nodeId,
20836
+ token: sequence
20837
+ } });
20838
+ }
20839
+ }
20840
+ }));
20841
+ /** Reset everything (tests, and sign-out if a host wants it). */
20842
+ function resetAgentMarks() {
20843
+ useAgentMarksStore.setState({
20844
+ callouts: [],
20845
+ notes: [],
20846
+ nextNumber: {},
20847
+ openNoteId: null,
20848
+ flash: null
20849
+ });
20850
+ }
20851
+
20852
+ // src/builder/agentNotes/notesKey.ts
20853
+ /**
20854
+ * ★ BOFF-11584: which workflow a canvas note belongs to. A saved workflow is
20855
+ * known by its key across versions; a new one has no key until its first
20856
+ * save, so its in-memory id stands in.
20857
+ */
20858
+ function notesKeyOf(workflow) {
20859
+ return workflow.workflowKey || workflow.id || "unsaved";
20860
+ }
20861
+
20862
+ // src/builder/agentNotes/CanvasNotesLayer.tsx
20863
+ /**
20864
+ * ★ BOFF-11584: the assistant's notes on the builder canvas
20865
+ * (`fg_canvas_annotate`). Rendered as a child of `<ReactFlow>`, outside every
20866
+ * node component: the nodes know nothing about notes.
20867
+ *
20868
+ * - Numbered pins sit above their node (`NodeToolbar`), so they follow it
20869
+ * through drags, pans and zooms. A pin opens the note's card: the message,
20870
+ * the parameter it is about, "Open node" and "Mark resolved".
20871
+ * - A dashed ring marks each node with an open note (`ViewportPortal`, in
20872
+ * canvas coordinates), and a brief ring shows the node the assistant just
20873
+ * brought into view.
20874
+ * - The notes list (top left; the FluidGrids mark holds the top right)
20875
+ * holds every note, including orphaned ones whose node was deleted: those
20876
+ * are never moved onto another node.
20877
+ *
20878
+ * Everything here is `data-agent="deny"`: an agent cannot read or press its
20879
+ * own notes. Messages render as React text, never as markup.
20880
+ */
20881
+ const FLASH_MS = 1800;
20882
+ const RING_PAD = 8;
20883
+ function pinClass(note, open) {
20884
+ return cn("flex size-6 items-center justify-center rounded-full text-xs font-semibold shadow-md transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", note.resolved ? "bg-muted text-muted-foreground" : note.kind === "fix" ? "bg-destructive text-destructive-foreground" : "bg-primary text-primary-foreground", open && "scale-110 ring-2 ring-ring ring-offset-1 ring-offset-background");
20885
+ }
20886
+ /** A ring around a node, drawn in canvas coordinates. */
20887
+ const NodeRing = ({ nodeId, tone }) => {
20888
+ const node = useInternalNode(nodeId);
20889
+ const width = node?.measured.width ?? 0;
20890
+ const height = node?.measured.height ?? 0;
20891
+ if (!node || width === 0 || height === 0) return null;
20892
+ const { x, y } = node.internals.positionAbsolute;
20893
+ return /* @__PURE__ */ jsx("div", {
20894
+ "aria-hidden": "true",
20895
+ "data-testid": `fg-canvas-note-ring-${tone}`,
20896
+ className: cn("pointer-events-none absolute left-0 top-0 rounded-2xl border-2", tone === "fix" && "border-dashed border-destructive", tone === "info" && "border-dashed border-primary", tone === "flash" && "border-primary ring-4 ring-primary/30 motion-safe:animate-pulse"),
20897
+ style: {
20898
+ transform: `translate(${x - RING_PAD}px, ${y - RING_PAD}px)`,
20899
+ width: width + 16,
20900
+ height: height + 16
20901
+ }
20902
+ });
20903
+ };
20904
+ /** The node the assistant just brought into view, for a moment. */
20905
+ const FlashRing = () => {
20906
+ const flash = useAgentMarksStore((state) => state.flash);
20907
+ const [shown, setShown] = useState(null);
20908
+ useEffect(() => {
20909
+ if (!flash) return;
20910
+ setShown(flash.token);
20911
+ const timer = setTimeout(() => setShown(null), FLASH_MS);
20912
+ return () => clearTimeout(timer);
20913
+ }, [flash]);
20914
+ if (!flash || shown !== flash.token) return null;
20915
+ return /* @__PURE__ */ jsx(NodeRing, {
20916
+ nodeId: flash.nodeId,
20917
+ tone: "flash"
20918
+ });
20919
+ };
20920
+ const CanvasNoteCard = ({ note, nodeOnCanvas }) => {
20921
+ const tr = useTr();
20922
+ const { openNote, setResolved } = useAgentMarksStore((state) => state.actions);
20923
+ const openNodePanel = () => {
20924
+ const { workflow, actions } = useFlowStore.getState();
20925
+ const node = workflow.nodes.find((candidate) => candidate.id === note.nodeId);
20926
+ if (!node?.type) return;
20927
+ actions.ndv.open({
20928
+ nodeId: node.id,
20929
+ type: node.type
20930
+ });
20931
+ openNote(null);
20932
+ };
20933
+ return /* @__PURE__ */ jsxs("div", {
20934
+ "data-testid": "fg-canvas-note-card",
20935
+ className: "nowheel w-64 cursor-default rounded-lg border border-border bg-popover p-3 text-left text-xs text-foreground shadow-lg",
20936
+ children: [
20937
+ /* @__PURE__ */ jsxs("div", {
20938
+ className: "mb-1.5 flex items-center justify-between gap-2",
20939
+ children: [/* @__PURE__ */ jsxs("span", {
20940
+ className: "flex items-center gap-1.5 font-semibold",
20941
+ children: [/* @__PURE__ */ jsx(Sparkles, {
20942
+ "aria-hidden": "true",
20943
+ className: "size-3.5 text-primary"
20944
+ }), note.kind === "fix" ? tr("fluidgrids.agentMarks.noteFixTitle", "Note {{number}} · To fix", { number: note.number }) : tr("fluidgrids.agentMarks.noteInfoTitle", "Note {{number}}", { number: note.number })]
20945
+ }), /* @__PURE__ */ jsx("button", {
20946
+ type: "button",
20947
+ "aria-label": tr("fluidgrids.agentMarks.closeNote", "Close note"),
20948
+ onClick: () => openNote(null),
20949
+ className: "-m-1 rounded p-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
20950
+ children: /* @__PURE__ */ jsx(X, {
20951
+ "aria-hidden": "true",
20952
+ className: "size-3.5"
20953
+ })
20954
+ })]
20955
+ }),
20956
+ /* @__PURE__ */ jsxs("p", {
20957
+ className: "mb-1 truncate text-muted-foreground",
20958
+ children: [note.nodeLabel, note.paramKey ? ` · ${tr("fluidgrids.agentMarks.noteParam", "Field: {{param}}", { param: note.paramKey })}` : ""]
20959
+ }),
20960
+ /* @__PURE__ */ jsx("p", {
20961
+ "data-testid": "fg-canvas-note-message",
20962
+ className: "whitespace-pre-line break-words text-sm text-foreground",
20963
+ children: note.message
20964
+ }),
20965
+ !nodeOnCanvas && /* @__PURE__ */ jsx("p", {
20966
+ className: "mt-1.5 text-muted-foreground",
20967
+ children: tr("fluidgrids.agentMarks.noteOrphaned", "This node was removed from the workflow.")
20968
+ }),
20969
+ /* @__PURE__ */ jsxs("div", {
20970
+ className: "mt-2.5 flex justify-end gap-2",
20971
+ children: [nodeOnCanvas && /* @__PURE__ */ jsx("button", {
20972
+ type: "button",
20973
+ onClick: openNodePanel,
20974
+ className: "rounded-md border border-border px-2 py-1 font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
20975
+ children: tr("fluidgrids.agentMarks.openNode", "Open node")
20976
+ }), /* @__PURE__ */ jsx("button", {
20977
+ type: "button",
20978
+ "data-testid": "fg-canvas-note-resolve",
20979
+ onClick: () => setResolved(note.id, !note.resolved),
20980
+ className: "rounded-md bg-primary px-2 py-1 font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
20981
+ children: note.resolved ? tr("fluidgrids.agentMarks.reopenNote", "Reopen") : tr("fluidgrids.agentMarks.resolveNote", "Mark resolved")
20982
+ })]
20983
+ })
20984
+ ]
20985
+ });
20986
+ };
20987
+ /** The pins above one node, and the open card. */
20988
+ const NodePins = ({ nodeId, notes }) => {
20989
+ const tr = useTr();
20990
+ const openNoteId = useAgentMarksStore((state) => state.openNoteId);
20991
+ const openNote = useAgentMarksStore((state) => state.actions.openNote);
20992
+ const open = notes.find((note) => note.id === openNoteId);
20993
+ return /* @__PURE__ */ jsx(NodeToolbar, {
20994
+ nodeId,
20995
+ isVisible: true,
20996
+ position: Position.Top,
20997
+ align: "end",
20998
+ offset: 6,
20999
+ "data-agent": "deny",
21000
+ "data-testid": "fg-canvas-note-pins",
21001
+ className: "nodrag nopan",
21002
+ children: /* @__PURE__ */ jsxs("div", {
21003
+ className: "flex flex-col items-end gap-1.5",
21004
+ children: [open && /* @__PURE__ */ jsx(CanvasNoteCard, {
21005
+ note: open,
21006
+ nodeOnCanvas: true
21007
+ }), /* @__PURE__ */ jsx("div", {
21008
+ className: "flex gap-1",
21009
+ children: notes.map((note) => /* @__PURE__ */ jsx("button", {
21010
+ type: "button",
21011
+ "data-testid": "fg-canvas-note-pin",
21012
+ "aria-expanded": note.id === openNoteId,
21013
+ "aria-label": tr("fluidgrids.agentMarks.notePin", "Assistant note {{number}}", { number: note.number }),
21014
+ onClick: () => openNote(note.id === openNoteId ? null : note.id),
21015
+ className: pinClass(note, note.id === openNoteId),
21016
+ children: note.resolved ? /* @__PURE__ */ jsx(Check, {
21017
+ "aria-hidden": "true",
21018
+ className: "size-3.5"
21019
+ }) : note.number
21020
+ }, note.id))
21021
+ })]
21022
+ })
21023
+ });
21024
+ };
21025
+ /** Every note on this workflow, orphaned ones included. */
21026
+ const NotesList = ({ notes, live }) => {
21027
+ const tr = useTr();
21028
+ const [expanded, setExpanded] = useState(false);
21029
+ const { fitView } = useReactFlow();
21030
+ const { openNote, removeNote, flashNode } = useAgentMarksStore((state) => state.actions);
21031
+ const openCount = notes.filter((note) => !note.resolved).length;
21032
+ const orphaned = notes.filter((note) => !live.has(note.nodeId)).length;
21033
+ const show = (note) => {
21034
+ if (!live.has(note.nodeId)) return;
21035
+ fitView({
21036
+ nodes: [{ id: note.nodeId }],
21037
+ duration: 400,
21038
+ padding: .6,
21039
+ maxZoom: 1.25
21040
+ }).then((done) => {
21041
+ if (done) flashNode(note.nodeId);
21042
+ });
21043
+ openNote(note.id);
21044
+ };
21045
+ return /* @__PURE__ */ jsx(Panel, {
21046
+ position: "top-left",
21047
+ "data-agent": "deny",
21048
+ "data-testid": "fg-canvas-notes-list",
21049
+ className: "nodrag nopan nowheel",
21050
+ children: /* @__PURE__ */ jsxs("div", {
21051
+ className: "w-64 rounded-lg border border-border bg-popover text-xs text-foreground shadow-md",
21052
+ children: [/* @__PURE__ */ jsxs("button", {
21053
+ type: "button",
21054
+ "aria-expanded": expanded,
21055
+ onClick: () => setExpanded((value) => !value),
21056
+ className: "flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
21057
+ children: [/* @__PURE__ */ jsxs("span", {
21058
+ className: "flex items-center gap-1.5",
21059
+ children: [/* @__PURE__ */ jsx(Sparkles, {
21060
+ "aria-hidden": "true",
21061
+ className: "size-3.5 text-primary"
21062
+ }), tr("fluidgrids.agentMarks.notesHeading", "Assistant notes")]
21063
+ }), /* @__PURE__ */ jsxs("span", {
21064
+ className: "text-muted-foreground",
21065
+ children: [tr("fluidgrids.agentMarks.notesOpenCount", "{{count}} open", { count: openCount }), orphaned > 0 ? ` · ${tr("fluidgrids.agentMarks.notesOrphanedCount", "{{count}} removed", { count: orphaned })}` : ""]
21066
+ })]
21067
+ }), expanded && /* @__PURE__ */ jsx("ul", {
21068
+ className: "max-h-64 overflow-y-auto border-t border-border py-1",
21069
+ children: notes.map((note) => {
21070
+ const onCanvas = live.has(note.nodeId);
21071
+ return /* @__PURE__ */ jsxs("li", {
21072
+ "data-testid": "fg-canvas-notes-item",
21073
+ className: "flex items-start gap-2 px-3 py-1.5",
21074
+ children: [
21075
+ /* @__PURE__ */ jsx("span", {
21076
+ className: cn(pinClass(note, false), "size-5 shrink-0"),
21077
+ children: note.resolved ? /* @__PURE__ */ jsx(Check, {
21078
+ "aria-hidden": "true",
21079
+ className: "size-3"
21080
+ }) : note.number
21081
+ }),
21082
+ /* @__PURE__ */ jsxs("button", {
21083
+ type: "button",
21084
+ disabled: !onCanvas,
21085
+ onClick: () => show(note),
21086
+ className: "min-w-0 flex-1 text-left disabled:cursor-default",
21087
+ children: [/* @__PURE__ */ jsx("span", {
21088
+ className: "block truncate font-medium",
21089
+ children: note.nodeLabel
21090
+ }), /* @__PURE__ */ jsx("span", {
21091
+ className: cn("block truncate text-muted-foreground", note.resolved && "line-through"),
21092
+ children: onCanvas ? note.message : tr("fluidgrids.agentMarks.noteOrphaned", "This node was removed from the workflow.")
21093
+ })]
21094
+ }),
21095
+ !onCanvas && /* @__PURE__ */ jsx("button", {
21096
+ type: "button",
21097
+ "aria-label": tr("fluidgrids.agentMarks.removeNote", "Remove note"),
21098
+ onClick: () => removeNote(note.id),
21099
+ className: "shrink-0 rounded p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
21100
+ children: /* @__PURE__ */ jsx(X, {
21101
+ "aria-hidden": "true",
21102
+ className: "size-3.5"
21103
+ })
21104
+ })
21105
+ ]
21106
+ }, note.id);
21107
+ })
21108
+ })]
21109
+ })
21110
+ });
21111
+ };
21112
+ const CanvasNotesLayer = () => {
21113
+ const workflowKey = useFlowStore((state) => notesKeyOf(state.workflow));
21114
+ const nodeIds = useFlowStore(useShallow((state) => state.workflow.nodes.map((node) => node.id)));
21115
+ const allNotes = useAgentMarksStore((state) => state.notes);
21116
+ const notes = useMemo(() => allNotes.filter((note) => note.workflowKey === workflowKey), [allNotes, workflowKey]);
21117
+ const live = useMemo(() => new Set(nodeIds), [nodeIds]);
21118
+ const byNode = useMemo(() => {
21119
+ const groups = /* @__PURE__ */ new Map();
21120
+ for (const note of notes) {
21121
+ if (!live.has(note.nodeId)) continue;
21122
+ groups.set(note.nodeId, [...groups.get(note.nodeId) ?? [], note]);
21123
+ }
21124
+ return [...groups.entries()];
21125
+ }, [notes, live]);
21126
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
21127
+ /* @__PURE__ */ jsxs(ViewportPortal, { children: [byNode.map(([nodeId, nodeNotes]) => {
21128
+ const openNotes = nodeNotes.filter((note) => !note.resolved);
21129
+ if (openNotes.length === 0) return null;
21130
+ const tone = openNotes.some((note) => note.kind === "fix") ? "fix" : "info";
21131
+ return /* @__PURE__ */ jsx(NodeRing, {
21132
+ nodeId,
21133
+ tone
21134
+ }, nodeId);
21135
+ }), /* @__PURE__ */ jsx(FlashRing, {})] }),
21136
+ byNode.map(([nodeId, nodeNotes]) => /* @__PURE__ */ jsx(NodePins, {
21137
+ nodeId,
21138
+ notes: nodeNotes
21139
+ }, nodeId)),
21140
+ notes.length > 0 && /* @__PURE__ */ jsx(NotesList, {
21141
+ notes,
21142
+ live
21143
+ })
21144
+ ] });
21145
+ };
21146
+
20705
21147
  // src/builder/lib/autosave-runner.ts
20706
21148
  function createAutosaveRunner(deps) {
20707
21149
  let timer;
@@ -27143,8 +27585,8 @@ function EditLeaseBanner() {
27143
27585
  }
27144
27586
 
27145
27587
  // src/builder/components/flow-builder/flow-builder.tsx
27146
- const LazyNodeDetailView = lazy(() => import("./node-detail-view-DRk8Ln5L.js").then((m) => ({ default: m.NodeDetailView })));
27147
- const LazyQuickConfigPanel = lazy(() => import("./quick-config-panel-40_1utiL.js").then((m) => ({ default: m.QuickConfigPanel })));
27588
+ const LazyNodeDetailView = lazy(() => import("./node-detail-view-CPD_NSZc.js").then((m) => ({ default: m.NodeDetailView })));
27589
+ const LazyQuickConfigPanel = lazy(() => import("./quick-config-panel-DHkYGaCL.js").then((m) => ({ default: m.QuickConfigPanel })));
27148
27590
  const edgeTypes = { deletable: CustomDeletableEdge };
27149
27591
  const edgeOptions = {
27150
27592
  type: "deletable",
@@ -27204,7 +27646,7 @@ const FlowBuilder = ({ readOnly = false }) => {
27204
27646
  const [agentTools, setAgentTools] = useState([]);
27205
27647
  useEffect(() => {
27206
27648
  let cancelled = false;
27207
- import("./agentTools-Dgrdql-W.js").then(({ createBuilderAgentTools }) => {
27649
+ import("./agentTools-C0g2OWvI.js").then(({ createBuilderAgentTools }) => {
27208
27650
  if (cancelled) return;
27209
27651
  setAgentTools(createBuilderAgentTools({ durableFunctionsEnabled: () => paletteGatesRef.current }));
27210
27652
  }).catch((error) => {
@@ -27251,7 +27693,30 @@ const FlowBuilder = ({ readOnly = false }) => {
27251
27693
  }
27252
27694
  return merged;
27253
27695
  }, [nodeTypesVersion, nodes]);
27254
- const { getNodes } = useReactFlow();
27696
+ const { getNodes, fitView } = useReactFlow();
27697
+ const fitViewRef = useRef(fitView);
27698
+ useEffect(() => {
27699
+ fitViewRef.current = fitView;
27700
+ }, [fitView]);
27701
+ const [noteTools, setNoteTools] = useState([]);
27702
+ useEffect(() => {
27703
+ let cancelled = false;
27704
+ import("./canvasNoteTools-BUWY18XK.js").then(({ createCanvasNoteTools }) => {
27705
+ if (cancelled) return;
27706
+ setNoteTools(createCanvasNoteTools({ focusNode: (nodeId) => fitViewRef.current({
27707
+ nodes: [{ id: nodeId }],
27708
+ duration: 400,
27709
+ padding: .6,
27710
+ maxZoom: 1.25
27711
+ }) }));
27712
+ }).catch((error) => {
27713
+ console.error("[FlowBuilder] Assistant note tools failed to load:", error);
27714
+ });
27715
+ return () => {
27716
+ cancelled = true;
27717
+ };
27718
+ }, []);
27719
+ useAgentTools(noteTools);
27255
27720
  const { handleOnEdgeDropConnectEnd, floatingMenuWrapperRef, handleAddConnectedNode } = useAddNodeOnEdgeDrop();
27256
27721
  const deleteKeyCode = useDeleteKeyCode();
27257
27722
  useNodeSelectedEvent();
@@ -27321,7 +27786,7 @@ const FlowBuilder = ({ readOnly = false }) => {
27321
27786
  className: "relative flex-1 rounded-b-xl border border-card-foreground/10 border-t-0 overflow-hidden mx-4 mb-1",
27322
27787
  children: [
27323
27788
  /* @__PURE__ */ jsx(CanvasControls, {}),
27324
- /* @__PURE__ */ jsx(ReactFlow, {
27789
+ /* @__PURE__ */ jsxs(ReactFlow, {
27325
27790
  proOptions: { hideAttribution: true },
27326
27791
  onInit: ({ fitView }) => fitView({
27327
27792
  minZoom: 1,
@@ -27354,7 +27819,7 @@ const FlowBuilder = ({ readOnly = false }) => {
27354
27819
  maxZoom: 1
27355
27820
  },
27356
27821
  defaultEdgeOptions: edgeOptions,
27357
- children: /* @__PURE__ */ jsx(Background, { gap: 24 })
27822
+ children: [/* @__PURE__ */ jsx(Background, { gap: 24 }), /* @__PURE__ */ jsx(CanvasNotesLayer, {})]
27358
27823
  }),
27359
27824
  /* @__PURE__ */ jsx(FluidGridsMark, {}),
27360
27825
  nodes.length === 0 && !editingBlocked && /* @__PURE__ */ jsx("div", {
@@ -27544,4 +28009,4 @@ const FlowBuilderPage = ({ workflow, readOnly = false }) => {
27544
28009
  ] });
27545
28010
  };
27546
28011
 
27547
- export { PopoverTrigger$1 as $, DialogTitle$1 as A, NodeEditorTabBody as B, DialogClose as C, isDurableFunctionNodeType as Ct, DialogHeader$1 as D, DialogFooter$1 as E, buildRequiredNodeTypes as F, runnerInstallCommand as G, RunOnSection as H, useImportWorkflowTemplate as I, NodeParamsForm as J, useRunnerActions as K, useWorkflowTemplates as L, computeAttachPosition as M, useTemplateImportFlow as N, DialogOverlay as O, buildNodeCheck as P, PopoverContent$1 as Q, useInstalledNodes as R, Dialog$1 as S, filterPaletteNodes as St, DialogDescription$1 as T, isRunControlNodeType as Tt, RunnerStatusPill as U, NodeSettingsPanel as V, RUNNER_INSTALL_SCRIPT_URL as W, NDV_Z_INDEX as X, useGlobalStoreClient as Y, Popover$1 as Z, TableView as _, CardTitle$1 as _t, useAiWorkflowTemplates as a, SCHEDULE_NODE_TYPE as at, Badge$1 as b, RUN_CONTROL_NODE_TYPES as bt, RunsUsageMeter as c, TabButton as ct, readRunsQuota as d, FluidGridsMark as dt, CodeEditorField as et, runsQuotaLevel as f, Card$1 as ft, JsonView as g, CardHeader$1 as gt, useWorkflowReloadStore as h, CardFooter as ht, enrichDraftNodes as i, useNodeEditorTab as it, DialogTrigger$1 as j, DialogPortal as k, RunsUsageMeterView as l, NodeInputFields as lt, releaseLeaseKeptForReload as m, CardDescription$1 as mt, AI_DRAFT_MAX_PROMPT_LENGTH as n, ConnectionFormDialog as nt, BUILDER_FULLSCREEN_ATTRIBUTE as o, SchedulePanel as ot, summarizeRunsQuota as p, CardContent$1 as pt, useRunners as q, draftToCanvas as r, ReadOnlyFieldset as rt, BUILDER_FULLSCREEN_EVENT as s, NodeEditorTabs as st, FlowBuilderPage as t, ConnectionSelector as tt, useRunsQuota as u, useNodeSelectedEvent as ut, TemplateCard as v, Input$1 as vt, DialogContent$1 as w, isNodeTypeHiddenFromPalette as wt, badgeVariants as x, WORKFLOW_TEST_NODE_TYPES as xt, templatePreview as y, DURABLE_FUNCTION_NODE_TYPES as yt, NodeExecutionActions as z };
28012
+ export { useGlobalStoreClient as $, DialogFooter$1 as A, useImportWorkflowTemplate as B, templatePreview as C, DURABLE_FUNCTION_NODE_TYPES as Ct, DialogClose as D, isDurableFunctionNodeType as Dt, Dialog$1 as E, filterPaletteNodes as Et, DialogTrigger$1 as F, NodeSettingsPanel as G, useInstalledNodes as H, computeAttachPosition as I, RUNNER_INSTALL_SCRIPT_URL as J, RunOnSection as K, useTemplateImportFlow as L, DialogOverlay as M, DialogPortal as N, DialogContent$1 as O, isNodeTypeHiddenFromPalette as Ot, DialogTitle$1 as P, NodeParamsForm as Q, buildNodeCheck as R, TemplateCard as S, Input$1 as St, badgeVariants as T, WORKFLOW_TEST_NODE_TYPES as Tt, NodeExecutionActions as U, useWorkflowTemplates as V, NodeEditorTabBody as W, useRunnerActions as X, runnerInstallCommand as Y, useRunners as Z, resetAgentMarks as _, CardContent$1 as _t, useAiWorkflowTemplates as a, ConnectionSelector as at, JsonView as b, CardHeader$1 as bt, RunsUsageMeter as c, useNodeEditorTab as ct, readRunsQuota as d, NodeEditorTabs as dt, NDV_Z_INDEX as et, runsQuotaLevel as f, TabButton as ft, notesKeyOf as g, Card$1 as gt, useWorkflowReloadStore as h, FluidGridsMark as ht, enrichDraftNodes as i, CodeEditorField as it, DialogHeader$1 as j, DialogDescription$1 as k, isRunControlNodeType as kt, RunsUsageMeterView as l, SCHEDULE_NODE_TYPE as lt, releaseLeaseKeptForReload as m, useNodeSelectedEvent as mt, AI_DRAFT_MAX_PROMPT_LENGTH as n, PopoverContent$1 as nt, BUILDER_FULLSCREEN_ATTRIBUTE as o, ConnectionFormDialog as ot, summarizeRunsQuota as p, NodeInputFields as pt, RunnerStatusPill as q, draftToCanvas as r, PopoverTrigger$1 as rt, BUILDER_FULLSCREEN_EVENT as s, ReadOnlyFieldset as st, FlowBuilderPage as t, Popover$1 as tt, useRunsQuota as u, SchedulePanel as ut, useAgentMarksStore as v, CardDescription$1 as vt, Badge$1 as w, RUN_CONTROL_NODE_TYPES as wt, TableView as x, CardTitle$1 as xt, toPlainText as y, CardFooter as yt, buildRequiredNodeTypes as z };