@flowdrop/flowdrop 1.3.0 → 1.5.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 (114) hide show
  1. package/README.md +68 -24
  2. package/dist/adapters/WorkflowAdapter.js +2 -22
  3. package/dist/adapters/agentspec/autoLayout.d.ts +51 -5
  4. package/dist/adapters/agentspec/autoLayout.js +120 -23
  5. package/dist/chat/commandClassifier.d.ts +19 -0
  6. package/dist/chat/commandClassifier.js +30 -0
  7. package/dist/chat/index.d.ts +27 -0
  8. package/dist/chat/index.js +32 -0
  9. package/dist/chat/responseParser.d.ts +21 -0
  10. package/dist/chat/responseParser.js +87 -0
  11. package/dist/commands/batch.d.ts +18 -0
  12. package/dist/commands/batch.js +56 -0
  13. package/dist/commands/executor.d.ts +37 -0
  14. package/dist/commands/executor.js +1044 -0
  15. package/dist/commands/index.d.ts +14 -0
  16. package/dist/commands/index.js +17 -0
  17. package/dist/commands/parser.d.ts +16 -0
  18. package/dist/commands/parser.js +278 -0
  19. package/dist/commands/positioner.d.ts +19 -0
  20. package/dist/commands/positioner.js +33 -0
  21. package/dist/commands/storeIntegration.svelte.d.ts +16 -0
  22. package/dist/commands/storeIntegration.svelte.js +67 -0
  23. package/dist/commands/types.d.ts +343 -0
  24. package/dist/commands/types.js +45 -0
  25. package/dist/components/App.svelte +431 -17
  26. package/dist/components/App.svelte.d.ts +10 -0
  27. package/dist/components/CanvasBanner.stories.svelte +6 -2
  28. package/dist/components/CanvasController.svelte +38 -0
  29. package/dist/components/CanvasController.svelte.d.ts +32 -0
  30. package/dist/components/ConfigMappingRow.svelte +130 -0
  31. package/dist/components/ConfigMappingRow.svelte.d.ts +8 -0
  32. package/dist/components/ConfigPanel.svelte +56 -7
  33. package/dist/components/ConfigPanel.svelte.d.ts +2 -0
  34. package/dist/components/FlowDropEdge.svelte +8 -57
  35. package/dist/components/Logo.svelte +14 -14
  36. package/dist/components/LogsSidebar.svelte +5 -5
  37. package/dist/components/Navbar.svelte +58 -10
  38. package/dist/components/Navbar.svelte.d.ts +7 -0
  39. package/dist/components/NodeSidebar.svelte +238 -362
  40. package/dist/components/NodeSwapPicker.svelte +537 -0
  41. package/dist/components/NodeSwapPicker.svelte.d.ts +16 -0
  42. package/dist/components/PortMappingRow.svelte +209 -0
  43. package/dist/components/PortMappingRow.svelte.d.ts +12 -0
  44. package/dist/components/SwapMappingEditor.svelte +550 -0
  45. package/dist/components/SwapMappingEditor.svelte.d.ts +12 -0
  46. package/dist/components/WorkflowEditor.svelte +99 -4
  47. package/dist/components/WorkflowEditor.svelte.d.ts +8 -0
  48. package/dist/components/chat/AIChatPanel.svelte +658 -0
  49. package/dist/components/chat/AIChatPanel.svelte.d.ts +13 -0
  50. package/dist/components/chat/CommandPreview.svelte +184 -0
  51. package/dist/components/chat/CommandPreview.svelte.d.ts +9 -0
  52. package/dist/components/console/CommandConsole.stories.svelte +93 -0
  53. package/dist/components/console/CommandConsole.stories.svelte.d.ts +27 -0
  54. package/dist/components/console/CommandConsole.svelte +259 -0
  55. package/dist/components/console/CommandConsole.svelte.d.ts +11 -0
  56. package/dist/components/console/ConsoleAutocomplete.svelte +139 -0
  57. package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +21 -0
  58. package/dist/components/console/ConsoleInput.svelte +712 -0
  59. package/dist/components/console/ConsoleInput.svelte.d.ts +16 -0
  60. package/dist/components/console/ConsoleOutput.svelte +121 -0
  61. package/dist/components/console/ConsoleOutput.svelte.d.ts +11 -0
  62. package/dist/components/console/formatters.d.ts +26 -0
  63. package/dist/components/console/formatters.js +118 -0
  64. package/dist/components/interrupt/index.d.ts +1 -0
  65. package/dist/components/interrupt/index.js +1 -0
  66. package/dist/components/nodes/SimpleNode.stories.svelte +64 -0
  67. package/dist/components/nodes/SimpleNode.svelte +27 -11
  68. package/dist/components/nodes/SquareNode.stories.svelte +45 -0
  69. package/dist/components/nodes/SquareNode.svelte +27 -11
  70. package/dist/components/nodes/WorkflowNode.stories.svelte +63 -0
  71. package/dist/config/endpoints.d.ts +8 -0
  72. package/dist/config/endpoints.js +5 -0
  73. package/dist/core/index.d.ts +5 -0
  74. package/dist/core/index.js +9 -0
  75. package/dist/editor/index.d.ts +3 -1
  76. package/dist/editor/index.js +4 -2
  77. package/dist/helpers/proximityConnect.js +8 -1
  78. package/dist/helpers/workflowEditorHelper.d.ts +3 -53
  79. package/dist/helpers/workflowEditorHelper.js +13 -228
  80. package/dist/playground/index.d.ts +1 -1
  81. package/dist/playground/index.js +1 -1
  82. package/dist/schemas/v1/workflow.schema.json +107 -22
  83. package/dist/services/chatService.d.ts +65 -0
  84. package/dist/services/chatService.js +131 -0
  85. package/dist/services/historyService.d.ts +6 -4
  86. package/dist/services/historyService.js +21 -6
  87. package/dist/skins/slate.js +16 -0
  88. package/dist/stores/interruptStore.svelte.js +6 -1
  89. package/dist/stores/playgroundStore.svelte.d.ts +1 -1
  90. package/dist/stores/playgroundStore.svelte.js +11 -2
  91. package/dist/stores/portCoordinateStore.svelte.d.ts +4 -0
  92. package/dist/stores/portCoordinateStore.svelte.js +20 -26
  93. package/dist/stores/workflowStore.svelte.d.ts +31 -2
  94. package/dist/stores/workflowStore.svelte.js +84 -64
  95. package/dist/stories/EdgeDecorator.svelte +4 -4
  96. package/dist/styles/base.css +48 -0
  97. package/dist/svelte-app.d.ts +7 -1
  98. package/dist/svelte-app.js +4 -1
  99. package/dist/types/chat.d.ts +63 -0
  100. package/dist/types/chat.js +9 -0
  101. package/dist/types/events.d.ts +28 -2
  102. package/dist/types/events.js +1 -0
  103. package/dist/types/index.d.ts +8 -0
  104. package/dist/types/settings.d.ts +6 -0
  105. package/dist/types/settings.js +3 -0
  106. package/dist/utils/edgeStyling.d.ts +42 -0
  107. package/dist/utils/edgeStyling.js +176 -0
  108. package/dist/utils/nodeIds.d.ts +31 -0
  109. package/dist/utils/nodeIds.js +42 -0
  110. package/dist/utils/nodeSwap.d.ts +221 -0
  111. package/dist/utils/nodeSwap.js +686 -0
  112. package/package.json +6 -1
  113. package/dist/helpers/nodeLayoutHelper.d.ts +0 -14
  114. package/dist/helpers/nodeLayoutHelper.js +0 -19
@@ -5,12 +5,18 @@
5
5
  -->
6
6
 
7
7
  <script lang="ts">
8
- import { onMount } from "svelte";
8
+ import { onMount, tick } from "svelte";
9
9
  import MainLayout from "./layouts/MainLayout.svelte";
10
10
  import WorkflowEditor from "./WorkflowEditor.svelte";
11
11
  import NodeSidebar from "./NodeSidebar.svelte";
12
+ import Icon from "@iconify/svelte";
12
13
  import ConfigForm from "./ConfigForm.svelte";
13
14
  import ConfigPanel from "./ConfigPanel.svelte";
15
+ import CommandConsole from "./console/CommandConsole.svelte";
16
+ import AIChatPanel from "./chat/AIChatPanel.svelte";
17
+ import type { UIAction } from "../commands/index.js";
18
+ import NodeSwapPicker from "./NodeSwapPicker.svelte";
19
+ import SwapMappingEditor from "./SwapMappingEditor.svelte";
14
20
  import Navbar from "./Navbar.svelte";
15
21
  import { api, setEndpointConfig } from "../services/api.js";
16
22
  import { EnhancedFlowDropApiClient } from "../api/enhanced-client.js";
@@ -21,6 +27,14 @@
21
27
  ConfigSchema,
22
28
  NodeUIExtensions,
23
29
  } from "../types/index.js";
30
+ import type { InteractiveSwapState, SwapEventContext } from "../utils/nodeSwap.js";
31
+ import {
32
+ computeInteractiveState,
33
+ buildSwapPreviewFromState,
34
+ executeSwap,
35
+ validateSwapResult,
36
+ } from "../utils/nodeSwap.js";
37
+ import type { SwapStrategy } from "../utils/nodeSwap.js";
24
38
  import { DEFAULT_WORKFLOW_FORMAT } from "../types/index.js";
25
39
  import { createEndpointConfig } from "../config/endpoints.js";
26
40
  import type { EndpointConfig } from "../config/endpoints.js";
@@ -46,12 +60,16 @@
46
60
  } from "../services/globalSave.js";
47
61
  import { apiToasts, dismissToast } from "../services/toastService.js";
48
62
  import { initAutoSave } from "../services/autoSaveService.js";
49
- import { getUiSettings } from "../stores/settingsStore.svelte.js";
50
- import { initializePortCompatibility } from "../utils/connections.js";
63
+ import {
64
+ getUiSettings,
65
+ updateSettings,
66
+ } from "../stores/settingsStore.svelte.js";
67
+ import { initializePortCompatibility, getPortCompatibilityChecker } from "../utils/connections.js";
51
68
  import { DEFAULT_PORT_CONFIG } from "../config/defaultPortConfig.js";
52
69
  import { workflowFormatRegistry } from "../registry/workflowFormatRegistry.js";
53
70
  import { logger } from "../utils/logger.js";
54
71
  import { validateWorkflowData } from "../utils/validation.js";
72
+ import type { SettingsCategory } from "../types/settings.js";
55
73
 
56
74
  /**
57
75
  * Configuration props for runtime customization
@@ -104,6 +122,14 @@
104
122
  features?: FlowDropFeatures;
105
123
  /** Visual theme — named built-in ('default' | 'minimal') or custom theme object */
106
124
  theme?: FlowDropTheme | FlowDropThemeName;
125
+ /** Which settings tabs to show in the modal */
126
+ settingsCategories?: SettingsCategory[];
127
+ /** Show the "Sync to Cloud" button in the settings modal */
128
+ showSettingsSyncButton?: boolean;
129
+ /** Show the reset buttons in the settings modal */
130
+ showSettingsResetButton?: boolean;
131
+ /** Pluggable swap strategies — instance-scoped, checked in order */
132
+ swapStrategies?: SwapStrategy[];
107
133
  }
108
134
 
109
135
  let {
@@ -126,6 +152,10 @@
126
152
  eventHandlers,
127
153
  features: propFeatures,
128
154
  theme: themeProp,
155
+ settingsCategories,
156
+ showSettingsSyncButton,
157
+ showSettingsResetButton,
158
+ swapStrategies,
129
159
  }: Props = $props();
130
160
 
131
161
  // svelte-ignore state_referenced_locally — feature flags don't change at runtime
@@ -166,7 +196,7 @@
166
196
  });
167
197
 
168
198
  // Create breadcrumb-style title - at top level to avoid store subscription issues
169
- let breadcrumbTitle = $derived(() => {
199
+ let breadcrumbTitle = $derived.by(() => {
170
200
  // Use custom navbar title if provided
171
201
  if (navbarTitle) {
172
202
  return navbarTitle;
@@ -199,6 +229,11 @@
199
229
  // Workflow settings sidebar state
200
230
  let isWorkflowSettingsOpen = $state(false);
201
231
 
232
+ // Node swap state
233
+ let swapMode = $state<"idle" | "picking" | "mapping">("idle");
234
+ let swapTargetMetadata = $state<NodeMetadata | null>(null);
235
+ let swapInteractiveState = $state<InteractiveSwapState | null>(null);
236
+
202
237
  // Workflow configuration schema (derived to pick up dynamic format options)
203
238
  let workflowConfigSchema: ConfigSchema = $derived({
204
239
  type: "object" as const,
@@ -235,7 +270,7 @@
235
270
  });
236
271
 
237
272
  // Get the current node from the workflow store
238
- let selectedNodeForConfig = $derived(() => {
273
+ let selectedNodeForConfig = $derived.by(() => {
239
274
  const wf = getWorkflowStore();
240
275
  if (!selectedNodeId || !wf) return null;
241
276
  return wf.nodes.find((node) => node.id === selectedNodeId) || null;
@@ -430,11 +465,19 @@
430
465
  }
431
466
  selectedNodeId = node.id;
432
467
  isConfigSidebarOpen = true;
468
+ // Reset swap state when switching nodes
469
+ swapMode = "idle";
470
+ swapTargetMetadata = null;
471
+ swapInteractiveState = null;
433
472
  }
434
473
 
435
474
  function closeConfigSidebar(): void {
436
475
  isConfigSidebarOpen = false;
437
476
  selectedNodeId = null;
477
+ // Reset swap state when closing
478
+ swapMode = "idle";
479
+ swapTargetMetadata = null;
480
+ swapInteractiveState = null;
438
481
  }
439
482
 
440
483
  /**
@@ -448,6 +491,144 @@
448
491
  }
449
492
  }
450
493
 
494
+ /**
495
+ * Start swap mode — transitions the right sidebar to the node picker
496
+ */
497
+ function startSwap(): void {
498
+ swapMode = "picking";
499
+ swapTargetMetadata = null;
500
+ swapInteractiveState = null;
501
+ }
502
+
503
+ /**
504
+ * Handle selection of a target node type for swap
505
+ */
506
+ function handleSwapSelect(metadata: NodeMetadata): void {
507
+ const node = selectedNodeForConfig;
508
+ if (!node) return;
509
+
510
+ const wf = getWorkflowStore();
511
+ if (!wf) return;
512
+
513
+ // Format compatibility guard — defence-in-depth behind picker's own filter
514
+ const currentFormat = getWorkflowFormat();
515
+ if (metadata.formats?.length && !metadata.formats.includes(currentFormat)) {
516
+ return;
517
+ }
518
+
519
+ // Get port compatibility checker (may be null if not initialized)
520
+ let checker: import("../utils/connections.js").PortCompatibilityChecker | null = null;
521
+ try {
522
+ checker = getPortCompatibilityChecker();
523
+ } catch {
524
+ // Checker not initialized — computeSwapPreview will use exact dataType matching
525
+ }
526
+
527
+ const interactive = computeInteractiveState(
528
+ node,
529
+ metadata,
530
+ wf.edges,
531
+ wf.nodes,
532
+ { checker, strategies: swapStrategies },
533
+ );
534
+
535
+ swapTargetMetadata = metadata;
536
+ swapInteractiveState = interactive;
537
+ swapMode = "mapping";
538
+ }
539
+
540
+ /**
541
+ * Execute the confirmed node swap
542
+ */
543
+ async function executeNodeSwap(finalState?: InteractiveSwapState): Promise<void> {
544
+ const state = finalState ?? swapInteractiveState;
545
+ if (!state) return;
546
+
547
+ const wf = getWorkflowStore();
548
+ if (!wf) return;
549
+
550
+ const oldLabel = state.oldNode.data.label;
551
+ const newLabel = state.newMetadata.name;
552
+
553
+ // Convert interactive state to swap preview
554
+ const preview = buildSwapPreviewFromState(state, wf.edges);
555
+
556
+ // Execute the swap
557
+ const result = executeSwap(
558
+ state.oldNode,
559
+ state.newMetadata,
560
+ preview,
561
+ wf.nodes,
562
+ wf.edges,
563
+ );
564
+
565
+ // Post-swap validation
566
+ const validation = validateSwapResult(result);
567
+ if (!validation.valid) {
568
+ logger.error("Swap validation failed:", validation.error);
569
+ return;
570
+ }
571
+
572
+ // onBeforeSwap hook — abort if returns false
573
+ if (eventHandlers?.onBeforeSwap) {
574
+ const swapEventCtx: SwapEventContext = {
575
+ oldNode: state.oldNode,
576
+ newMetadata: state.newMetadata,
577
+ preview,
578
+ portOverrides: [],
579
+ configOverrides: [],
580
+ };
581
+ const shouldProceed = await eventHandlers.onBeforeSwap(swapEventCtx);
582
+ if (shouldProceed === false) return;
583
+ }
584
+
585
+ // Apply as a single atomic swap with descriptive history entry
586
+ workflowActions.swapNode({
587
+ nodes: result.updatedNodes,
588
+ edges: result.updatedEdges,
589
+ description: `Swap node: ${oldLabel} → ${newLabel}`,
590
+ });
591
+
592
+ // onAfterSwap hook (fire-and-forget — swap is already applied)
593
+ if (eventHandlers?.onAfterSwap) {
594
+ try {
595
+ eventHandlers.onAfterSwap(result, state.oldNode, state.newNodeId);
596
+ } catch (err) {
597
+ logger.error("onAfterSwap hook error:", err);
598
+ }
599
+ }
600
+
601
+ // Select the new node in the sidebar
602
+ const newNodeId = state.newNodeId;
603
+ selectedNodeId = newNodeId;
604
+
605
+ // Reset swap state
606
+ swapMode = "idle";
607
+ swapTargetMetadata = null;
608
+ swapInteractiveState = null;
609
+
610
+ // Wait for SvelteFlow to process the new node before updating visual state
611
+ await tick();
612
+
613
+ // Refresh the editor visual state
614
+ if (workflowEditorRef) {
615
+ const newNode = result.updatedNodes.find((n) => n.id === newNodeId);
616
+ if (newNode) {
617
+ workflowEditorRef.updateNodeData(newNodeId, newNode.data);
618
+ await workflowEditorRef.refreshEdgePositions(newNodeId);
619
+ }
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Cancel swap and return to normal config view
625
+ */
626
+ function cancelSwap(): void {
627
+ swapMode = "idle";
628
+ swapTargetMetadata = null;
629
+ swapInteractiveState = null;
630
+ }
631
+
451
632
  /**
452
633
  * Handle workflow configuration save
453
634
  */
@@ -661,22 +842,102 @@
661
842
  * Config panel always appears on the right side
662
843
  */
663
844
  const hasConfigPanelOpen = $derived(
664
- isWorkflowSettingsOpen || !!selectedNodeForConfig(),
845
+ isWorkflowSettingsOpen ||
846
+ !!selectedNodeForConfig ||
847
+ swapMode !== "idle",
665
848
  );
666
849
  const showRightPanel = $derived(!disableSidebar && hasConfigPanelOpen);
667
850
 
668
851
  /**
669
852
  * Calculate left sidebar width based on collapsed state
670
- * When collapsed, use 48px; otherwise use user-configured width
853
+ * When collapsed, use 0; otherwise use user-configured width
671
854
  */
672
855
  const leftSidebarWidth = $derived(
673
- getUiSettings().sidebarCollapsed ? 48 : getUiSettings().sidebarWidth,
856
+ getUiSettings().sidebarCollapsed ? 0 : getUiSettings().sidebarWidth,
674
857
  );
675
858
 
859
+ /** Whether the sidebar is collapsed */
860
+ const isSidebarCollapsed = $derived(getUiSettings().sidebarCollapsed);
861
+
862
+ /** Toggle sidebar collapsed state */
863
+ function toggleSidebar(): void {
864
+ updateSettings({
865
+ ui: { sidebarCollapsed: !getUiSettings().sidebarCollapsed },
866
+ });
867
+ }
868
+
676
869
  // File input reference for workflow import
677
870
  let fileInputRef = $state<HTMLInputElement | null>(null);
871
+
872
+ /**
873
+ * Handle global keyboard shortcut for console toggle.
874
+ * Backtick (`) toggles the console open/closed unless user is typing in an input.
875
+ */
876
+ function handleGlobalKeydown(event: KeyboardEvent): void {
877
+ // Dead key on international keyboards — do not intercept
878
+ if (event.key === "Dead") return;
879
+
880
+ if (event.key !== "`") return;
881
+
882
+ // Don't intercept when user is typing in an input, textarea, or contenteditable
883
+ const target = event.target as HTMLElement;
884
+ const isInputElement =
885
+ target.tagName === "INPUT" ||
886
+ target.tagName === "TEXTAREA" ||
887
+ target.isContentEditable;
888
+
889
+ if (isInputElement) return;
890
+
891
+ event.preventDefault();
892
+ toggleConsole();
893
+ }
894
+
895
+ function handleConsoleUIAction(action: UIAction): void {
896
+ if (action.type === "open_config") {
897
+ const wf = getWorkflowStore();
898
+ if (!wf) return;
899
+ const node = wf.nodes.find((n) => n.id === action.nodeId);
900
+ if (node) openConfigSidebar(node);
901
+ } else if (action.type === "select_node") {
902
+ selectedNodeId = action.nodeId;
903
+ } else if (action.type === "canvas_fit_view") {
904
+ workflowEditorRef?.canvasFitView();
905
+ } else if (action.type === "canvas_zoom_in") {
906
+ workflowEditorRef?.canvasZoomIn();
907
+ } else if (action.type === "canvas_zoom_out") {
908
+ workflowEditorRef?.canvasZoomOut();
909
+ } else if (action.type === "canvas_zoom_to") {
910
+ workflowEditorRef?.canvasZoomTo(action.level);
911
+ } else if (action.type === "canvas_pan_to") {
912
+ workflowEditorRef?.canvasPanTo(action.position.x, action.position.y);
913
+ } else if (action.type === "canvas_reset_view") {
914
+ workflowEditorRef?.canvasResetView();
915
+ }
916
+ }
917
+
918
+ function toggleConsole(): void {
919
+ const currentOpen = getUiSettings().consoleOpen;
920
+ updateSettings({ ui: { consoleOpen: !currentOpen } });
921
+
922
+ // Focus management after DOM update
923
+ tick().then(() => {
924
+ if (currentOpen) {
925
+ // Console was open, now closing — focus the canvas
926
+ const canvas = document.querySelector<HTMLElement>(".flowdrop-editor-main");
927
+ canvas?.focus();
928
+ } else {
929
+ // Console was closed, now opening — focus first focusable element inside console
930
+ const consoleEl = document.querySelector<HTMLElement>(".command-console");
931
+ const focusTarget =
932
+ consoleEl?.querySelector<HTMLElement>("input, button, [tabindex]");
933
+ focusTarget?.focus();
934
+ }
935
+ });
936
+ }
678
937
  </script>
679
938
 
939
+ <svelte:window onkeydown={handleGlobalKeydown} />
940
+
680
941
  <svelte:head>
681
942
  <title>FlowDrop - Visual Workflow Manager</title>
682
943
  <meta
@@ -700,13 +961,14 @@
700
961
  showHeader={showNavbar}
701
962
  showLeftSidebar={!disableSidebar}
702
963
  showRightSidebar={showRightPanel}
703
- showBottomPanel={false}
964
+ showBottomPanel={getUiSettings().consoleOpen && !readOnly && !lockWorkflow}
965
+ bottomPanelHeight={getUiSettings().consoleHeight}
704
966
  showFooter={false}
705
967
  headerHeight={60}
706
968
  {leftSidebarWidth}
707
969
  rightSidebarWidth={400}
708
- leftSidebarMinWidth={getUiSettings().sidebarCollapsed ? 48 : 280}
709
- leftSidebarMaxWidth={getUiSettings().sidebarCollapsed ? 48 : 450}
970
+ leftSidebarMinWidth={getUiSettings().sidebarCollapsed ? 0 : 280}
971
+ leftSidebarMaxWidth={getUiSettings().sidebarCollapsed ? 0 : 450}
710
972
  rightSidebarMinWidth={320}
711
973
  rightSidebarMaxWidth={550}
712
974
  enableLeftSplitPane={false}
@@ -716,7 +978,7 @@
716
978
  <!-- Header: Navbar -->
717
979
  {#snippet header()}
718
980
  <Navbar
719
- title={breadcrumbTitle()}
981
+ title={breadcrumbTitle}
720
982
  primaryActions={navbarActions.length > 0
721
983
  ? navbarActions
722
984
  : [
@@ -763,6 +1025,9 @@
763
1025
  ]}
764
1026
  showStatus={true}
765
1027
  {showSettings}
1028
+ {settingsCategories}
1029
+ {showSettingsSyncButton}
1030
+ {showSettingsResetButton}
766
1031
  />
767
1032
  {/snippet}
768
1033
 
@@ -777,9 +1042,26 @@
777
1042
  />
778
1043
  {/snippet}
779
1044
 
780
- <!-- Right Sidebar: Configuration or Workflow Settings -->
1045
+ <!-- Right Sidebar: Configuration, Swap, or Workflow Settings -->
781
1046
  {#snippet rightSidebar()}
782
- {#if isWorkflowSettingsOpen}
1047
+ {#if swapMode === "mapping" && swapInteractiveState && selectedNodeForConfig}
1048
+ {@const swapChecker = (() => { try { return getPortCompatibilityChecker(); } catch { return null; } })()}
1049
+ <SwapMappingEditor
1050
+ interactiveState={swapInteractiveState}
1051
+ checker={swapChecker}
1052
+ onConfirm={executeNodeSwap}
1053
+ onCancel={cancelSwap}
1054
+ onBack={() => { swapMode = "picking"; swapInteractiveState = null; }}
1055
+ />
1056
+ {:else if swapMode === "picking" && selectedNodeForConfig}
1057
+ <NodeSwapPicker
1058
+ currentNode={selectedNodeForConfig}
1059
+ availableNodes={nodes}
1060
+ activeFormat={getWorkflowFormat()}
1061
+ onSelect={handleSwapSelect}
1062
+ onCancel={cancelSwap}
1063
+ />
1064
+ {:else if isWorkflowSettingsOpen}
783
1065
  <ConfigPanel
784
1066
  title="Workflow Settings"
785
1067
  id={getWorkflowStore()?.id}
@@ -840,8 +1122,8 @@
840
1122
  }}
841
1123
  />
842
1124
  </ConfigPanel>
843
- {:else if selectedNodeForConfig()}
844
- {@const currentNode = selectedNodeForConfig()!}
1125
+ {:else if selectedNodeForConfig}
1126
+ {@const currentNode = selectedNodeForConfig}
845
1127
  <ConfigPanel
846
1128
  title={currentNode.data.label}
847
1129
  id={currentNode.id}
@@ -858,6 +1140,7 @@
858
1140
  },
859
1141
  ]}
860
1142
  onClose={closeConfigSidebar}
1143
+ onSwap={!readOnly && !lockWorkflow && features.enableNodeSwap ? startSwap : undefined}
861
1144
  >
862
1145
  <ConfigForm
863
1146
  {authProvider}
@@ -902,6 +1185,34 @@
902
1185
  {/if}
903
1186
  {/snippet}
904
1187
 
1188
+ <!-- Bottom Panel: Tabbed Console / AI Chat -->
1189
+ {#snippet bottomPanel()}
1190
+ <div class="bottom-panel-tabs">
1191
+ <div class="bottom-panel-tabs__bar">
1192
+ <button
1193
+ class="bottom-panel-tabs__tab {getUiSettings().bottomPanelTab === 'console' ? 'bottom-panel-tabs__tab--active' : ''}"
1194
+ onclick={() => updateSettings({ ui: { bottomPanelTab: 'console' } })}
1195
+ >
1196
+ Console
1197
+ </button>
1198
+ <button
1199
+ class="bottom-panel-tabs__tab {getUiSettings().bottomPanelTab === 'chat' ? 'bottom-panel-tabs__tab--active' : ''}"
1200
+ onclick={() => updateSettings({ ui: { bottomPanelTab: 'chat' } })}
1201
+ >
1202
+ AI Chat
1203
+ </button>
1204
+ </div>
1205
+ <div class="bottom-panel-tabs__content">
1206
+ <div class="bottom-panel-tabs__panel" style:display={getUiSettings().bottomPanelTab === 'console' ? 'contents' : 'none'}>
1207
+ <CommandConsole nodeTypes={nodes} onUIAction={handleConsoleUIAction} />
1208
+ </div>
1209
+ <div class="bottom-panel-tabs__panel" style:display={getUiSettings().bottomPanelTab === 'chat' ? 'flex' : 'none'}>
1210
+ <AIChatPanel nodeTypes={nodes} workflowId={getWorkflowStore()?.id} onUIAction={handleConsoleUIAction} endpointConfig={endpointConfig} />
1211
+ </div>
1212
+ </div>
1213
+ </div>
1214
+ {/snippet}
1215
+
905
1216
  <!-- Main Content: Workflow Editor with Error Status -->
906
1217
  <!-- Status Display: aria-live announces API errors dynamically without requiring focus -->
907
1218
  {#if error}
@@ -974,6 +1285,20 @@
974
1285
  role="region"
975
1286
  aria-label="Workflow canvas"
976
1287
  >
1288
+ <!-- Floating sidebar toggle — always visible on the canvas top-left -->
1289
+ {#if !disableSidebar}
1290
+ <button
1291
+ class="flowdrop-sidebar-fab"
1292
+ onclick={toggleSidebar}
1293
+ aria-label={isSidebarCollapsed
1294
+ ? "Expand sidebar"
1295
+ : "Collapse sidebar"}
1296
+ title={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
1297
+ >
1298
+ <Icon icon={isSidebarCollapsed ? "mdi:menu" : "mdi:menu-open"} />
1299
+ </button>
1300
+ {/if}
1301
+
977
1302
  <WorkflowEditor
978
1303
  bind:this={workflowEditorRef}
979
1304
  {nodes}
@@ -981,13 +1306,15 @@
981
1306
  {width}
982
1307
  endpointConfig={endpointConfig ?? undefined}
983
1308
  {isConfigSidebarOpen}
984
- selectedNodeForConfig={selectedNodeForConfig()}
1309
+ selectedNodeForConfig={selectedNodeForConfig}
985
1310
  {openConfigSidebar}
986
1311
  {closeConfigSidebar}
987
1312
  {lockWorkflow}
988
1313
  {readOnly}
989
1314
  {nodeStatuses}
990
1315
  {pipelineId}
1316
+ consoleOpen={getUiSettings().consoleOpen}
1317
+ onToggleConsole={toggleConsole}
991
1318
  />
992
1319
  </div>
993
1320
  </MainLayout>
@@ -1098,6 +1425,40 @@
1098
1425
  font-weight: 500;
1099
1426
  }
1100
1427
 
1428
+ /* Floating sidebar toggle button */
1429
+ .flowdrop-sidebar-fab {
1430
+ position: absolute;
1431
+ top: 12px;
1432
+ left: 12px;
1433
+ z-index: 50;
1434
+ display: flex;
1435
+ align-items: center;
1436
+ justify-content: center;
1437
+ width: 2.25rem;
1438
+ height: 2.25rem;
1439
+ border: 1px solid var(--fd-border);
1440
+ border-radius: var(--fd-radius-md);
1441
+ background-color: var(--fd-background);
1442
+ color: var(--fd-muted-foreground);
1443
+ cursor: pointer;
1444
+ box-shadow: var(--fd-shadow-md);
1445
+ transition:
1446
+ color var(--fd-transition-fast),
1447
+ background-color var(--fd-transition-fast),
1448
+ box-shadow var(--fd-transition-fast);
1449
+ }
1450
+
1451
+ .flowdrop-sidebar-fab:hover {
1452
+ color: var(--fd-foreground);
1453
+ background-color: var(--fd-subtle);
1454
+ box-shadow: var(--fd-shadow-lg);
1455
+ }
1456
+
1457
+ .flowdrop-sidebar-fab:focus {
1458
+ outline: none;
1459
+ box-shadow: 0 0 0 2px var(--fd-ring);
1460
+ }
1461
+
1101
1462
  /* Main editor area */
1102
1463
  .flowdrop-editor-main {
1103
1464
  flex: 1;
@@ -1107,4 +1468,57 @@
1107
1468
  overflow: hidden;
1108
1469
  background: var(--fd-layout-background);
1109
1470
  }
1471
+
1472
+ /* Bottom panel tab system */
1473
+ .bottom-panel-tabs {
1474
+ display: flex;
1475
+ flex-direction: column;
1476
+ height: 100%;
1477
+ overflow: hidden;
1478
+ }
1479
+
1480
+ .bottom-panel-tabs__bar {
1481
+ display: flex;
1482
+ gap: 0;
1483
+ background: var(--fd-muted);
1484
+ border-bottom: 1px solid var(--fd-border);
1485
+ flex-shrink: 0;
1486
+ }
1487
+
1488
+ .bottom-panel-tabs__tab {
1489
+ padding: 0.375rem 0.75rem;
1490
+ font-size: 0.75rem;
1491
+ font-weight: 500;
1492
+ cursor: pointer;
1493
+ border: none;
1494
+ border-bottom: 2px solid transparent;
1495
+ background: transparent;
1496
+ color: var(--fd-muted-foreground);
1497
+ transition: all var(--fd-transition-fast);
1498
+ }
1499
+
1500
+ .bottom-panel-tabs__tab:hover {
1501
+ color: var(--fd-foreground);
1502
+ background: var(--fd-background);
1503
+ }
1504
+
1505
+ .bottom-panel-tabs__tab--active {
1506
+ color: var(--fd-foreground);
1507
+ border-bottom-color: var(--fd-primary);
1508
+ background: var(--fd-background);
1509
+ }
1510
+
1511
+ .bottom-panel-tabs__content {
1512
+ flex: 1;
1513
+ overflow: hidden;
1514
+ display: flex;
1515
+ flex-direction: column;
1516
+ }
1517
+
1518
+ .bottom-panel-tabs__panel {
1519
+ flex: 1;
1520
+ overflow: hidden;
1521
+ flex-direction: column;
1522
+ }
1523
+
1110
1524
  </style>
@@ -1,8 +1,10 @@
1
1
  import type { NodeMetadata, Workflow } from "../types/index.js";
2
+ import type { SwapStrategy } from "../utils/nodeSwap.js";
2
3
  import type { EndpointConfig } from "../config/endpoints.js";
3
4
  import type { AuthProvider } from "../types/auth.js";
4
5
  import type { FlowDropEventHandlers, FlowDropFeatures } from "../types/events.js";
5
6
  import type { FlowDropTheme, FlowDropThemeName } from "../types/theme.js";
7
+ import type { SettingsCategory } from "../types/settings.js";
6
8
  /**
7
9
  * Configuration props for runtime customization
8
10
  */
@@ -51,6 +53,14 @@ interface Props {
51
53
  features?: FlowDropFeatures;
52
54
  /** Visual theme — named built-in ('default' | 'minimal') or custom theme object */
53
55
  theme?: FlowDropTheme | FlowDropThemeName;
56
+ /** Which settings tabs to show in the modal */
57
+ settingsCategories?: SettingsCategory[];
58
+ /** Show the "Sync to Cloud" button in the settings modal */
59
+ showSettingsSyncButton?: boolean;
60
+ /** Show the reset buttons in the settings modal */
61
+ showSettingsResetButton?: boolean;
62
+ /** Pluggable swap strategies — instance-scoped, checked in order */
63
+ swapStrategies?: SwapStrategy[];
54
64
  }
55
65
  declare const App: import("svelte").Component<Props, {}, "">;
56
66
  type App = ReturnType<typeof App>;
@@ -2,6 +2,7 @@
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
3
  import CanvasBanner from "./CanvasBanner.svelte";
4
4
  import CanvasDecorator from "../stories/CanvasDecorator.svelte";
5
+ import Icon from "@iconify/svelte";
5
6
 
6
7
  const { Story } = defineMeta({
7
8
  title: "Display/CanvasBanner",
@@ -17,8 +18,11 @@
17
18
  <CanvasBanner
18
19
  title="Empty Canvas"
19
20
  description="Drag nodes from the sidebar to get started"
20
- iconName="heroicons:squares-plus"
21
- />
21
+ >
22
+ {#snippet icon()}
23
+ <Icon icon="heroicons:squares-plus" />
24
+ {/snippet}
25
+ </CanvasBanner>
22
26
  </CanvasDecorator>
23
27
  </Story>
24
28