@cognizant-ai-lab/ui-common 1.8.0 → 1.10.0

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.
Files changed (59) hide show
  1. package/README.md +3 -3
  2. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +9 -3
  3. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +94 -27
  4. package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +7 -0
  5. package/dist/components/AgentChat/ChatCommon/ChatHistory.js +1 -1
  6. package/dist/components/AgentChat/ChatCommon/ControlButtons.d.ts +4 -2
  7. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +8 -2
  8. package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +5 -5
  9. package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +1 -0
  10. package/dist/components/AgentChat/ChatCommon/Thinking.js +2 -2
  11. package/dist/components/AgentChat/Common/Utils.d.ts +0 -7
  12. package/dist/components/AgentChat/Common/Utils.js +0 -12
  13. package/dist/components/Common/Breadcrumbs.js +1 -1
  14. package/dist/components/Common/Footer.js +4 -0
  15. package/dist/components/Common/notification.d.ts +4 -4
  16. package/dist/components/MultiAgentAccelerator/{AgentFlow.d.ts → AgentFlow/AgentFlow.d.ts} +15 -5
  17. package/dist/components/MultiAgentAccelerator/{AgentFlow.js → AgentFlow/AgentFlow.js} +99 -58
  18. package/dist/components/MultiAgentAccelerator/{AgentNode.d.ts → AgentFlow/AgentNode.d.ts} +1 -1
  19. package/dist/components/MultiAgentAccelerator/{AgentNode.js → AgentFlow/AgentNode.js} +11 -9
  20. package/dist/components/MultiAgentAccelerator/{GraphLayouts.d.ts → AgentFlow/GraphLayouts.d.ts} +4 -4
  21. package/dist/components/MultiAgentAccelerator/{GraphLayouts.js → AgentFlow/GraphLayouts.js} +15 -41
  22. package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.d.ts +21 -0
  23. package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.js +27 -0
  24. package/dist/components/MultiAgentAccelerator/AgentFlow/PlasmaEdge.d.ts +7 -0
  25. package/dist/components/MultiAgentAccelerator/{PlasmaEdge.js → AgentFlow/PlasmaEdge.js} +13 -8
  26. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +74 -83
  27. package/dist/components/MultiAgentAccelerator/Schema/SlyData.d.ts +17 -0
  28. package/dist/components/MultiAgentAccelerator/Schema/SlyData.js +23 -0
  29. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +2 -0
  30. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +22 -25
  31. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +16 -22
  32. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +1 -0
  33. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +27 -7
  34. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +8 -0
  35. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +61 -13
  36. package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.d.ts → ThoughtBubbles/ThoughtBubbleEdge.d.ts} +1 -1
  37. package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.js → ThoughtBubbles/ThoughtBubbleOverlay.js} +1 -1
  38. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +7 -2
  39. package/dist/components/MultiAgentAccelerator/const.d.ts +11 -10
  40. package/dist/components/MultiAgentAccelerator/const.js +22 -20
  41. package/dist/components/Settings/SettingsDialog.js +26 -10
  42. package/dist/controller/agent/Agent.d.ts +14 -6
  43. package/dist/controller/agent/Agent.js +18 -14
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/state/Settings.d.ts +81 -4
  47. package/dist/state/Settings.js +87 -7
  48. package/dist/state/TemporaryNetworks.js +12 -21
  49. package/dist/tsconfig.build.tsbuildinfo +1 -1
  50. package/dist/utils/AgentName.d.ts +17 -0
  51. package/dist/utils/AgentName.js +46 -0
  52. package/dist/utils/BrowserNavigation.js +2 -0
  53. package/dist/utils/File.d.ts +4 -1
  54. package/dist/utils/File.js +4 -9
  55. package/dist/utils/text.js +3 -7
  56. package/package.json +9 -10
  57. package/dist/components/MultiAgentAccelerator/PlasmaEdge.d.ts +0 -3
  58. /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.js → ThoughtBubbles/ThoughtBubbleEdge.js} +0 -0
  59. /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.d.ts → ThoughtBubbles/ThoughtBubbleOverlay.d.ts} +0 -0
@@ -33,47 +33,50 @@ import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
33
33
  import Tooltip from "@mui/material/Tooltip";
34
34
  import Typography from "@mui/material/Typography";
35
35
  import { applyNodeChanges, Background, ConnectionMode, ControlButton, Controls, ReactFlow, useReactFlow, useStore, } from "@xyflow/react";
36
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
36
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
37
37
  import { AgentNode, NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
38
- import { AgentNodePopup } from "./AgentNodePopup.js";
39
- import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_NAME_KEY, BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING, } from "./const.js";
38
+ import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_NAME_KEY, BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING, } from "../const.js";
40
39
  import { addThoughtBubbleEdge, layoutLinear, layoutRadial } from "./GraphLayouts.js";
41
40
  import { PlasmaEdge } from "./PlasmaEdge.js";
42
- import { convertReservationsToNetworks, extractNetworkHocon, extractReservations, isEditableAgent, mergeNetworks, } from "./TemporaryNetworks.js";
43
- import { ThoughtBubbleEdge } from "./ThoughtBubbleEdge.js";
44
- import { ThoughtBubbleOverlay } from "./ThoughtBubbleOverlay.js";
45
- import { sendChatQuery } from "../../controller/agent/Agent.js";
46
- import { StreamingUnit } from "../../controller/llm/LlmChat.js";
47
- import { useAgentChatHistoryStore } from "../../state/ChatHistory.js";
48
- import { usePalette, useSettingsStore } from "../../state/Settings.js";
49
- import { useTempNetworksStore } from "../../state/TemporaryNetworks.js";
50
- import { getZIndex } from "../../utils/zIndexLayers.js";
51
- import { chatMessageFromChunk } from "../AgentChat/Common/Utils.js";
52
- import { MUIAlert } from "../Common/MUIAlert.js";
53
- import { NotificationType, sendNotification } from "../Common/notification.js";
54
- // #endregion: Types
55
- // #region: Constants
56
- // Timeout for thought bubbles is set to 10 seconds
41
+ import { sendChatQuery } from "../../../controller/agent/Agent.js";
42
+ import { StreamingUnit } from "../../../controller/llm/LlmChat.js";
43
+ import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
44
+ import { usePalette, useSettingsStore } from "../../../state/Settings.js";
45
+ import { useTempNetworksStore } from "../../../state/TemporaryNetworks.js";
46
+ import { getZIndex } from "../../../utils/zIndexLayers.js";
47
+ import { chatMessageFromChunk } from "../../AgentChat/Common/Utils.js";
48
+ import { MUIAlert } from "../../Common/MUIAlert.js";
49
+ import { NotificationType, sendNotification } from "../../Common/notification.js";
50
+ import { AgentNodePopup } from "../AgentNodePopup.js";
51
+ import { convertReservationsToNetworks, extractNetworkHocon, extractReservations, isEditableAgent, mergeNetworks, } from "../TemporaryNetworks.js";
52
+ import { ThoughtBubbleEdge } from "../ThoughtBubbles/ThoughtBubbleEdge.js";
53
+ import { ThoughtBubbleOverlay } from "../ThoughtBubbles/ThoughtBubbleOverlay.js";
54
+ //#endregion: Types
55
+ //#region: Constants
56
+ const AGENT_SAVE_TIMEOUT_MS = 60_000;
57
+ const DOCK_STREAM_TIMEOUT_MS = 120_000;
57
58
  const THOUGHT_BUBBLE_TIMEOUT_MS = 10_000;
58
59
  // How long the dock's status banner stays visible before auto-dismissing. Error banners persist until dismissed.
59
60
  // Exported for tests.
60
61
  export const DOCK_BANNER_AUTO_DISMISS_MS = 5_000;
61
- // #endregion: Constants
62
62
  const DOCK_PROMPT_PLACEHOLDER = "Describe a change to the network";
63
- // #region: Helpers
63
+ //#endregion: Constants
64
+ //#region: Helpers
64
65
  /**
65
66
  * Streams the Agent Network Designer endpoint with a natural-language prompt and the current
66
67
  * network definition, collecting any returned reservations.
67
68
  */
68
- const streamNetworkDesignerPrompt = async (neuroSanURL, signal, userPrompt, currentDefinition, agentNetworkName, currentUser) => {
69
+ const applyNetworkDesignerChanges = async (neuroSanURL, signal, userPrompt, currentDefinition, agentNetworkName, currentUser) => {
69
70
  let newNetworks = [];
70
71
  await sendChatQuery(neuroSanURL, signal, userPrompt, AGENT_NETWORK_DESIGNER_ID, (chunk) => {
71
72
  const chatMessage = chatMessageFromChunk(chunk);
72
- if (!chatMessage)
73
+ if (!chatMessage) {
73
74
  return;
75
+ }
74
76
  const reservations = extractReservations(chatMessage);
75
- if (reservations.length === 0)
77
+ if (reservations.length === 0) {
76
78
  return;
79
+ }
77
80
  const networkHocon = extractNetworkHocon(chatMessage);
78
81
  const agentNetworkNameFromMessage = chatMessage.sly_data?.[AGENT_NETWORK_NAME_KEY];
79
82
  const networkName = agentNetworkName ?? agentNetworkNameFromMessage;
@@ -86,7 +89,22 @@ const streamNetworkDesignerPrompt = async (neuroSanURL, signal, userPrompt, curr
86
89
  }, currentUser, StreamingUnit.Line);
87
90
  return newNetworks;
88
91
  };
89
- // #endregion: Helpers
92
+ /**
93
+ * Filters node events based on the current mode. Don't allow any topological-modifying events (adding, deleting nodes)
94
+ * and in Agent Network Designer mode, don't allow dragging nodes (position changes) either.
95
+ * @param change The node change event to filter.
96
+ * @param isAgentNetworkDesignerMode Whether the flow is in Agent Network Designer mode (read-only preview).
97
+ * @return True if the event should be allowed, false if it should be filtered out.
98
+ */
99
+ export const filterNodeEvents = (change, isAgentNetworkDesignerMode) => {
100
+ // Only allow nodes to be dragged, no topological edits to the graph (read-only)
101
+ if (["remove", "add", "replace"].includes(change.type))
102
+ return false;
103
+ // Disallow dragging nodes in AND mode since it's supposed to be a read-only preview but
104
+ // pass along all other event types as
105
+ return !(change.type === "position" && isAgentNetworkDesignerMode);
106
+ };
107
+ //#endregion: Helpers
90
108
  export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork, currentConversations, currentUser, id, isAgentNetworkDesignerMode, isAwaitingLlm, isEditMode, isStreaming, isSelectedNetworkTemporary: isTemporaryNetwork, networkDisplayName, networkId, neuroSanURL, onEnterEditMode, onNetworkReplaced, onExitEditMode, onSaveAgent, thoughtBubbleEdges, setThoughtBubbleEdges, }) => {
91
109
  const theme = useTheme();
92
110
  const { fitView } = useReactFlow();
@@ -304,26 +322,40 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
304
322
  detail: "Nothing was changed. Your prompt is restored below.",
305
323
  });
306
324
  }, [showDockBanner]);
307
- const handleNodeClick = useCallback((_event, node) => {
325
+ const openNodeEditor = useCallback((node) => {
308
326
  // Popup is only available for temporary networks.
309
327
  if (!isTemporaryNetwork)
310
328
  return;
311
329
  // Only llm_agent nodes support instructions/description editing.
312
330
  if (!isEditableAgent(node.data.displayAs))
313
331
  return;
314
- // Find the clicked agent's existing instructions and description from the temp network definition.
332
+ // Find the agent's existing instructions and description from the temp network definition.
315
333
  const currentTempNetwork = networkId
316
334
  ? tempNetworks.find((n) => n.agentInfo.agent_name === networkId)
317
335
  : undefined;
318
- const found = (currentTempNetwork?.agentNetworkDefinition ?? []).find((e) => e.origin === node.id);
336
+ const currentAgentDefinition = currentTempNetwork?.agentNetworkDefinition?.find((e) => e.origin === node.id);
319
337
  setSelectedAgent({
320
338
  agentId: node.id,
321
339
  agentName: node.data.agentName,
322
- initialInstructions: found?.instructions ?? "",
323
- initialDescription: found?.description ?? "",
340
+ initialInstructions: currentAgentDefinition?.instructions ?? "",
341
+ initialDescription: currentAgentDefinition?.description ?? "",
324
342
  });
325
343
  setIsPopupOpen(true);
326
344
  }, [tempNetworks, isTemporaryNetwork, networkId]);
345
+ const handleNodeClick = useCallback((_event, node) => openNodeEditor(node), [openNodeEditor]);
346
+ // ReactFlow makes nodes focusable and selects them on Enter, but it never fires onNodeClick from the keyboard.
347
+ // Route Enter on a focused node to the editor so keyboard users can open a node the same way a click does.
348
+ const handleNodeKeyDown = useCallback((event) => {
349
+ if (event.key !== "Enter")
350
+ return;
351
+ const target = event.target;
352
+ if (!(target instanceof HTMLElement))
353
+ return;
354
+ const nodeId = target.closest(".react-flow__node")?.dataset["id"];
355
+ const node = nodeId ? nodes.find((n) => n.id === nodeId) : undefined;
356
+ if (node)
357
+ openNodeEditor(node);
358
+ }, [nodes, openNodeEditor]);
327
359
  const handlePopupClose = useCallback(() => {
328
360
  // If a save is in-flight, abort it immediately so the stream doesn't hang.
329
361
  saveAbortControllerRef.current?.abort();
@@ -335,7 +367,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
335
367
  * Applies the networks returned by the designer: upserts them and triggers navigation if needed.
336
368
  * Returns true when a matching reservation was applied, false (and surfaces an error banner) otherwise.
337
369
  */
338
- const applyNetworkSaveResult = useCallback((newNetworksFromSave, currentAgentNetworkName) => {
370
+ const saveUpdates = useCallback((newNetworksFromSave, currentAgentNetworkName) => {
339
371
  if (newNetworksFromSave.length === 0) {
340
372
  showDockBanner({
341
373
  severity: "error",
@@ -344,6 +376,9 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
344
376
  });
345
377
  return false;
346
378
  }
379
+ // Find the returned reservation that stands in for the current network (matched by name). When
380
+ // present, persist every returned network, then carry the open network's chat history over to the
381
+ // replacement and notify the parent so it can navigate to the new reservation.
347
382
  const replacement = newNetworksFromSave.find((n) => n.agentNetworkName === currentAgentNetworkName);
348
383
  if (replacement) {
349
384
  useTempNetworksStore.getState().upsertTempNetworks(newNetworksFromSave);
@@ -362,7 +397,8 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
362
397
  return false;
363
398
  }, [networkId, onNetworkReplaced, showDockBanner]);
364
399
  const handleDockApply = useCallback(async () => {
365
- if (!dockPrompt.trim() || !neuroSanURL || !currentUser)
400
+ const readyToApplyEdit = Boolean(dockPrompt.trim() && neuroSanURL && currentUser);
401
+ if (!readyToApplyEdit)
366
402
  return;
367
403
  const currentTempNetwork = networkId
368
404
  ? tempNetworks.find((n) => n.agentInfo.agent_name === networkId)
@@ -375,11 +411,11 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
375
411
  const timeoutId = setTimeout(() => {
376
412
  hasTimedOut = true;
377
413
  controller.abort();
378
- }, 120_000); // 2 min timeout
414
+ }, DOCK_STREAM_TIMEOUT_MS);
379
415
  try {
380
- const newNetworks = await streamNetworkDesignerPrompt(neuroSanURL, controller.signal, dockPrompt, currentDefinition, currentTempNetwork?.agentNetworkName, currentUser);
381
- const applied = applyNetworkSaveResult(newNetworks, currentTempNetwork?.agentNetworkName);
382
- if (applied) {
416
+ const newNetworks = await applyNetworkDesignerChanges(neuroSanURL, controller.signal, dockPrompt, currentDefinition, currentTempNetwork?.agentNetworkName, currentUser);
417
+ const appliedSuccess = saveUpdates(newNetworks, currentTempNetwork?.agentNetworkName);
418
+ if (appliedSuccess) {
383
419
  setDockPrompt("");
384
420
  showDockBanner({
385
421
  severity: "success",
@@ -408,7 +444,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
408
444
  dockAbortControllerRef.current = null;
409
445
  setIsDockStreaming(false);
410
446
  }
411
- }, [applyNetworkSaveResult, currentUser, dockPrompt, networkId, neuroSanURL, showDockBanner, tempNetworks]);
447
+ }, [saveUpdates, currentUser, dockPrompt, networkId, neuroSanURL, showDockBanner, tempNetworks]);
412
448
  const handleExitEditMode = useCallback(() => {
413
449
  if (isDockStreaming) {
414
450
  dockAbortControllerRef.current?.abort();
@@ -417,6 +453,18 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
417
453
  }
418
454
  onExitEditMode?.();
419
455
  }, [isDockStreaming, onExitEditMode]);
456
+ // Pressing Escape exits edit mode, mirroring the explicit exit button. Skip while the
457
+ // node popup is open so Escape closes the popup first rather than the whole edit mode.
458
+ useEffect(() => {
459
+ if (!isEditMode || isPopupOpen)
460
+ return undefined;
461
+ const handleEscape = (e) => {
462
+ if (e.key === "Escape")
463
+ handleExitEditMode();
464
+ };
465
+ document.addEventListener("keydown", handleEscape);
466
+ return () => document.removeEventListener("keydown", handleEscape);
467
+ }, [isEditMode, isPopupOpen, handleExitEditMode]);
420
468
  const handlePopupSave = useCallback(async (agentName, instructionsText, descriptionText) => {
421
469
  if (!selectedAgent)
422
470
  return;
@@ -426,11 +474,11 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
426
474
  : undefined;
427
475
  // Produce a new array with the saved agent's fields updated; all other entries pass through unchanged.
428
476
  const currentDefinitions = currentTempNetwork?.agentNetworkDefinition ?? [];
429
- const updated = currentDefinitions.map((entry) => entry.origin === selectedAgent.agentId
477
+ const updatedDefinitions = currentDefinitions.map((entry) => entry.origin === selectedAgent.agentId
430
478
  ? { ...entry, instructions: instructionsText, description: descriptionText }
431
479
  : entry);
432
480
  if (networkId) {
433
- updateTempNetworkDefinition(networkId, updated);
481
+ updateTempNetworkDefinition(networkId, updatedDefinitions);
434
482
  }
435
483
  if (!onSaveAgent) {
436
484
  setIsPopupOpen(false);
@@ -439,10 +487,9 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
439
487
  setIsSavingAgent(true);
440
488
  const saveController = new AbortController();
441
489
  saveAbortControllerRef.current = saveController;
442
- const saveTimeoutId = setTimeout(() => saveController.abort(new DOMException("Save timed out", "TimeoutError")), 60_000 // 1 min timeout
443
- );
490
+ const saveTimeoutId = setTimeout(() => saveController.abort(new DOMException("Save timed out", "TimeoutError")), AGENT_SAVE_TIMEOUT_MS);
444
491
  try {
445
- await onSaveAgent(agentName, updated, currentTempNetwork?.agentNetworkName, saveController.signal);
492
+ await onSaveAgent(agentName, updatedDefinitions, currentTempNetwork?.agentNetworkName, saveController.signal);
446
493
  }
447
494
  catch (e) {
448
495
  console.error(`Error saving network ${agentName}. See onSaveAgent implementation for details.`, e);
@@ -466,10 +513,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
466
513
  }, 50);
467
514
  }, [agentsInNetwork, fitView, layout]);
468
515
  const onNodesChange = useCallback((changes) => {
469
- setNodes((currentNodes) => applyNodeChanges(
470
- // For now, we only allow dragging, no updates. In agent network designer mode, it doesn't make
471
- // sense to allow position changes since the user isn't actually manipulating a real network
472
- changes.filter((c) => c.type === "position" && !isAgentNetworkDesignerMode), currentNodes));
516
+ setNodes((currentNodes) => applyNodeChanges(changes.filter((change) => filterNodeEvents(change, isAgentNetworkDesignerMode)), currentNodes));
473
517
  }, [isAgentNetworkDesignerMode]);
474
518
  const transform = useStore((state) => state.transform);
475
519
  // Why not just a "const"? See: https://reactflow.dev/learn/customization/custom-nodes
@@ -529,7 +573,6 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
529
573
  }, size: "small", sx: {
530
574
  marginLeft: theme.spacing(2),
531
575
  "& .MuiToggleButton-root": {
532
- backgroundColor: theme.palette.background.paper,
533
576
  borderColor: theme.palette.divider,
534
577
  color: theme.palette.text.primary,
535
578
  minHeight: 22,
@@ -554,7 +597,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
554
597
  height: "1rem",
555
598
  }, children: "Heatmap" })] })] }));
556
599
  };
557
- // Get the background color for the control buttons based on the layout and dark mode setting
600
+ // Get the background color for the control buttons; differs based on whether the button is active or not
558
601
  const getControlButtonBackgroundColor = (isActive) => {
559
602
  return isActive ? theme.palette.action.selected : undefined;
560
603
  };
@@ -593,12 +636,10 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
593
636
  }, children: [_jsx(Tooltip, { title: networkDisplayName, placement: "top", children: _jsx(Box, { sx: { pointerEvents: "auto" }, children: _jsx(Typography, { id: `${id}-network-title`, variant: "subtitle1", sx: {
594
637
  backdropFilter: "blur(6px)",
595
638
  backgroundColor: titleBackgroundColor,
596
- border: `1px solid ${alpha(theme.palette.divider, 0.6)}`,
639
+ border: `1px solid ${alpha(theme.palette.divider, 0.75)}`,
597
640
  borderRadius: 2,
598
- boxShadow: theme.palette.mode === "dark"
599
- ? `0 6px 20px ${alpha(theme.palette.common.black, 0.35)}`
600
- : `0 6px 16px ${alpha(theme.palette.common.black, 0.12)}`,
601
- color: theme.palette.getContrastText(alpha(titleBackgroundColor, 0.65)),
641
+ boxShadow: theme.shadows[6],
642
+ color: theme.palette.text.primary,
602
643
  fontWeight: 600,
603
644
  letterSpacing: "0.01em",
604
645
  lineHeight: 1.35,
@@ -623,18 +664,18 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
623
664
  "& .react-flow__node": {
624
665
  border: "1px solid divider",
625
666
  },
626
- "& .react-flow__panel": {
667
+ "& .react-flow__panel, & .react-flow__controls-button": {
627
668
  backgroundColor: theme.palette.background.paper,
628
- border: "1px solid divider",
629
669
  color: theme.palette.text.primary,
630
670
  },
671
+ "& .react-flow__panel": {
672
+ border: "1px solid divider",
673
+ },
631
674
  "& .react-flow__controls-button": {
632
- backgroundColor: theme.palette.background.paper,
633
675
  borderBottom: "1px solid divider",
634
- color: theme.palette.text.primary,
635
676
  fill: theme.palette.text.primary,
636
677
  },
637
- }, children: [_jsxs(Box, { id: `${id}-react-flow-wrapper`, sx: { position: "relative", flex: 1, minHeight: 0 }, children: [networkDisplayName ? _jsx(Box, { sx: { marginBottom: "1rem" }, children: getTitle() }) : null, _jsx(ReactFlow, { id: `${id}-react-flow`, nodes: nodes, edges: edges, onNodesChange: onNodesChange, onNodeClick: handleNodeClick, fitView: true, nodeTypes: nodeTypes, edgeTypes: edgeTypes, connectionMode: ConnectionMode.Loose, children: !isAwaitingLlm && (_jsxs(_Fragment, { children: [agentsInNetwork?.length && !isAgentNetworkDesignerMode && !isEditMode ? getLegend() : null, _jsx(Background, { id: `${id}-background` }), !isAgentNetworkDesignerMode && !isEditMode && getControls(), shouldShowRadialGuides ? getRadialGuides() : null] })) }), _jsx(ThoughtBubbleOverlay, { nodes: nodes, edges: thoughtBubbleEdgesForOverlay, showThoughtBubbles: showThoughtBubbles, isStreaming: isStreaming, onBubbleHoverChange: handleBubbleHoverChange })] }), isEditMode && isTemporaryNetwork && !isAwaitingLlm && (_jsxs(Box, { sx: {
678
+ }, children: [_jsxs(Box, { id: `${id}-react-flow-wrapper`, sx: { position: "relative", flex: 1, minHeight: 0 }, children: [networkDisplayName ? _jsx(Box, { sx: { marginBottom: "1rem" }, children: getTitle() }) : null, _jsx(ReactFlow, { connectionMode: ConnectionMode.Loose, edgeTypes: edgeTypes, edges: edges, fitView: true, id: `${id}-react-flow`, nodeTypes: nodeTypes, nodes: nodes, nodesDraggable: !isAgentNetworkDesignerMode, onKeyDown: handleNodeKeyDown, onNodeClick: handleNodeClick, onNodesChange: onNodesChange, children: !isAwaitingLlm && (_jsxs(_Fragment, { children: [agentsInNetwork?.length && !isAgentNetworkDesignerMode && !isEditMode ? getLegend() : null, _jsx(Background, { id: `${id}-background` }), !isAgentNetworkDesignerMode && !isEditMode && getControls(), shouldShowRadialGuides ? getRadialGuides() : null] })) }), _jsx(ThoughtBubbleOverlay, { nodes: nodes, edges: thoughtBubbleEdgesForOverlay, showThoughtBubbles: showThoughtBubbles, isStreaming: isStreaming, onBubbleHoverChange: handleBubbleHoverChange })] }), isEditMode && isTemporaryNetwork && !isAwaitingLlm && (_jsxs(Box, { sx: {
638
679
  backdropFilter: "blur(8px)",
639
680
  backgroundColor: alpha(theme.palette.background.paper, 0.2),
640
681
  borderTop: `2px solid ${theme.palette.primary.main}`,
@@ -672,7 +713,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
672
713
  px: 1,
673
714
  py: 0.5,
674
715
  alignItems: "center",
675
- }, children: [_jsx(TextField, { fullWidth: true, placeholder: DOCK_PROMPT_PLACEHOLDER, variant: "outlined", size: "small", value: dockPrompt, onChange: (e) => setDockPrompt(e.target.value), onKeyDown: (e) => {
716
+ }, children: [_jsx(TextField, { autoFocus: true, fullWidth: true, placeholder: DOCK_PROMPT_PLACEHOLDER, variant: "outlined", size: "small", value: dockPrompt, onChange: (e) => setDockPrompt(e.target.value), onKeyDown: (e) => {
676
717
  if (e.key === "Enter" && !e.shiftKey) {
677
718
  e.preventDefault();
678
719
  void handleDockApply();
@@ -1,7 +1,7 @@
1
1
  import { NodeProps } from "@xyflow/react";
2
2
  import type { Node as RFNode } from "@xyflow/react";
3
3
  import { FC } from "react";
4
- import { AgentConversation } from "./AgentConversations.js";
4
+ import { AgentConversation } from "../AgentConversations.js";
5
5
  export interface AgentNodeProps extends Record<string, unknown> {
6
6
  readonly agentCounts?: Map<string, number>;
7
7
  readonly agentName: string;
@@ -27,11 +27,10 @@ import { keyframes, styled, useTheme } from "@mui/material/styles";
27
27
  import Tooltip from "@mui/material/Tooltip";
28
28
  import Typography from "@mui/material/Typography";
29
29
  import { Handle, Position } from "@xyflow/react";
30
- import { useState } from "react";
31
- import { DisplayAs } from "./const.js";
32
- import { usePalette, useSettingsStore } from "../../state/Settings.js";
33
- import { isLightColor } from "../../Theme/Theme.js";
34
- import { getZIndex } from "../../utils/zIndexLayers.js";
30
+ import { usePalette, useSettingsStore } from "../../../state/Settings.js";
31
+ import { isLightColor } from "../../../Theme/Theme.js";
32
+ import { getZIndex } from "../../../utils/zIndexLayers.js";
33
+ import { DisplayAs } from "../const.js";
35
34
  // A zero-width space character, used to give the browser a hint where to break long names
36
35
  const ZERO_WIDTH_SPACE = "\u200B";
37
36
  // Node dimensions
@@ -154,7 +153,6 @@ export const AgentNode = (props) => {
154
153
  const glowColor = isLightColor(theme.palette.background.default)
155
154
  ? theme.palette.common.black
156
155
  : theme.palette.common.white;
157
- const [isHovered, setIsHovered] = useState(false);
158
156
  // Insert zero-width spaces into the agent name to give the browser hints where to wrap.
159
157
  const wrappedAgentName = agentName
160
158
  // Allow wrap after underscore
@@ -165,7 +163,7 @@ export const AgentNode = (props) => {
165
163
  .replaceAll(/(?<abbr>[A-Z])(?<word>[A-Z][a-z])/gu, `$<abbr>${ZERO_WIDTH_SPACE}$<word>`);
166
164
  const height = NODE_HEIGHT * (isFrontman ? FRONTMAN_SIZE_MULTIPLIER : 1.0);
167
165
  const width = height;
168
- const getEditButton = () => (_jsx(Tooltip, { title: "Edit", placement: "top", disableInteractive: true, children: _jsx(IconButton, { sx: {
166
+ const getEditButton = () => (_jsx(Tooltip, { title: "Edit", placement: "top", disableInteractive: true, children: _jsx(IconButton, { className: "agent-node-edit-icon", "aria-hidden": true, tabIndex: -1, sx: {
169
167
  position: "absolute",
170
168
  top: 0,
171
169
  right: 0,
@@ -197,13 +195,17 @@ export const AgentNode = (props) => {
197
195
  width,
198
196
  zIndex: getZIndex(1, theme),
199
197
  }, children: wrappedAgentName }) }));
200
- const getNode = () => (_jsxs(AnimatedNode, { id: agentId, "data-testid": agentId, glowColor: glowColor, isActive: isActiveAgent, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), sx: {
198
+ const getNode = () => (_jsxs(AnimatedNode, { id: agentId, "data-testid": agentId, glowColor: glowColor, isActive: isActiveAgent, sx: {
201
199
  backgroundColor,
202
200
  color,
203
201
  cursor: isEditable ? "pointer" : "grab",
204
202
  height,
205
203
  width,
206
204
  zIndex: getZIndex(1, theme),
207
- }, children: [getDisplayAsIcon(), isHovered && isEditable && getEditButton(), _jsx(Handle, { id: `${agentId}-left-handle`, position: Position.Left, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-right-handle`, position: Position.Right, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-top-handle`, position: Position.Top, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-bottom-handle`, position: Position.Bottom, type: "source", style: { visibility: handleVisibility } })] }));
205
+ // Reveal the edit icon on hover, or when a keyboard user tabs to the node.
206
+ "& .agent-node-edit-icon": { opacity: 0, transition: "opacity 0.15s ease-in-out" },
207
+ "&:hover .agent-node-edit-icon": { opacity: 1 },
208
+ ".react-flow__node:focus-visible & .agent-node-edit-icon": { opacity: 1 },
209
+ }, children: [getDisplayAsIcon(), isEditable && getEditButton(), _jsx(Handle, { id: `${agentId}-left-handle`, position: Position.Left, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-right-handle`, position: Position.Right, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-top-handle`, position: Position.Top, type: "source", style: { visibility: handleVisibility } }), _jsx(Handle, { id: `${agentId}-bottom-handle`, position: Position.Bottom, type: "source", style: { visibility: handleVisibility } })] }));
208
210
  return (_jsxs(_Fragment, { children: [getNode(), getNodeName()] }));
209
211
  };
@@ -1,9 +1,9 @@
1
1
  import { Edge, Node as RFNode } from "@xyflow/react";
2
- import { AgentConversation } from "./AgentConversations.js";
2
+ import { AgentConversation } from "../AgentConversations.js";
3
3
  import { AgentNodeProps } from "./AgentNode.js";
4
- import { ThoughtBubbleEdgeShape } from "./ThoughtBubbleEdge.js";
5
- import { AgentIconSuggestions } from "../../controller/Types/AgentIconSuggestions.js";
6
- import { ConnectivityInfo } from "../../generated/neuro-san/NeuroSanClient.js";
4
+ import { AgentIconSuggestions } from "../../../controller/Types/AgentIconSuggestions.js";
5
+ import { ConnectivityInfo } from "../../../generated/neuro-san/NeuroSanClient.js";
6
+ import { ThoughtBubbleEdgeShape } from "../ThoughtBubbles/ThoughtBubbleEdge.js";
7
7
  export declare const MAX_GLOBAL_THOUGHT_BUBBLES = 5;
8
8
  /**
9
9
  * Result of the layout algorithms, containing the nodes and edges with their computed positions and properties.
@@ -20,9 +20,11 @@ import dagre from "@dagrejs/dagre";
20
20
  import { MarkerType } from "@xyflow/react";
21
21
  import cloneDeep from "lodash-es/cloneDeep.js";
22
22
  import { FRONTMAN_SIZE_MULTIPLIER, NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
23
- import { BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING } from "./const.js";
24
- import { isEditableAgent } from "./TemporaryNetworks.js";
25
- import { cleanUpAgentName, KNOWN_MESSAGE_TYPES_FOR_PLASMA } from "../AgentChat/Common/Utils.js";
23
+ import { cleanUpAgentName } from "../../../utils/AgentName.js";
24
+ import { KNOWN_MESSAGE_TYPES_FOR_PLASMA } from "../../AgentChat/Common/Utils.js";
25
+ import { BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING } from "../const.js";
26
+ import { getFrontman, getParentAgents, getParents } from "./GraphStructure.js";
27
+ import { isEditableAgent } from "../TemporaryNetworks.js";
26
28
  export const MAX_GLOBAL_THOUGHT_BUBBLES = 5;
27
29
  const FRONTMAN_OFFSET_X = (NODE_WIDTH * (FRONTMAN_SIZE_MULTIPLIER - 1)) / 2;
28
30
  const FRONTMAN_OFFSET_Y = (NODE_HEIGHT * (FRONTMAN_SIZE_MULTIPLIER - 1)) / 2;
@@ -60,34 +62,6 @@ const areInSameConversation = (conversations, sourceAgent, targetAgent) => {
60
62
  // Name for custom node
61
63
  const AGENT_NODE_TYPE_NAME = "agentNode";
62
64
  // #endregion: Constants
63
- /**
64
- * Returns the "origins" (node names) of the _immediate_ parents of a node in the agent network. Grandparents and
65
- * higher are not included.
66
- *
67
- * @param node Node ID for which to find parents
68
- * @param parentAgents Full list of parent agents in the network
69
- * @returns The IDs of the immediate parent nodes for the given node or empty array if no parents are found (frontman)
70
- */
71
- const getParents = (node, parentAgents) => {
72
- return parentAgents.filter((agent) => agent.tools.includes(node)).map((parentNode) => parentNode.origin);
73
- };
74
- // "Parent agents" are those that have tools, aka "child agents"
75
- const getParentAgents = (agentsInNetwork) => agentsInNetwork.length === 1 ? agentsInNetwork : agentsInNetwork.filter((agent) => agent.tools?.length > 0);
76
- // "Child agents" are those that are declared as tools by other agents
77
- const getChildAgents = (parentAgents) => {
78
- const childAgentsSet = new Set();
79
- parentAgents.forEach((agent) => {
80
- agent?.tools?.forEach((tool) => {
81
- childAgentsSet.add(tool);
82
- });
83
- });
84
- return childAgentsSet;
85
- };
86
- // Frontman is defined as the agent that has no parent. There should be only one in the graph.
87
- // Frontman is one of the parent agents.
88
- // There is no explicit field that tells us which agent is the frontman, so we determine it by checking
89
- // which parent agent does not appear in the set of child agents.
90
- const getFrontman = (parentAgents, childAgents) => parentAgents.find((agent) => !childAgents.has(agent.origin));
91
65
  // Generates the properties for an edge in the graph.
92
66
  // Common for both radial and linear layouts.
93
67
  const getEdgeProperties = (sourceId, targetId, sourceHandle, targetHandle, isAnimated) => {
@@ -102,6 +76,8 @@ const getEdgeProperties = (sourceId, targetId, sourceHandle, targetHandle, isAni
102
76
  type: isAnimated ? "plasmaEdge" : undefined,
103
77
  };
104
78
  };
79
+ // An agent is editable if it belongs to a temporary network and is of an editable type.
80
+ const isNodeEditable = (displayAs, isTemporaryNetwork) => isTemporaryNetwork && isEditableAgent(displayAs);
105
81
  export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsInNetwork, currentConversations, isAgentNetworkDesignerMode, isAwaitingLlm, isTemporaryNetwork = false, thoughtBubbleEdges, useNativeNames, }) => {
106
82
  const nodesInNetwork = [];
107
83
  const edgesInNetwork = [];
@@ -109,8 +85,7 @@ export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsI
109
85
  const nodeDepths = new Map();
110
86
  const queue = [];
111
87
  const parentAgents = getParentAgents(agentsInNetwork);
112
- const childAgents = getChildAgents(parentAgents);
113
- const frontman = getFrontman(parentAgents, childAgents);
88
+ const frontman = getFrontman(agentsInNetwork);
114
89
  if (frontman) {
115
90
  // Add the frontman node to the network
116
91
  queue.push({ id: frontman.origin, depth: 0 });
@@ -147,6 +122,7 @@ export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsI
147
122
  const x = DEFAULT_FRONTMAN_X_POS + radius * Math.cos(angle);
148
123
  const y = DEFAULT_FRONTMAN_Y_POS + radius * Math.sin(angle);
149
124
  const isFrontman = frontman?.origin === nodeId;
125
+ const displayAs = agentsInNetwork.find((a) => a.origin === nodeId)?.display_as;
150
126
  const parentNodes = getParents(nodeId, parentAgents);
151
127
  // Create an edge from each parent node to this node
152
128
  for (const parentNode of parentNodes) {
@@ -187,13 +163,12 @@ export const layoutRadial = ({ agentCounts, agentIconSuggestions = null, agentsI
187
163
  agentCounts,
188
164
  agentName: useNativeNames ? nodeId : cleanUpAgentName(nodeId),
189
165
  depth,
190
- displayAs: agentsInNetwork.find((a) => a.origin === nodeId)?.display_as,
166
+ displayAs,
191
167
  // Use current conversations for node highlighting (cleared at end)
192
168
  getConversations: () => currentConversations,
193
169
  isAwaitingLlm,
194
170
  agentIconSuggestion: agentIconSuggestions?.[nodeId],
195
- isEditable: isTemporaryNetwork &&
196
- isEditableAgent(agentsInNetwork.find((a) => a.origin === nodeId)?.display_as),
171
+ isEditable: isNodeEditable(displayAs, isTemporaryNetwork),
197
172
  },
198
173
  // position: allow for Frontman being larger
199
174
  position: isFrontman
@@ -220,9 +195,8 @@ export const layoutLinear = ({ agentCounts, agentIconSuggestions = null, agentsI
220
195
  const edgesInNetwork = [];
221
196
  // Do these calculations outside the loop for efficiency
222
197
  const parentAgents = getParentAgents(agentsInNetwork);
223
- const childAgents = getChildAgents(parentAgents);
224
- const frontman = getFrontman(parentAgents, childAgents);
225
- agentsInNetwork.forEach(({ origin: nodeId }) => {
198
+ const frontman = getFrontman(agentsInNetwork);
199
+ agentsInNetwork.forEach(({ display_as: displayAs, origin: nodeId }) => {
226
200
  const parentIds = getParents(nodeId, parentAgents);
227
201
  const isFrontman = frontman?.origin === nodeId;
228
202
  nodesInNetwork.push({
@@ -231,13 +205,13 @@ export const layoutLinear = ({ agentCounts, agentIconSuggestions = null, agentsI
231
205
  data: {
232
206
  agentCounts,
233
207
  agentName: useNativeNames ? nodeId : cleanUpAgentName(nodeId),
234
- displayAs: agentsInNetwork.find((a) => a.origin === nodeId)?.display_as,
208
+ displayAs,
235
209
  // Use current conversations for node highlighting (cleared at end)
236
210
  getConversations: () => currentConversations,
237
211
  isAwaitingLlm,
238
212
  depth: undefined, // Depth will be computed later,
239
213
  agentIconSuggestion: agentIconSuggestions?.[nodeId],
240
- isEditable: isTemporaryNetwork && isEditableAgent(agentsInNetwork.find((a) => a.origin === nodeId)?.display_as),
214
+ isEditable: isNodeEditable(displayAs, isTemporaryNetwork),
241
215
  },
242
216
  position: isFrontman ? { x: DEFAULT_FRONTMAN_X_POS, y: DEFAULT_FRONTMAN_Y_POS } : { x: 0, y: 0 },
243
217
  style: {
@@ -0,0 +1,21 @@
1
+ import { ConnectivityInfo } from "../../../generated/neuro-san/NeuroSanClient.js";
2
+ export declare const getParentAgents: <T extends ConnectivityInfo>(agents: readonly T[]) => readonly T[];
3
+ /**
4
+ * Determines the frontman of an agent network — the single source of truth for "who is the frontman".
5
+ *
6
+ * The frontman is the agent with no parent: a "parent" agent (one that has tools) that is not itself
7
+ * listed as a tool by any other agent. A lone single-agent network is its own frontman. Returns
8
+ * undefined when no agent qualifies (e.g. a fully cyclic network); callers decide how to fall back.
9
+ *
10
+ * Consumed by the graph layouts (to seed the depth-0 node) and by the network import flow.
11
+ */
12
+ export declare const getFrontman: (agents: readonly ConnectivityInfo[]) => ConnectivityInfo | undefined;
13
+ /**
14
+ * Returns the "origins" (node names) of the _immediate_ parents of a node in the agent network. Grandparents and
15
+ * higher are not included.
16
+ *
17
+ * @param node Node ID for which to find parents
18
+ * @param parentAgents Full list of parent agents in the network
19
+ * @returns The IDs of the immediate parent nodes for the given node or empty array if no parents are found (frontman)
20
+ */
21
+ export declare const getParents: (node: string, parentAgents: readonly ConnectivityInfo[]) => string[];
@@ -0,0 +1,27 @@
1
+ // "Parent agents" are those that have tools, aka agents with "child agents". A lone single-agent
2
+ // network is treated as its own parent.
3
+ export const getParentAgents = (agents) => agents.length === 1 ? agents : agents.filter((agent) => (agent.tools?.length ?? 0) > 0);
4
+ /**
5
+ * Determines the frontman of an agent network — the single source of truth for "who is the frontman".
6
+ *
7
+ * The frontman is the agent with no parent: a "parent" agent (one that has tools) that is not itself
8
+ * listed as a tool by any other agent. A lone single-agent network is its own frontman. Returns
9
+ * undefined when no agent qualifies (e.g. a fully cyclic network); callers decide how to fall back.
10
+ *
11
+ * Consumed by the graph layouts (to seed the depth-0 node) and by the network import flow.
12
+ */
13
+ export const getFrontman = (agents) => {
14
+ const parentAgents = getParentAgents(agents);
15
+ // Child agents are everything referenced as a tool by a parent.
16
+ const childAgents = new Set(parentAgents.flatMap((agent) => agent.tools ?? []));
17
+ return parentAgents.find((agent) => agent.origin !== undefined && !childAgents.has(agent.origin));
18
+ };
19
+ /**
20
+ * Returns the "origins" (node names) of the _immediate_ parents of a node in the agent network. Grandparents and
21
+ * higher are not included.
22
+ *
23
+ * @param node Node ID for which to find parents
24
+ * @param parentAgents Full list of parent agents in the network
25
+ * @returns The IDs of the immediate parent nodes for the given node or empty array if no parents are found (frontman)
26
+ */
27
+ export const getParents = (node, parentAgents) => parentAgents.filter((agent) => agent.tools.includes(node)).map((parentNode) => parentNode.origin);
@@ -0,0 +1,7 @@
1
+ import { EdgeProps } from "@xyflow/react";
2
+ import { FC } from "react";
3
+ export type PlasmaEdgeProps = {
4
+ readonly maxParticles?: number;
5
+ readonly particlesPerFrame?: number;
6
+ };
7
+ export declare const PlasmaEdge: FC<EdgeProps & PlasmaEdgeProps>;