@flowdrop/flowdrop 1.4.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 (100) 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 +351 -12
  26. package/dist/components/App.svelte.d.ts +3 -0
  27. package/dist/components/CanvasController.svelte +38 -0
  28. package/dist/components/CanvasController.svelte.d.ts +32 -0
  29. package/dist/components/ConfigMappingRow.svelte +130 -0
  30. package/dist/components/ConfigMappingRow.svelte.d.ts +8 -0
  31. package/dist/components/ConfigPanel.svelte +56 -7
  32. package/dist/components/ConfigPanel.svelte.d.ts +2 -0
  33. package/dist/components/FlowDropEdge.svelte +2 -10
  34. package/dist/components/LogsSidebar.svelte +5 -5
  35. package/dist/components/NodeSidebar.svelte +15 -49
  36. package/dist/components/NodeSwapPicker.svelte +537 -0
  37. package/dist/components/NodeSwapPicker.svelte.d.ts +16 -0
  38. package/dist/components/PortMappingRow.svelte +209 -0
  39. package/dist/components/PortMappingRow.svelte.d.ts +12 -0
  40. package/dist/components/SwapMappingEditor.svelte +550 -0
  41. package/dist/components/SwapMappingEditor.svelte.d.ts +12 -0
  42. package/dist/components/WorkflowEditor.svelte +99 -4
  43. package/dist/components/WorkflowEditor.svelte.d.ts +8 -0
  44. package/dist/components/chat/AIChatPanel.svelte +658 -0
  45. package/dist/components/chat/AIChatPanel.svelte.d.ts +13 -0
  46. package/dist/components/chat/CommandPreview.svelte +184 -0
  47. package/dist/components/chat/CommandPreview.svelte.d.ts +9 -0
  48. package/dist/components/console/CommandConsole.stories.svelte +93 -0
  49. package/dist/components/console/CommandConsole.stories.svelte.d.ts +27 -0
  50. package/dist/components/console/CommandConsole.svelte +259 -0
  51. package/dist/components/console/CommandConsole.svelte.d.ts +11 -0
  52. package/dist/components/console/ConsoleAutocomplete.svelte +139 -0
  53. package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +21 -0
  54. package/dist/components/console/ConsoleInput.svelte +712 -0
  55. package/dist/components/console/ConsoleInput.svelte.d.ts +16 -0
  56. package/dist/components/console/ConsoleOutput.svelte +121 -0
  57. package/dist/components/console/ConsoleOutput.svelte.d.ts +11 -0
  58. package/dist/components/console/formatters.d.ts +26 -0
  59. package/dist/components/console/formatters.js +118 -0
  60. package/dist/components/interrupt/index.d.ts +1 -0
  61. package/dist/components/interrupt/index.js +1 -0
  62. package/dist/config/endpoints.d.ts +8 -0
  63. package/dist/config/endpoints.js +5 -0
  64. package/dist/core/index.d.ts +5 -0
  65. package/dist/core/index.js +9 -0
  66. package/dist/editor/index.d.ts +3 -1
  67. package/dist/editor/index.js +4 -2
  68. package/dist/helpers/proximityConnect.js +8 -1
  69. package/dist/helpers/workflowEditorHelper.d.ts +3 -53
  70. package/dist/helpers/workflowEditorHelper.js +13 -228
  71. package/dist/playground/index.d.ts +1 -1
  72. package/dist/playground/index.js +1 -1
  73. package/dist/schemas/v1/workflow.schema.json +107 -22
  74. package/dist/services/chatService.d.ts +65 -0
  75. package/dist/services/chatService.js +131 -0
  76. package/dist/services/historyService.d.ts +6 -4
  77. package/dist/services/historyService.js +21 -6
  78. package/dist/stores/interruptStore.svelte.js +6 -1
  79. package/dist/stores/playgroundStore.svelte.d.ts +1 -1
  80. package/dist/stores/playgroundStore.svelte.js +11 -2
  81. package/dist/stores/portCoordinateStore.svelte.d.ts +4 -0
  82. package/dist/stores/portCoordinateStore.svelte.js +20 -26
  83. package/dist/stores/workflowStore.svelte.d.ts +31 -2
  84. package/dist/stores/workflowStore.svelte.js +84 -64
  85. package/dist/types/chat.d.ts +63 -0
  86. package/dist/types/chat.js +9 -0
  87. package/dist/types/events.d.ts +28 -2
  88. package/dist/types/events.js +1 -0
  89. package/dist/types/index.d.ts +8 -0
  90. package/dist/types/settings.d.ts +6 -0
  91. package/dist/types/settings.js +3 -0
  92. package/dist/utils/edgeStyling.d.ts +42 -0
  93. package/dist/utils/edgeStyling.js +176 -0
  94. package/dist/utils/nodeIds.d.ts +31 -0
  95. package/dist/utils/nodeIds.js +42 -0
  96. package/dist/utils/nodeSwap.d.ts +221 -0
  97. package/dist/utils/nodeSwap.js +686 -0
  98. package/package.json +6 -1
  99. package/dist/helpers/nodeLayoutHelper.d.ts +0 -14
  100. package/dist/helpers/nodeLayoutHelper.js +0 -19
@@ -5,13 +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
12
  import Icon from "@iconify/svelte";
13
13
  import ConfigForm from "./ConfigForm.svelte";
14
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";
15
20
  import Navbar from "./Navbar.svelte";
16
21
  import { api, setEndpointConfig } from "../services/api.js";
17
22
  import { EnhancedFlowDropApiClient } from "../api/enhanced-client.js";
@@ -22,6 +27,14 @@
22
27
  ConfigSchema,
23
28
  NodeUIExtensions,
24
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";
25
38
  import { DEFAULT_WORKFLOW_FORMAT } from "../types/index.js";
26
39
  import { createEndpointConfig } from "../config/endpoints.js";
27
40
  import type { EndpointConfig } from "../config/endpoints.js";
@@ -51,7 +64,7 @@
51
64
  getUiSettings,
52
65
  updateSettings,
53
66
  } from "../stores/settingsStore.svelte.js";
54
- import { initializePortCompatibility } from "../utils/connections.js";
67
+ import { initializePortCompatibility, getPortCompatibilityChecker } from "../utils/connections.js";
55
68
  import { DEFAULT_PORT_CONFIG } from "../config/defaultPortConfig.js";
56
69
  import { workflowFormatRegistry } from "../registry/workflowFormatRegistry.js";
57
70
  import { logger } from "../utils/logger.js";
@@ -115,6 +128,8 @@
115
128
  showSettingsSyncButton?: boolean;
116
129
  /** Show the reset buttons in the settings modal */
117
130
  showSettingsResetButton?: boolean;
131
+ /** Pluggable swap strategies — instance-scoped, checked in order */
132
+ swapStrategies?: SwapStrategy[];
118
133
  }
119
134
 
120
135
  let {
@@ -140,6 +155,7 @@
140
155
  settingsCategories,
141
156
  showSettingsSyncButton,
142
157
  showSettingsResetButton,
158
+ swapStrategies,
143
159
  }: Props = $props();
144
160
 
145
161
  // svelte-ignore state_referenced_locally — feature flags don't change at runtime
@@ -180,7 +196,7 @@
180
196
  });
181
197
 
182
198
  // Create breadcrumb-style title - at top level to avoid store subscription issues
183
- let breadcrumbTitle = $derived(() => {
199
+ let breadcrumbTitle = $derived.by(() => {
184
200
  // Use custom navbar title if provided
185
201
  if (navbarTitle) {
186
202
  return navbarTitle;
@@ -213,6 +229,11 @@
213
229
  // Workflow settings sidebar state
214
230
  let isWorkflowSettingsOpen = $state(false);
215
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
+
216
237
  // Workflow configuration schema (derived to pick up dynamic format options)
217
238
  let workflowConfigSchema: ConfigSchema = $derived({
218
239
  type: "object" as const,
@@ -249,7 +270,7 @@
249
270
  });
250
271
 
251
272
  // Get the current node from the workflow store
252
- let selectedNodeForConfig = $derived(() => {
273
+ let selectedNodeForConfig = $derived.by(() => {
253
274
  const wf = getWorkflowStore();
254
275
  if (!selectedNodeId || !wf) return null;
255
276
  return wf.nodes.find((node) => node.id === selectedNodeId) || null;
@@ -444,11 +465,19 @@
444
465
  }
445
466
  selectedNodeId = node.id;
446
467
  isConfigSidebarOpen = true;
468
+ // Reset swap state when switching nodes
469
+ swapMode = "idle";
470
+ swapTargetMetadata = null;
471
+ swapInteractiveState = null;
447
472
  }
448
473
 
449
474
  function closeConfigSidebar(): void {
450
475
  isConfigSidebarOpen = false;
451
476
  selectedNodeId = null;
477
+ // Reset swap state when closing
478
+ swapMode = "idle";
479
+ swapTargetMetadata = null;
480
+ swapInteractiveState = null;
452
481
  }
453
482
 
454
483
  /**
@@ -462,6 +491,144 @@
462
491
  }
463
492
  }
464
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
+
465
632
  /**
466
633
  * Handle workflow configuration save
467
634
  */
@@ -675,7 +842,9 @@
675
842
  * Config panel always appears on the right side
676
843
  */
677
844
  const hasConfigPanelOpen = $derived(
678
- isWorkflowSettingsOpen || !!selectedNodeForConfig(),
845
+ isWorkflowSettingsOpen ||
846
+ !!selectedNodeForConfig ||
847
+ swapMode !== "idle",
679
848
  );
680
849
  const showRightPanel = $derived(!disableSidebar && hasConfigPanelOpen);
681
850
 
@@ -699,8 +868,76 @@
699
868
 
700
869
  // File input reference for workflow import
701
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
+ }
702
937
  </script>
703
938
 
939
+ <svelte:window onkeydown={handleGlobalKeydown} />
940
+
704
941
  <svelte:head>
705
942
  <title>FlowDrop - Visual Workflow Manager</title>
706
943
  <meta
@@ -724,7 +961,8 @@
724
961
  showHeader={showNavbar}
725
962
  showLeftSidebar={!disableSidebar}
726
963
  showRightSidebar={showRightPanel}
727
- showBottomPanel={false}
964
+ showBottomPanel={getUiSettings().consoleOpen && !readOnly && !lockWorkflow}
965
+ bottomPanelHeight={getUiSettings().consoleHeight}
728
966
  showFooter={false}
729
967
  headerHeight={60}
730
968
  {leftSidebarWidth}
@@ -740,7 +978,7 @@
740
978
  <!-- Header: Navbar -->
741
979
  {#snippet header()}
742
980
  <Navbar
743
- title={breadcrumbTitle()}
981
+ title={breadcrumbTitle}
744
982
  primaryActions={navbarActions.length > 0
745
983
  ? navbarActions
746
984
  : [
@@ -804,9 +1042,26 @@
804
1042
  />
805
1043
  {/snippet}
806
1044
 
807
- <!-- Right Sidebar: Configuration or Workflow Settings -->
1045
+ <!-- Right Sidebar: Configuration, Swap, or Workflow Settings -->
808
1046
  {#snippet rightSidebar()}
809
- {#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}
810
1065
  <ConfigPanel
811
1066
  title="Workflow Settings"
812
1067
  id={getWorkflowStore()?.id}
@@ -867,8 +1122,8 @@
867
1122
  }}
868
1123
  />
869
1124
  </ConfigPanel>
870
- {:else if selectedNodeForConfig()}
871
- {@const currentNode = selectedNodeForConfig()!}
1125
+ {:else if selectedNodeForConfig}
1126
+ {@const currentNode = selectedNodeForConfig}
872
1127
  <ConfigPanel
873
1128
  title={currentNode.data.label}
874
1129
  id={currentNode.id}
@@ -885,6 +1140,7 @@
885
1140
  },
886
1141
  ]}
887
1142
  onClose={closeConfigSidebar}
1143
+ onSwap={!readOnly && !lockWorkflow && features.enableNodeSwap ? startSwap : undefined}
888
1144
  >
889
1145
  <ConfigForm
890
1146
  {authProvider}
@@ -929,6 +1185,34 @@
929
1185
  {/if}
930
1186
  {/snippet}
931
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
+
932
1216
  <!-- Main Content: Workflow Editor with Error Status -->
933
1217
  <!-- Status Display: aria-live announces API errors dynamically without requiring focus -->
934
1218
  {#if error}
@@ -1022,13 +1306,15 @@
1022
1306
  {width}
1023
1307
  endpointConfig={endpointConfig ?? undefined}
1024
1308
  {isConfigSidebarOpen}
1025
- selectedNodeForConfig={selectedNodeForConfig()}
1309
+ selectedNodeForConfig={selectedNodeForConfig}
1026
1310
  {openConfigSidebar}
1027
1311
  {closeConfigSidebar}
1028
1312
  {lockWorkflow}
1029
1313
  {readOnly}
1030
1314
  {nodeStatuses}
1031
1315
  {pipelineId}
1316
+ consoleOpen={getUiSettings().consoleOpen}
1317
+ onToggleConsole={toggleConsole}
1032
1318
  />
1033
1319
  </div>
1034
1320
  </MainLayout>
@@ -1182,4 +1468,57 @@
1182
1468
  overflow: hidden;
1183
1469
  background: var(--fd-layout-background);
1184
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
+
1185
1524
  </style>
@@ -1,4 +1,5 @@
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";
@@ -58,6 +59,8 @@ interface Props {
58
59
  showSettingsSyncButton?: boolean;
59
60
  /** Show the reset buttons in the settings modal */
60
61
  showSettingsResetButton?: boolean;
62
+ /** Pluggable swap strategies — instance-scoped, checked in order */
63
+ swapStrategies?: SwapStrategy[];
61
64
  }
62
65
  declare const App: import("svelte").Component<Props, {}, "">;
63
66
  type App = ReturnType<typeof App>;
@@ -0,0 +1,38 @@
1
+ <!--
2
+ CanvasController Component
3
+ Provides viewport control methods (fitView, zoom, pan) via useSvelteFlow().
4
+ Must be rendered inside SvelteFlowProvider context.
5
+ -->
6
+
7
+ <script lang="ts">
8
+ import { useSvelteFlow } from "@xyflow/svelte";
9
+ import { getEditorSettings } from "../stores/settingsStore.svelte.js";
10
+
11
+ const { fitView, zoomIn, zoomOut, setZoom, setCenter, setViewport } =
12
+ useSvelteFlow();
13
+
14
+ export function canvasFitView(): void {
15
+ fitView({ padding: 0.2, duration: 300 });
16
+ }
17
+
18
+ export function canvasZoomIn(): void {
19
+ zoomIn({ duration: 300 });
20
+ }
21
+
22
+ export function canvasZoomOut(): void {
23
+ zoomOut({ duration: 300 });
24
+ }
25
+
26
+ export function canvasZoomTo(level: number): void {
27
+ setZoom(level, { duration: 300 });
28
+ }
29
+
30
+ export function canvasPanTo(x: number, y: number): void {
31
+ setCenter(x, y, { duration: 300 });
32
+ }
33
+
34
+ export function canvasResetView(): void {
35
+ const defaultZoom = getEditorSettings().defaultZoom;
36
+ setViewport({ x: 0, y: 0, zoom: defaultZoom }, { duration: 300 });
37
+ }
38
+ </script>
@@ -0,0 +1,32 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const CanvasController: $$__sveltets_2_IsomorphicComponent<{
15
+ canvasFitView?: () => void;
16
+ canvasZoomIn?: () => void;
17
+ canvasZoomOut?: () => void;
18
+ canvasZoomTo?: (level: number) => void;
19
+ canvasPanTo?: (x: number, y: number) => void;
20
+ canvasResetView?: () => void;
21
+ }, {
22
+ [evt: string]: CustomEvent<any>;
23
+ }, {}, {
24
+ canvasFitView: () => void;
25
+ canvasZoomIn: () => void;
26
+ canvasZoomOut: () => void;
27
+ canvasZoomTo: (level: number) => void;
28
+ canvasPanTo: (x: number, y: number) => void;
29
+ canvasResetView: () => void;
30
+ }, string>;
31
+ type CanvasController = InstanceType<typeof CanvasController>;
32
+ export default CanvasController;