Repository: KaotoIO/kaoto
Pull request #2194: fix(VSCode): Keep editor content upon reloading
Author: login: lordrip
Created: 2025-04-16T16:58:47Z | Merged: 2025-04-17T14:04:16Z

### Context
Before adding XML support to Kaoto, the Camel Catalog wasn't needed to parse Camel routes; we simply got each EIP in its JSON representation and created nodes for the canvas. By the time the catalog was loaded, we were able to show the configuration form.

With the introduction of XML, the Camel Catalog was needed to parse it, so we ended up in a situation where the editor wasn't available because it was behind the catalog loading distractor, so it missed the `setContent` calls from `MA`.

As a solution for the former, a small cache was implemented to remember the last content in case the editor wasn't available, and while it worked,  when reloading the editor due to switching runtimes, we initialized the route with the original content, because the cache wasn't updated.

### Changes
Considering that everything under `ReloadProvider` was unmounted/mounted whenever the runtime changed, the `RuntimeProvider` was lowered right before `EntitiesProvider`, which fixed the issue about losing the Source Code.

In addition to that, `KaotoBridge` was split into `SourceCodeBridgeProvider` so the `setContent` and `getContent` functions are available at all times, removing the need to keep a cache whenever the editor it's not available due to loading the catalog.

fix: https://github.com/KaotoIO/kaoto/issues/2142


Diff:
diff --git a/packages/ui/src/multiplying-architecture/Bridge/KaotoBridge.test.tsx b/packages/ui/src/multiplying-architecture/Bridge/KaotoBridge.test.tsx
new file mode 100644
index 000000000..4ca83d4d7
--- /dev/null
+++ b/packages/ui/src/multiplying-architecture/Bridge/KaotoBridge.test.tsx
@@ -0,0 +1,50 @@
+import { ChannelType } from '@kie-tools-core/editor/dist/api';
+import { render } from '@testing-library/react';
+import { KaotoBridge } from './KaotoBridge';
+
+describe('KaotoBridge', () => {
+  const mockOnReady = jest.fn();
+  const mockOnStateControlCommandUpdate = jest.fn();
+  const mockSetNotifications = jest.fn();
+  const mockGetMetadata = jest.fn();
+  const mockSetMetadata = jest.fn();
+  const mockGetResourceContent = jest.fn();
+  const mockSaveResourceContent = jest.fn();
+  const mockDeleteResource = jest.fn();
+  const mockAskUserForFileSelection = jest.fn();
+
+  const defaultProps = {
+    onReady: mockOnReady,
+    onStateControlCommandUpdate: mockOnStateControlCommandUpdate,
+    setNotifications: mockSetNotifications,
+    getMetadata: mockGetMetadata,
+    setMetadata: mockSetMetadata,
+    getResourceContent: mockGetResourceContent,
+    saveResourceContent: mockSaveResourceContent,
+    deleteResource: mockDeleteResource,
+    askUserForFileSelection: mockAskUserForFileSelection,
+    channelType: ChannelType.VSCODE_DESKTOP,
+  };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should render children components', () => {
+    const { getByText } = render(
+      <KaotoBridge {...defaultProps}>
+        <div>Test Child</div>
+      </KaotoBridge>,
+    );
+    expect(getByText('Test Child')).toBeInTheDocument();
+  });
+
+  it('should call onReady on mount', () => {
+    render(
+      <KaotoBridge {...defaultProps}>
+        <div>Test Child</div>
+      </KaotoBridge>,
+    );
+    expect(mockOnReady).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/packages/ui/src/multiplying-architecture/Bridge/KaotoBridge.tsx b/packages/ui/src/multiplying-architecture/Bridge/KaotoBridge.tsx
new file mode 100644
index 000000000..b7767354c
--- /dev/null
+++ b/packages/ui/src/multiplying-architecture/Bridge/KaotoBridge.tsx
@@ -0,0 +1,129 @@
+import { ChannelType, StateControlCommand } from '@kie-tools-core/editor/dist/api';
+import { Notification } from '@kie-tools-core/notifications/dist/api';
+import { VisualizationProvider } from '@patternfly/react-topology';
+import { FunctionComponent, PropsWithChildren, useEffect, useMemo } from 'react';
+import { NodeInteractionAddonProvider } from '../../components/registers/interactions/node-interaction-addon.provider';
+import { RegisterComponents } from '../../components/registers/RegisterComponents';
+import { RegisterNodeInteractionAddons } from '../../components/registers/RegisterNodeInteractionAddons';
+import { RenderingProvider } from '../../components/RenderingAnchor/rendering.provider';
+import { ControllerService } from '../../components/Visualization/Canvas/controller.service';
+import { CatalogTilesProvider, IMetadataApi, MetadataProvider, VisibleFlowsProvider } from '../../providers';
+
+interface KaotoBridgeProps {
+  /**
+   * Delegation for KogitoEditorChannelApi.kogitoEditor_ready() to signal to the Channel
+   * that the editor is ready.
+   */
+  onReady: () => void;
+
+  /**
+   * Delegation for KogitoEditorChannelApi.kogitoEditor_stateControlCommandUpdate(command) to signal to the Channel
+   * that the editor is performing an undo/redo operation.
+   */
+  onStateControlCommandUpdate: (command: StateControlCommand) => void;
+
+  /**
+   * Delegation for NotificationsChannelApi.kogigotNotifications_setNotifications(path, notifications) to report all validation
+   * notifications to the Channel that will replace existing notification for the path.
+   * @param path The path that references the Notification
+   * @param notifications List of Notifications
+   */
+  setNotifications: (path: string, notifications: Notification[]) => void;
+
+  /**
+   * Get metadata querying a Kaoto metadata file using the channel API.
+   * @param key The key to retrieve the metadata from the Kaoto metadata file
+   */
+  getMetadata<T>(key: string): Promise<T | undefined>;
+
+  /**
+   * Save metadata to a Kaoto metadata file using the channel API.
+   * @param key The key to set the metadata
+   * @param metadata The metadata to be saved
+   */
+  setMetadata<T>(key: string, metadata: T): Promise<void>;
+
+  /**
+   * Retrieve resource content using the channel API.
+   * @param path The path of the resource
+   */
+  getResourceContent(path: string): Promise<string | undefined>;
+
+  /**
+   * Save resource content using the channel API.
+   * @param path The path of the resource
+   * @param content The content to be saved
+   */
+  saveResourceContent(path: string, content: string): Promise<void>;
+
+  /**
+   * Delete resource using the channel API.
+   * @param path The path of the resource
+   */
+  deleteResource(path: string): Promise<boolean>;
+
+  /**
+   * Show a Quick Pick widget and ask the user to select one or more files available in the workspace.
+   * @param include The filter expression for the files to include
+   * @param exclude The filter expression for the files to exclude
+   * @param options The options to pass over to VSCode QuickPick
+   */
+  askUserForFileSelection(
+    include: string,
+    exclude?: string,
+    options?: Record<string, unknown>,
+  ): Promise<string[] | string | undefined>;
+
+  /**
+   * ChannelType where the component is running.
+   */
+  channelType: ChannelType;
+}
+
+export const KaotoBridge: FunctionComponent<PropsWithChildren<KaotoBridgeProps>> = ({
+  onReady,
+  children,
+  getMetadata,
+  setMetadata,
+  getResourceContent,
+  saveResourceContent,
+  deleteResource,
+  askUserForFileSelection,
+}) => {
+  const controller = useMemo(() => ControllerService.createController(), []);
+  const metadataApi: IMetadataApi = useMemo(
+    () => ({
+      getMetadata,
+      setMetadata,
+      getResourceContent,
+      saveResourceContent,
+      deleteResource,
+      askUserForFileSelection,
+      shouldSaveSchema: false,
+    }),
+    [getMetadata, setMetadata, getResourceContent, saveResourceContent, deleteResource, askUserForFileSelection],
+  );
+
+  /** Set editor as Ready */
+  useEffect(() => {
+    onReady();
+  }, [onReady]);
+
+  return (
+    <CatalogTilesProvider>
+      <VisualizationProvider controller={controller}>
+        <VisibleFlowsProvider>
+          <RenderingProvider>
+            <MetadataProvider api={metadataApi}>
+              <RegisterComponents>
+                <NodeInteractionAddonProvider>
+                  <RegisterNodeInteractionAddons>{children}</RegisterNodeInteractionAddons>
+                </NodeInteractionAddonProvider>
+              </RegisterComponents>
+            </MetadataProvider>
+          </RenderingProvider>
+        </VisibleFlowsProvider>
+      </VisualizationProvider>
+    </CatalogTilesProvider>
+  );
+};
diff --git a/packages/ui/src/multiplying-architecture/Bridge/SourceCodeBridgeProvider.test.tsx b/packages/ui/src/multiplying-architecture/Bridge/SourceCodeBridgeProvider.test.tsx
new file mode 100644
index 000000000..d8bec4353
--- /dev/null
+++ b/packages/ui/src/multiplying-architecture/Bridge/SourceCodeBridgeProvider.test.tsx
@@ -0,0 +1,149 @@
+import { act, render } from '@testing-library/react';
+import { FunctionComponent, useRef } from 'react';
+import { SourceCodeApiContext } from '../../providers';
+import { EventNotifier } from '../../utils/event-notifier';
+import { SourceCodeBridgeProvider } from './SourceCodeBridgeProvider';
+import { SourceCodeBridgeProviderRef } from './editor-api';
+
+describe('SourceCodeBridgeProvider', () => {
+  const mockCamelRoute = `
+    - from:
+        uri: "timer:foo"
+        steps:
+          - log:
+              message: "Hello World"
+  `;
+
+  it('should render children', () => {
+    const { getByText } = render(
+      <SourceCodeBridgeProvider onNewEdit={jest.fn()}>
+        <p>Test Child</p>
+      </SourceCodeBridgeProvider>,
+    );
+
+    expect(getByText('Test Child')).toBeInTheDocument();
+  });
+
+  it('should call onNewEdit when the entities:updated event is emitted', () => {
+    const mockOnNewEdit = jest.fn();
+
+    render(
+      <SourceCodeBridgeProvider onNewEdit={mockOnNewEdit}>
+        <p>Love letter</p>
+      </SourceCodeBridgeProvider>,
+    );
+
+    act(() => {
+      EventNotifier.getInstance().next('entities:updated', mockCamelRoute);
+    });
+
+    expect(mockOnNewEdit).toHaveBeenCalledWith(mockCamelRoute);
+  });
+
+  it('should ignore the first code:updated event', () => {
+    const mockOnNewEdit = jest.fn();
+
+    render(
+      <SourceCodeBridgeProvider onNewEdit={mockOnNewEdit}>
+        <p>Love letter</p>
+      </SourceCodeBridgeProvider>,
+    );
+
+    act(() => {
+      EventNotifier.getInstance().next('code:updated', { code: mockCamelRoute });
+    });
+
+    expect(mockOnNewEdit).not.toHaveBeenCalledWith(mockCamelRoute);
+  });
+
+  it('should call onNewEdit when the code:updated event if there is source code available', () => {
+    const mockOnNewEdit = jest.fn();
+
+    render(
+      <SourceCodeBridgeProvider onNewEdit={mockOnNewEdit}>
+        <p>Love letter</p>
+      </SourceCodeBridgeProvider>,
+    );
+
+    act(() => {
+      EventNotifier.getInstance().next(
+        'entities:updated',
+        `- from:
+            uri: "direct:foo"
+            steps: []`,
+      );
+      mockOnNewEdit.mockClear();
+    });
+
+    act(() => {
+      EventNotifier.getInstance().next('code:updated', { code: mockCamelRoute });
+    });
+
+    expect(mockOnNewEdit).toHaveBeenCalledWith(mockCamelRoute);
+    expect(mockOnNewEdit).toHaveBeenCalledTimes(1);
+  });
+
+  it('should unsubscribe from events on unmount', () => {
+    const eventNotifierInstance = EventNotifier.getInstance();
+    const unsubscribeFromEntitiesMock = jest.fn();
+    const unsubscribeFromSourceCodeMock = jest.fn();
+
+    jest.spyOn(eventNotifierInstance, 'subscribe').mockReturnValueOnce(unsubscribeFromEntitiesMock);
+    jest.spyOn(eventNotifierInstance, 'subscribe').mockReturnValueOnce(unsubscribeFromSourceCodeMock);
+
+    const { unmount } = render(
+      <SourceCodeBridgeProvider onNewEdit={async () => {}}>
+        <p>Tax statement letter</p>
+      </SourceCodeBridgeProvider>,
+    );
+
+    act(() => {
+      unmount();
+    });
+
+    expect(unsubscribeFromEntitiesMock).toHaveBeenCalled();
+    expect(unsubscribeFromSourceCodeMock).toHaveBeenCalled();
+  });
+
+  it('should not call setContent if the new content is the same as the current one', () => {
+    const setCodeAndNotifyMock = jest.fn();
+    const wrapper = render(<EnvelopeProviderTestingBed setCodeAndNotify={setCodeAndNotifyMock} />);
+
+    act(() => {
+      wrapper.getByText('Set Content').click();
+    });
+
+    act(() => {
+      wrapper.getByText('Set Content').click();
+    });
+
+    expect(setCodeAndNotifyMock).toHaveBeenCalledTimes(1);
+  });
+});
+
+const EnvelopeProviderTestingBed: FunctionComponent<{
+  setCodeAndNotify: (sourceCode: string, path?: string) => void;
+}> = ({ setCodeAndNotify }) => {
+  const envelopeRef = useRef<SourceCodeBridgeProviderRef>(null);
+
+  return (
+    <SourceCodeApiContext.Provider value={{ setCodeAndNotify }}>
+      <SourceCodeBridgeProvider ref={envelopeRef} onNewEdit={jest.fn()}>
+        <button
+          type="button"
+          onClick={() => {
+            envelopeRef.current?.setContent(
+              'test.camel.yaml',
+              `- from:
+            uri: "timer:foo"
+            steps: []
+            `,
+            );
+          }}
+        >
+          Set Content
+        </button>
+      </SourceCodeBridgeProvider>
+    </SourceCodeApiContext.Provider>
+  );
+};
diff --git a/packages/ui/src/multiplying-architecture/Bridge/SourceCodeBridgeProvider.tsx b/packages/ui/src/multiplying-architecture/Bridge/SourceCodeBridgeProvider.tsx
new file mode 100644
index 000000000..70e83e011
--- /dev/null
+++ b/packages/ui/src/multiplying-architecture/Bridge/SourceCodeBridgeProvider.tsx
@@ -0,0 +1,51 @@
+import { forwardRef, PropsWithChildren, useEffect, useImperativeHandle } from 'react';
+import { EventNotifier } from '../../utils';
+import { SourceCodeBridgeProviderRef, useEditorApi } from './editor-api';
+
+interface SourceCodeBridgeProviderProps extends PropsWithChildren {
+  /**
+   * Delegation for WorkspaceChannelApi.kogitoWorkspace_newEdit(edit) to signal to the Channel
+   * that a change has taken place.
+   * @param edit An object representing the unique change.
+   */
+  onNewEdit: (edit: string) => Promise<void>;
+}
+
+export const SourceCodeBridgeProvider = forwardRef<SourceCodeBridgeProviderRef, SourceCodeBridgeProviderProps>(
+  ({ onNewEdit, children }, ref) => {
+    const eventNotifier = EventNotifier.getInstance();
+    const { editorApi, sourceCodeRef } = useEditorApi();
+
+    /**
+     * Subscribe to the `entities:updated` event to update the File content.
+     */
+    useEffect(() => {
+      const unsubscribeFromEntities = eventNotifier.subscribe('entities:updated', (newContent: string) => {
+        onNewEdit(newContent);
+        sourceCodeRef.current = newContent;
+      });
+
+      const unsubscribeFromSourceCode = eventNotifier.subscribe('code:updated', ({ code: newContent }) => {
+        /** Ignore the first change, from an empty string to the file content  */
+        if (sourceCodeRef.current !== '') {
+          onNewEdit(newContent);
+        }
+        sourceCodeRef.current = newContent;
+      });
+
+      return () => {
+        unsubscribeFromEntities();
+        unsubscribeFromSourceCode();
+      };
+    }, [eventNotifier, onNewEdit, sourceCodeRef]);
+
+    /**
+     * The useImperativeHandler gives the control of the Editor component to who has it's reference,
+     * making it possible to communicate with the Editor.
+     * It returns all methods that are determined on the EditorApi.
+     */
+    useImperativeHandle(ref, () => editorApi);
+
+    return <>{children}</>;
+  },
+);
diff --git a/packages/ui/src/multiplying-architecture/Bridge/editor-api.test.tsx b/packages/ui/src/multiplying-architecture/Bridge/editor-api.test.tsx
new file mode 100644
index 000000000..d7dc26452
--- /dev/null
+++ b/packages/ui/src/multiplying-architecture/Bridge/editor-api.test.tsx
@@ -0,0 +1,99 @@
+import { FunctionComponent, PropsWithChildren } from 'react';
+import { SourceCodeApiContext } from '../../providers/source-code.provider';
+import { act, renderHook } from '@testing-library/react';
+import { useEditorApi } from './editor-api';
+import { EditorTheme } from '@kie-tools-core/editor/dist/api';
+
+describe('useEditorApi', () => {
+  const mockSetCodeAndNotify = jest.fn();
+
+  const wrapper: FunctionComponent<PropsWithChildren> = ({ children }) => (
+    <SourceCodeApiContext.Provider value={{ setCodeAndNotify: mockSetCodeAndNotify }}>
+      {children}
+    </SourceCodeApiContext.Provider>
+  );
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should initialize editorApi and sourceCodeRef', () => {
+    const { result } = renderHook(() => useEditorApi(), { wrapper });
+
+    expect(result.current.editorApi).toBeDefined();
+    expect(result.current.sourceCodeRef.current).toBe('');
+  });
+
+  it('should set content when setContent is called', async () => {
+    const { result } = renderHook(() => useEditorApi(), { wrapper });
+
+    const path = 'test-path';
+    const content = 'test-content';
+
+    await act(async () => {
+      await result.current.editorApi.setContent(path, content);
+    });
+
+    expect(mockSetCodeAndNotify).toHaveBeenCalledWith(content, path);
+    expect(result.current.sourceCodeRef.current).toBe(content);
+  });
+
+  it('should not update content if the new content is the same as the current one', async () => {
+    const { result } = renderHook(() => useEditorApi(), { wrapper });
+
+    const path = 'test-path';
+    const content = 'test-content';
+
+    await act(async () => {
+      await result.current.editorApi.setContent(path, content);
+    });
+
+    expect(mockSetCodeAndNotify).toHaveBeenCalledTimes(1);
+
+    await act(async () => {
+      await result.current.editorApi.setContent(path, content);
+    });
+
+    expect(mockSetCodeAndNotify).toHaveBeenCalledTimes(1);
+  });
+
+  it('should get content when getContent is called', async () => {
+    const { result } = renderHook(() => useEditorApi(), { wrapper });
+
+    const content = 'test-content';
+    result.current.sourceCodeRef.current = content;
+
+    const retrievedContent = await result.current.editorApi.getContent();
+
+    expect(retrievedContent).toBe(content);
+  });
+
+  it('should return undefined for getPreview', async () => {
+    const { result } = renderHook(() => useEditorApi(), { wrapper });
+
+    const preview = await result.current.editorApi.getPreview();
+
+    expect(preview).toBeUndefined();
+  });
+
+  it('should resolve undo and redo without errors', async () => {
+    const { result } = renderHook(() => useEditorApi(), { wrapper });
+
+    await expect(result.current.editorApi.undo()).resolves.toBeUndefined();
+    await expect(result.current.editorApi.redo()).resolves.toBeUndefined();
+  });
+
+  it('should validate and return an empty array', async () => {
+    const { result } = renderHook(() => useEditorApi(), { wrapper });
+
+    const validationResult = await result.current.editorApi.validate();
+
+    expect(validationResult).toEqual([]);
+  });
+
+  it('should resolve setTheme without errors', async () => {
+    const { result } = renderHook(() => useEditorApi(), { wrapper });
+
+    await expect(result.current.editorApi.setTheme(EditorTheme.LIGHT)).resolves.toBeUndefined();
+  });
+});
diff --git a/packages/ui/src/multiplying-architecture/Bridge/editor-api.tsx b/packages/ui/src/multiplying-architecture/Bridge/editor-api.tsx
new file mode 100644
index 000000000..83901cf2b
--- /dev/null
+++ b/packages/ui/src/multiplying-architecture/Bridge/editor-api.tsx
@@ -0,0 +1,80 @@
+import { EditorApi } from '@kie-tools-core/editor/dist/api';
+import { useCallback, useContext, useMemo, useRef } from 'react';
+import { SourceCodeApiContext } from '../../providers/source-code.provider';
+
+export interface SourceCodeBridgeProviderRef extends EditorApi {
+  setContent: (path: string, content: string) => Promise<void>;
+  getContent: () => Promise<string>;
+}
+
+export const useEditorApi = () => {
+  const sourceCodeRef = useRef<string>('');
+  const sourceCodeApiContext = useContext(SourceCodeApiContext);
+
+  /**
+   * Callback is exposed to the Channel that is called when a new file is opened.
+   * It sets the originalContent to the received value.
+   */
+  const setContent = useCallback(
+    (path: string, content: string) => {
+      /**
+       * If the new content is the same as the current one, we don't need to update the Editor,
+       * as it will regenerate the Camel Resource, hence disconnecting the configuration form (if open).
+       *
+       * This happens due to the multiplying architecture lifecycle, where the content is set
+       * after saving the file.
+       *
+       * The lifecycle is:
+       * 1. User edits the file either adding a new node or modifying an existing one using the form
+       * 2. User saves the file
+       * 3. The Envelope uses the `getContent` callback to retrieve the new content
+       * 4. The Envelope sets the new content using the `setContent` callback
+       *
+       * At this point, both the new content and the current content are the same, so the Editor
+       * don't need to be updated.
+       */
+      if (sourceCodeRef.current === content) return;
+
+      sourceCodeApiContext.setCodeAndNotify(content, path);
+      sourceCodeRef.current = content;
+    },
+    [sourceCodeApiContext],
+  );
+
+  /**
+   * The useImperativeHandler gives the control of the Editor component to who has it's reference,
+   * making it possible to communicate with the Editor.
+   * It returns all methods that are determined on the EditorApi.
+   */
+  const editorApi: SourceCodeBridgeProviderRef = useMemo(
+    () => ({
+      /* Callback is exposed to the Channel to set the content of the file into the current Editor. */
+      setContent: (path: string, content: string) => Promise.resolve(setContent(path, content)),
+
+      /**
+       * Callback is exposed to the Channel to retrieve the current value of the Editor. It returns the value of
+       * the editorContent, which is the state that has the kaoto yaml.
+       */
+      getContent: () => Promise.resolve(sourceCodeRef.current),
+      getPreview: () => Promise.resolve(undefined),
+      undo: (): Promise<void> => {
+        return Promise.resolve();
+      },
+      redo: (): Promise<void> => {
+        return Promise.resolve();
+      },
+      validate: () => Promise.resolve([]),
+      setTheme: () => Promise.resolve(),
+    }),
+    [setContent],
+  );
+
+  const output = useMemo(() => {
+    return {
+      editorApi,
+      sourceCodeRef,
+    };
+  }, [editorApi]);
+
+  return output;
+};
diff --git a/packages/ui/src/multiplying-architecture/KaotoBridge.tsx b/packages/ui/src/multiplying-architecture/KaotoBridge.tsx
deleted file mode 100644
index 0eb2d8cbb..000000000
--- a/packages/ui/src/multiplying-architecture/KaotoBridge.tsx
+++ /dev/null
@@ -1,242 +0,0 @@
-import { ChannelType, EditorApi, StateControlCommand } from '@kie-tools-core/editor/dist/api';
-import { Notification } from '@kie-tools-core/notifications/dist/api';
-import { VisualizationProvider } from '@patternfly/react-topology';
-import {
-  PropsWithChildren,
-  forwardRef,
-  useCallback,
-  useContext,
-  useEffect,
-  useImperativeHandle,
-  useMemo,
-  useRef,
-} from 'react';
-import { NodeInteractionAddonProvider } from '../components/registers/interactions/node-interaction-addon.provider';
-import { RegisterComponents } from '../components/registers/RegisterComponents';
-import { RegisterNodeInteractionAddons } from '../components/registers/RegisterNodeInteractionAddons';
-import { RenderingProvider } from '../components/RenderingAnchor/rendering.provider';
-import { ControllerService } from '../components/Visualization/Canvas/controller.service';
-import {
-  CatalogTilesProvider,
-  MetadataProvider,
-  ReloadProvider,
-  SourceCodeApiContext,
-  VisibleFlowsProvider,
-} from '../providers';
-import { EventNotifier } from '../utils';
-
-interface KaotoBridgeProps {
-  /**
-   * Delegation for KogitoEditorChannelApi.kogitoEditor_ready() to signal to the Channel
-   * that the editor is ready.
-   */
-  onReady: () => void;
-
-  /**
-   * Delegation for KogitoEditorChannelApi.kogitoEditor_stateControlCommandUpdate(command) to signal to the Channel
-   * that the editor is performing an undo/redo operation.
-   */
-  onStateControlCommandUpdate: (command: StateControlCommand) => void;
-
-  /**
-   * Delegation for WorkspaceChannelApi.kogitoWorkspace_newEdit(edit) to signal to the Channel
-   * that a change has taken place.
-   * @param edit An object representing the unique change.
-   */
-  onNewEdit: (edit: string) => Promise<void>;
-
-  /**
-   * Delegation for NotificationsChannelApi.kogigotNotifications_setNotifications(path, notifications) to report all validation
-   * notifications to the Channel that will replace existing notification for the path.
-   * @param path The path that references the Notification
-   * @param notifications List of Notifications
-   */
-  setNotifications: (path: string, notifications: Notification[]) => void;
-
-  /**
-   * Get metadata querying a Kaoto metadata file using the channel API.
-   * @param key The key to retrieve the metadata from the Kaoto metadata file
-   */
-  getMetadata<T>(key: string): Promise<T | undefined>;
-
-  /**
-   * Save metadata to a Kaoto metadata file using the channel API.
-   * @param key The key to set the metadata
-   * @param metadata The metadata to be saved
-   */
-  setMetadata<T>(key: string, metadata: T): Promise<void>;
-
-  /**
-   * Retrieve resource content using the channel API.
-   * @param path The path of the resource
-   */
-  getResourceContent(path: string): Promise<string | undefined>;
-
-  /**
-   * Save resource content using the channel API.
-   * @param path The path of the resource
-   * @param content The content to be saved
-   */
-  saveResourceContent(path: string, content: string): Promise<void>;
-
-  /**
-   * Delete resource using the channel API.
-   * @param path The path of the resource
-   */
-  deleteResource(path: string): Promise<boolean>;
-
-  /**
-   * Show a Quick Pick widget and ask the user to select one or more files available in the workspace.
-   * @param include The filter expression for the files to include
-   * @param exclude The filter expression for the files to exclude
-   * @param options The options to pass over to VSCode QuickPick
-   */
-  askUserForFileSelection(
-    include: string,
-    exclude?: string,
-    options?: Record<string, unknown>,
-  ): Promise<string[] | string | undefined>;
-
-  /**
-   * ChannelType where the component is running.
-   */
-  channelType: ChannelType;
-}
-
-export const KaotoBridge = forwardRef<EditorApi, PropsWithChildren<KaotoBridgeProps>>(
-  (
-    {
-      onNewEdit,
-      onReady,
-      children,
-      getMetadata,
-      setMetadata,
-      getResourceContent,
-      saveResourceContent,
-      deleteResource,
-      askUserForFileSelection,
-    },
-    forwardedRef,
-  ) => {
-    const controller = useMemo(() => ControllerService.createController(), []);
-    const eventNotifier = EventNotifier.getInstance();
-    const sourceCodeApiContext = useContext(SourceCodeApiContext);
-    const sourceCodeRef = useRef<string>('');
-    const metadataApi = useMemo(
-      () => ({
-        getMetadata,
-        setMetadata,
-        getResourceContent,
-        saveResourceContent,
-        deleteResource,
-        askUserForFileSelection,
-        shouldSaveSchema: false,
-      }),
-      [getMetadata, setMetadata, getResourceContent, saveResourceContent, deleteResource, askUserForFileSelection],
-    );
-
-    /**
-     * Callback is exposed to the Channel that is called when a new file is opened.
-     * It sets the originalContent to the received value.
-     */
-    const setContent = useCallback(
-      (path: string, content: string) => {
-        /**
-         * If the new content is the same as the current one, we don't need to update the Editor,
-         * as it will regenerate the Camel Resource, hence disconnecting the configuration form (if open).
-         *
-         * This happens due to the multiplying architecture lifecycle, where the content is set
-         * after saving the file.
-         *
-         * The lifecycle is:
-         * 1. User edits the file either adding a new node or modifying an existing one using the form
-         * 2. User saves the file
-         * 3. The Envelope uses the `getContent` callback to retrieve the new content
-         * 4. The Envelope sets the new content using the `setContent` callback
-         *
-         * At this point, both the new content and the current content are the same, so the Editor
-         * don't need to be updated.
-         */
-        if (sourceCodeRef.current === content) return;
-
-        sourceCodeApiContext.setCodeAndNotify(content, path);
-        sourceCodeRef.current = content;
-      },
-      [sourceCodeApiContext],
-    );
-
-    /**
-     * Subscribe to the `entities:updated` event to update the File content.
-     */
-    useEffect(() => {
-      const unsubscribeFromEntities = eventNotifier.subscribe('entities:updated', (newContent: string) => {
-        onNewEdit(newContent);
-        sourceCodeRef.current = newContent;
-      });
-
-      const unsubscribeFromSourceCode = eventNotifier.subscribe('code:updated', ({ code: newContent }) => {
-        /** Ignore the first change, from an empty string to the file content  */
-        if (sourceCodeRef.current !== '') {
-          onNewEdit(newContent);
-        }
-        sourceCodeRef.current = newContent;
-      });
-
-      return () => {
-        unsubscribeFromEntities();
-        unsubscribeFromSourceCode();
-      };
-    }, [eventNotifier, onNewEdit]);
-
-    /**
-     * The useImperativeHandler gives the control of the Editor component to who has it's reference,
-     * making it possible to communicate with the Editor.
-     * It returns all methods that are determined on the EditorApi.
-     */
-    useImperativeHandle(forwardedRef, () => {
-      return {
-        /* Callback is exposed to the Channel to set the content of the file into the current Editor. */
-        setContent: (path: string, content: string) => Promise.resolve(setContent(path, content)),
-        /**
-         * Callback is exposed to the Channel to retrieve the current value of the Editor. It returns the value of
-         * the editorContent, which is the state that has the kaoto yaml.
-         */
-        getContent: () => Promise.resolve(sourceCodeRef.current),
-        getPreview: () => Promise.resolve(undefined),
-        undo: (): Promise<void> => {
-          return Promise.resolve();
-        },
-        redo: (): Promise<void> => {
-          return Promise.resolve();
-        },
-        validate: () => Promise.resolve([]),
-        setTheme: () => Promise.resolve(),
-      };
-    });
-
-    /** Set editor as Ready */
-    useEffect(() => {
-      onReady();
-    }, [onReady]);
-
-    return (
-      <ReloadProvider>
-        <CatalogTilesProvider>
-          <VisualizationProvider controller={controller}>
-            <VisibleFlowsProvider>
-              <RenderingProvider>
-                <MetadataProvider api={metadataApi}>
-                  <RegisterComponents>
-                    <NodeInteractionAddonProvider>
-                      <RegisterNodeInteractionAddons>{children}</RegisterNodeInteractionAddons>
-                    </NodeInteractionAddonProvider>
-                  </RegisterComponents>
-                </MetadataProvider>
-              </RenderingProvider>
-            </VisibleFlowsProvider>
-          </VisualizationProvider>
-        </CatalogTilesProvider>
-      </ReloadProvider>
-    );
-  },
-);
diff --git a/packages/ui/src/multiplying-architecture/KaotoEditorApp.test.tsx b/packages/ui/src/multiplying-architecture/KaotoEditorApp.test.tsx
index fed09ea3c..4f839c996 100644
--- a/packages/ui/src/multiplying-architecture/KaotoEditorApp.test.tsx
+++ b/packages/ui/src/multiplying-architecture/KaotoEditorApp.test.tsx
@@ -122,18 +122,6 @@ describe('KaotoEditorApp', () => {
 
       expect(editorRef.current!.setContent).toHaveBeenCalledWith('path', 'content');
     });
-
-    it('should store the calling arguments until the editor is ready', async () => {
-      kaotoEditorApp.setEditorRef({ current: null });
-
-      await kaotoEditorApp.setContent('path', 'content');
-
-      kaotoEditorApp.setEditorRef(editorRef);
-      await kaotoEditorApp.sendReady();
-
-      expect(editorRef.current!.setContent).toHaveBeenCalledWith('path', 'content');
-      expect(editorRef.current!.setContent).toHaveBeenCalledTimes(1);
-    });
   });
 
   it('getContent', async () => {
diff --git a/packages/ui/src/multiplying-architecture/KaotoEditorApp.tsx b/packages/ui/src/multiplying-architecture/KaotoEditorApp.tsx
index 499736086..52043e1c5 100644
--- a/packages/ui/src/multiplying-architecture/KaotoEditorApp.tsx
+++ b/packages/ui/src/multiplying-architecture/KaotoEditorApp.tsx
@@ -1,6 +1,5 @@
 import {
   Editor,
-  EditorApi,
   EditorInitArgs,
   EditorTheme,
   KogitoEditorEnvelopeContextType,
@@ -14,29 +13,30 @@ import { RouterProvider } from 'react-router-dom';
 import { AbstractSettingsAdapter } from '../models/settings';
 import { CatalogLoaderProvider } from '../providers/catalog.provider';
 import { EntitiesProvider } from '../providers/entities.provider';
+import { ReloadProvider } from '../providers/reload.provider';
 import { RuntimeProvider } from '../providers/runtime.provider';
 import { SettingsProvider } from '../providers/settings.provider';
 import { SourceCodeProvider } from '../providers/source-code.provider';
 import { setColorScheme } from '../utils/color-scheme';
+import { SourceCodeBridgeProviderRef } from './Bridge/editor-api';
+import { KaotoBridge } from './Bridge/KaotoBridge';
+import { SourceCodeBridgeProvider } from './Bridge/SourceCodeBridgeProvider';
 import { EditService } from './EditService';
-import { KaotoBridge } from './KaotoBridge';
 import { KaotoEditorChannelApi } from './KaotoEditorChannelApi';
 import { kaotoEditorRouter } from './KaotoEditorRouter';
 
 export class KaotoEditorApp implements Editor {
-  protected editorRef: RefObject<EditorApi>;
+  protected editorRef: RefObject<SourceCodeBridgeProviderRef>;
   af_isReact = true;
   af_componentId = 'kaoto-editor';
   af_componentTitle = 'Kaoto Editor';
-  private pendingPath = '';
-  private pendingContent = '';
 
   constructor(
     protected readonly envelopeContext: KogitoEditorEnvelopeContextType<KaotoEditorChannelApi>,
     protected readonly initArgs: EditorInitArgs,
     protected readonly settingsAdapter: AbstractSettingsAdapter,
   ) {
-    this.editorRef = createRef<EditorApi>();
+    this.editorRef = createRef<SourceCodeBridgeProviderRef>();
     this.sendReady = this.sendReady.bind(this);
     this.sendNewEdit = this.sendNewEdit.bind(this);
     this.sendNotifications = this.sendNotifications.bind(this);
@@ -58,14 +58,7 @@ export class KaotoEditorApp implements Editor {
 
     EditService.getInstance().clearEdits();
 
-    if (this.editorRef.current !== null) {
-      /** If the editor is available, we can send the content from the file */
-      return this.editorRef.current.setContent(path, content);
-    }
-
-    /** Otherwise we store the parameters for when the editor is available */
-    this.pendingPath = path;
-    this.pendingContent = content;
+    await this.editorRef.current?.setContent(path, content);
   }
 
   async getContent(): Promise<string> {
@@ -96,8 +89,6 @@ export class KaotoEditorApp implements Editor {
 
   async sendReady(): Promise<void> {
     this.envelopeContext.channelApi.notifications.kogitoEditor_ready.send();
-
-    await this.setContent(this.pendingPath, this.pendingContent);
   }
 
   async sendNewEdit(content: string): Promise<void> {
@@ -149,32 +140,34 @@ export class KaotoEditorApp implements Editor {
 
   af_componentRoot() {
     return (
-      <RuntimeProvider catalogUrl={this.settingsAdapter.getSettings().catalogUrl}>
-        <CatalogLoaderProvider>
+      <ReloadProvider>
+        <SettingsProvider adapter={this.settingsAdapter}>
           <SourceCodeProvider>
-            <EntitiesProvider fileExtension={this.initArgs.fileExtension}>
-              <SettingsProvider adapter={this.settingsAdapter}>
-                <KaotoBridge
-                  ref={this.editorRef}
-                  channelType={this.initArgs.channel}
-                  onReady={this.sendReady}
-                  onNewEdit={this.sendNewEdit}
-                  setNotifications={this.sendNotifications}
-                  onStateControlCommandUpdate={this.sendStateControlCommand}
-                  getMetadata={this.getMetadata}
-                  setMetadata={this.setMetadata}
-                  getResourceContent={this.getResourceContent}
-                  saveResourceContent={this.saveResourceContent}
-                  deleteResource={this.deleteResource}
-                  askUserForFileSelection={this.askUserForFileSelection}
-                >
-                  <RouterProvider router={kaotoEditorRouter} />
-                </KaotoBridge>
-              </SettingsProvider>
-            </EntitiesProvider>
+            <SourceCodeBridgeProvider ref={this.editorRef} onNewEdit={this.sendNewEdit}>
+              <RuntimeProvider catalogUrl={this.settingsAdapter.getSettings().catalogUrl}>
+                <CatalogLoaderProvider>
+                  <EntitiesProvider fileExtension={this.initArgs.fileExtension}>
+                    <KaotoBridge
+                      channelType={this.initArgs.channel}
+                      onReady={this.sendReady}
+                      setNotifications={this.sendNotifications}
+                      onStateControlCommandUpdate={this.sendStateControlCommand}
+                      getMetadata={this.getMetadata}
+                      setMetadata={this.setMetadata}
+                      getResourceContent={this.getResourceContent}
+                      saveResourceContent={this.saveResourceContent}
+                      deleteResource={this.deleteResource}
+                      askUserForFileSelection={this.askUserForFileSelection}
+                    >
+                      <RouterProvider router={kaotoEditorRouter} />
+                    </KaotoBridge>
+                  </EntitiesProvider>
+                </CatalogLoaderProvider>
+              </RuntimeProvider>
+            </SourceCodeBridgeProvider>
           </SourceCodeProvider>
-        </CatalogLoaderProvider>
-      </RuntimeProvider>
+        </SettingsProvider>
+      </ReloadProvider>
     );
   }
 }
diff --git a/packages/ui/src/providers/entities.provider.test.tsx b/packages/ui/src/providers/entities.provider.test.tsx
index 40392aa02..3932652cb 100644
--- a/packages/ui/src/providers/entities.provider.test.tsx
+++ b/packages/ui/src/providers/entities.provider.test.tsx
@@ -1,5 +1,6 @@
 import { act, renderHook } from '@testing-library/react';
 import { PropsWithChildren, useContext } from 'react';
+import { parse } from 'yaml';
 import { CamelResource } from '../models/camel';
 import { CamelRouteVisualEntity } from '../models/visualization/flows';
 import { SerializerType } from '../serializers';
@@ -7,6 +8,7 @@ import { camelRouteJson, camelRouteYaml } from '../stubs/camel-route';
 import { camelRouteYaml_1_1_original, camelRouteYaml_1_1_updated } from '../stubs/camel-route-yaml-1.1';
 import { EventNotifier } from '../utils';
 import { EntitiesContext, EntitiesProvider } from './entities.provider';
+import { SourceCodeContext } from './source-code.provider';
 
 describe('EntitiesProvider', () => {
   let eventNotifier: EventNotifier;
@@ -31,6 +33,52 @@ describe('EntitiesProvider', () => {
     },
   );
 
+  describe('Initialization', () => {
+    it('should use the sourceCode context to initialize the Camel Resource', () => {
+      const { result } = renderHook(() => useContext(EntitiesContext), {
+        wrapper: ({ children }: PropsWithChildren) => (
+          <SourceCodeContext.Provider value={camelRouteYaml}>
+            <EntitiesProvider>{children}</EntitiesProvider>
+          </SourceCodeContext.Provider>
+        ),
+      });
+
+      expect(result.current?.camelResource.toJSON()).toEqual(parse(camelRouteYaml));
+    });
+
+    it('should create an empty Camel Resource if there is no Source Code available', () => {
+      const { result } = renderHook(() => useContext(EntitiesContext), {
+        wrapper: EntitiesProvider,
+      });
+
+      expect(result.current?.camelResource.toJSON()).toEqual([]);
+    });
+
+    it('should ignore non-camel entities', () => {
+      const { result } = renderHook(() => useContext(EntitiesContext), {
+        wrapper: ({ children }: PropsWithChildren) => (
+          <SourceCodeContext.Provider value="A non camel source code">
+            <EntitiesProvider>{children}</EntitiesProvider>
+          </SourceCodeContext.Provider>
+        ),
+      });
+
+      expect(result.current?.camelResource.toJSON()).toEqual(['A non camel source code']);
+    });
+
+    it('should fallback to an empty Camel Resource when there is a wrong Source Code', () => {
+      const { result } = renderHook(() => useContext(EntitiesContext), {
+        wrapper: ({ children }: PropsWithChildren) => (
+          <SourceCodeContext.Provider value={'- from: {'}>
+            <EntitiesProvider>{children}</EntitiesProvider>
+          </SourceCodeContext.Provider>
+        ),
+      });
+
+      expect(result.current?.camelResource.toJSON()).toEqual([]);
+    });
+  });
+
   it('it should subscribe to the `code:updated` notification', () => {
     const notifierSpy = jest.spyOn(eventNotifier, 'subscribe');
 
diff --git a/packages/ui/src/providers/entities.provider.tsx b/packages/ui/src/providers/entities.provider.tsx
index f127e2226..272a0537c 100644
--- a/packages/ui/src/providers/entities.provider.tsx
+++ b/packages/ui/src/providers/entities.provider.tsx
@@ -3,6 +3,7 @@ import {
   PropsWithChildren,
   createContext,
   useCallback,
+  useContext,
   useLayoutEffect,
   useMemo,
   useState,
@@ -12,6 +13,7 @@ import { CamelResource, SourceSchemaType } from '../models/camel';
 import { CamelResourceFactory } from '../models/camel/camel-resource-factory';
 import { BaseCamelEntity } from '../models/camel/entities';
 import { EventNotifier } from '../utils';
+import { SourceCodeContext } from './source-code.provider';
 
 export interface EntitiesContextResult {
   entities: BaseCamelEntity[];
@@ -40,16 +42,24 @@ export interface EntitiesContextResult {
 
 export const EntitiesContext = createContext<EntitiesContextResult | null>(null);
 
-export const EntitiesProvider: FunctionComponent<PropsWithChildren<{ fileExtension?: string }>> = ({
-  fileExtension,
-  children,
-}) => {
+interface EntitiesProviderProps extends PropsWithChildren {
+  fileExtension?: string;
+}
+
+export const EntitiesProvider: FunctionComponent<EntitiesProviderProps> = ({ fileExtension, children }) => {
   const eventNotifier = EventNotifier.getInstance();
-  const [camelResource, setCamelResource] = useState<CamelResource>(
-    CamelResourceFactory.createCamelResource('', { path: fileExtension }),
-  );
-  const [entities, setEntities] = useState<BaseCamelEntity[]>([]);
-  const [visualEntities, setVisualEntities] = useState<BaseVisualCamelEntity[]>([]);
+  const initialSourceCode = useContext(SourceCodeContext);
+
+  let initialCamelResource: CamelResource;
+  try {
+    initialCamelResource = CamelResourceFactory.createCamelResource(initialSourceCode, { path: fileExtension });
+  } catch (error) {
+    initialCamelResource = CamelResourceFactory.createCamelResource('', { path: fileExtension });
+  }
+
+  const [camelResource, setCamelResource] = useState<CamelResource>(initialCamelResource);
+  const [entities, setEntities] = useState<BaseCamelEntity[]>(camelResource.getEntities());
+  const [visualEntities, setVisualEntities] = useState<BaseVisualCamelEntity[]>(camelResource.getVisualEntities());
 
   /**
    * Subscribe to the `code:updated` event to recreate the CamelResource


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:59:26Z
user: login: lordrip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T17:10:16Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T17:13:34Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**packages/ui/src/multiplying-architecture/KaotoEditorApp.tsx:60**
* Using optional chaining here means that if editorRef.current is null, setContent will not be invoked. Please verify that silently skipping the content update is the desired behavior.
```
await this.editorRef.current?.setContent(path, content);
```
**packages/ui/src/multiplying-architecture/KaotoBridge.tsx:83**
* The removal of ref forwarding in KaotoBridge changes the previously exposed imperative API. Ensure that this refactor aligns with the intended component contract and that clients are migrated to use the new EnvelopeProvider accordingly.
```
export const KaotoBridge: FunctionComponent<PropsWithChildren<KaotoBridgeProps>> = ({
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T19:00:12Z
user: login: igarashitm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T13:28:36Z
user: login: apupier
body: not detected any problem while testing in VS Code
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T13:53:01Z
user: login: lordrip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T14:02:28Z

Review comments:
diff_hunk: @@ -149,32 +139,35 @@ export class KaotoEditorApp implements Editor {
 
   af_componentRoot() {
     return (
-      <RuntimeProvider catalogUrl={this.settingsAdapter.getSettings().catalogUrl}>
-        <CatalogLoaderProvider>
+      <ReloadProvider>
+        <SettingsProvider adapter={this.settingsAdapter}>
           <SourceCodeProvider>
-            <EntitiesProvider fileExtension={this.initArgs.fileExtension}>
-              <SettingsProvider adapter={this.settingsAdapter}>
-                <KaotoBridge
-                  ref={this.editorRef}
-                  channelType={this.initArgs.channel}
-                  onReady={this.sendReady}
-                  onNewEdit={this.sendNewEdit}
-                  setNotifications={this.sendNotifications}
-                  onStateControlCommandUpdate={this.sendStateControlCommand}
-                  getMetadata={this.getMetadata}
-                  setMetadata={this.setMetadata}
-                  getResourceContent={this.getResourceContent}
-                  saveResourceContent={this.saveResourceContent}
-                  deleteResource={this.deleteResource}
-                  askUserForFileSelection={this.askUserForFileSelection}
-                >
-                  <RouterProvider router={kaotoEditorRouter} />
-                </KaotoBridge>
-              </SettingsProvider>
-            </EntitiesProvider>
+            <EnvelopeProvider ref={this.editorRef} onNewEdit={this.sendNewEdit}>
+              <RuntimeProvider catalogUrl={this.settingsAdapter.getSettings().catalogUrl}>
+                <CatalogLoaderProvider>
+                  <EntitiesProvider fileExtension={this.initArgs.fileExtension}>
+                    <KaotoBridge
+                      channelType={this.initArgs.channel}
+                      onReady={this.sendReady}
+                      onNewEdit={async () => {}}
path: packages/ui/src/multiplying-architecture/KaotoEditorApp.tsx
user: login: Copilot
body: The 'onNewEdit' prop is currently assigned an empty async function, which may prevent new edit events from being handled appropriately. Consider passing the existing sendNewEdit handler or a proper implementation to ensure that new edits are processed as intended.
```suggestion
                      onNewEdit={this.sendNewEdit}
```
created_at: 2025-04-16T16:59:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -149,32 +139,35 @@ export class KaotoEditorApp implements Editor {
 
   af_componentRoot() {
     return (
-      <RuntimeProvider catalogUrl={this.settingsAdapter.getSettings().catalogUrl}>
-        <CatalogLoaderProvider>
+      <ReloadProvider>
+        <SettingsProvider adapter={this.settingsAdapter}>
           <SourceCodeProvider>
-            <EntitiesProvider fileExtension={this.initArgs.fileExtension}>
-              <SettingsProvider adapter={this.settingsAdapter}>
-                <KaotoBridge
-                  ref={this.editorRef}
-                  channelType={this.initArgs.channel}
-                  onReady={this.sendReady}
-                  onNewEdit={this.sendNewEdit}
-                  setNotifications={this.sendNotifications}
-                  onStateControlCommandUpdate={this.sendStateControlCommand}
-                  getMetadata={this.getMetadata}
-                  setMetadata={this.setMetadata}
-                  getResourceContent={this.getResourceContent}
-                  saveResourceContent={this.saveResourceContent}
-                  deleteResource={this.deleteResource}
-                  askUserForFileSelection={this.askUserForFileSelection}
-                >
-                  <RouterProvider router={kaotoEditorRouter} />
-                </KaotoBridge>
-              </SettingsProvider>
-            </EntitiesProvider>
+            <EnvelopeProvider ref={this.editorRef} onNewEdit={this.sendNewEdit}>
+              <RuntimeProvider catalogUrl={this.settingsAdapter.getSettings().catalogUrl}>
+                <CatalogLoaderProvider>
+                  <EntitiesProvider fileExtension={this.initArgs.fileExtension}>
+                    <KaotoBridge
+                      channelType={this.initArgs.channel}
+                      onReady={this.sendReady}
+                      onNewEdit={async () => {}}
path: packages/ui/src/multiplying-architecture/KaotoEditorApp.tsx
user: login: lordrip
body: Right, but the solution should be remove the property altogether, rather than assign it to anything else, because the `onNewEdit` is now handled by the `EnvelopeProvider`.
created_at: 2025-04-16T17:10:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+import { EditorApi } from '@kie-tools-core/editor/dist/api';
+import { forwardRef, PropsWithChildren, useCallback, useContext, useEffect, useImperativeHandle, useRef } from 'react';
+import { SourceCodeApiContext } from '../providers/source-code.provider';
+import { EventNotifier } from '../utils';
+
+export interface EnvelopeProviderRef extends EditorApi {
+  setContent: (path: string, content: string) => Promise<void>;
+  getContent: () => Promise<string>;
+}
+
+export interface EnvelopeProviderProps extends PropsWithChildren {
+  /**
+   * Delegation for WorkspaceChannelApi.kogitoWorkspace_newEdit(edit) to signal to the Channel
+   * that a change has taken place.
+   * @param edit An object representing the unique change.
+   */
+  onNewEdit: (edit: string) => Promise<void>;
+}
+
+export const EnvelopeProvider = forwardRef<EnvelopeProviderRef, EnvelopeProviderProps>(
+  ({ onNewEdit, children }, ref) => {
+    const sourceCodeRef = useRef<string>('');
+    const sourceCodeApiContext = useContext(SourceCodeApiContext);
+    const eventNotifier = EventNotifier.getInstance();
+
+    /**
+     * Callback is exposed to the Channel that is called when a new file is opened.
+     * It sets the originalContent to the received value.
+     */
+    const setContent = useCallback(
+      (path: string, content: string) => {
+        /**
+         * If the new content is the same as the current one, we don't need to update the Editor,
+         * as it will regenerate the Camel Resource, hence disconnecting the configuration form (if open).
+         *
+         * This happens due to the multiplying architecture lifecycle, where the content is set
+         * after saving the file.
+         *
+         * The lifecycle is:
+         * 1. User edits the file either adding a new node or modifying an existing one using the form
+         * 2. User saves the file
+         * 3. The Envelope uses the `getContent` callback to retrieve the new content
+         * 4. The Envelope sets the new content using the `setContent` callback
+         *
+         * At this point, both the new content and the current content are the same, so the Editor
+         * don't need to be updated.
+         */
+        if (sourceCodeRef.current === content) return;
+
+        sourceCodeApiContext.setCodeAndNotify(content, path);
+        sourceCodeRef.current = content;
+      },
+      [sourceCodeApiContext],
+    );
+
+    /**
+     * Subscribe to the `entities:updated` event to update the File content.
+     */
+    useEffect(() => {
+      const unsubscribeFromEntities = eventNotifier.subscribe('entities:updated', (newContent: string) => {
+        onNewEdit(newContent);
+        sourceCodeRef.current = newContent;
+      });
+
+      const unsubscribeFromSourceCode = eventNotifier.subscribe('code:updated', ({ code: newContent }) => {
+        /** Ignore the first change, from an empty string to the file content  */
+        if (sourceCodeRef.current !== '') {
+          onNewEdit(newContent);
+        }
+        sourceCodeRef.current = newContent;
+      });
+
+      return () => {
+        unsubscribeFromEntities();
+        unsubscribeFromSourceCode();
+      };
+    }, [eventNotifier, onNewEdit]);
+
+    /**
+     * The useImperativeHandler gives the control of the Editor component to who has it's reference,
+     * making it possible to communicate with the Editor.
+     * It returns all methods that are determined on the EditorApi.
+     */
+    useImperativeHandle(ref, () => ({
+      /* Callback is exposed to the Channel to set the content of the file into the current Editor. */
+      setContent: (path: string, content: string) => Promise.resolve(setContent(path, content)),
+
+      /**
+       * Callback is exposed to the Channel to retrieve the current value of the Editor. It returns the value of
+       * the editorContent, which is the state that has the kaoto yaml.
+       */
+      getContent: () => Promise.resolve(sourceCodeRef.current),
+      getPreview: () => Promise.resolve(undefined),
+      undo: (): Promise<void> => {
+        return Promise.resolve();
+      },
+      redo: (): Promise<void> => {
+        return Promise.resolve();
+      },
+      validate: () => Promise.resolve([]),
+      setTheme: () => Promise.resolve(),
+    }));
+
+    return <>{children};</>;
path: packages/ui/src/multiplying-architecture/EnvelopeProvider.tsx
user: login: Copilot
body: The JSX fragment contains an extraneous semicolon after {children} which could cause an unintended semicolon to be rendered in the UI. Please remove the semicolon so that the return statement reads as: return <>{children}</>;
```suggestion
    return <>{children}</>;
```
created_at: 2025-04-16T17:13:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,16 +42,25 @@ export interface EntitiesContextResult {
 
 export const EntitiesContext = createContext<EntitiesContextResult | null>(null);
 
-export const EntitiesProvider: FunctionComponent<PropsWithChildren<{ fileExtension?: string }>> = ({
-  fileExtension,
-  children,
-}) => {
+interface EntitiesProviderProps extends PropsWithChildren {
+  fileExtension?: string;
+}
+
+export const EntitiesProvider: FunctionComponent<EntitiesProviderProps> = ({ fileExtension, children }) => {
   const eventNotifier = EventNotifier.getInstance();
-  const [camelResource, setCamelResource] = useState<CamelResource>(
-    CamelResourceFactory.createCamelResource('', { path: fileExtension }),
-  );
-  const [entities, setEntities] = useState<BaseCamelEntity[]>([]);
-  const [visualEntities, setVisualEntities] = useState<BaseVisualCamelEntity[]>([]);
+  const initialSourceCode = useContext(SourceCodeContext);
+
+  let initialCamelResource: CamelResource;
+  try {
+    initialCamelResource = CamelResourceFactory.createCamelResource(initialSourceCode, { path: fileExtension });
+  } catch (error) {
+    initialCamelResource = CamelResourceFactory.createCamelResource('', { path: fileExtension });
+    console.error('Error creating initial CamelResource:', error);
path: packages/ui/src/providers/entities.provider.tsx
user: login: Copilot
body: [nitpick] Consider replacing console.error with a robust logging solution in production to ensure errors are tracked and managed appropriately.
created_at: 2025-04-16T19:00:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,16 +42,24 @@ export interface EntitiesContextResult {
 
 export const EntitiesContext = createContext<EntitiesContextResult | null>(null);
 
-export const EntitiesProvider: FunctionComponent<PropsWithChildren<{ fileExtension?: string }>> = ({
-  fileExtension,
-  children,
-}) => {
+interface EntitiesProviderProps extends PropsWithChildren {
+  fileExtension?: string;
+}
+
+export const EntitiesProvider: FunctionComponent<EntitiesProviderProps> = ({ fileExtension, children }) => {
   const eventNotifier = EventNotifier.getInstance();
-  const [camelResource, setCamelResource] = useState<CamelResource>(
-    CamelResourceFactory.createCamelResource('', { path: fileExtension }),
-  );
-  const [entities, setEntities] = useState<BaseCamelEntity[]>([]);
-  const [visualEntities, setVisualEntities] = useState<BaseVisualCamelEntity[]>([]);
+  const initialSourceCode = useContext(SourceCodeContext);
+
+  let initialCamelResource: CamelResource;
+  try {
+    initialCamelResource = CamelResourceFactory.createCamelResource(initialSourceCode, { path: fileExtension });
+  } catch (error) {
+    initialCamelResource = CamelResourceFactory.createCamelResource('', { path: fileExtension });
+  }
path: packages/ui/src/providers/entities.provider.tsx
user: login: apupier
body: this part sounds a bit weird.
With which kind of `initialSourceCode` it could happen? What about logging the problem or notifying the user because it sounds like some data can be lost
created_at: 2025-04-17T13:51:47Z
author_association: MEMBER
diff_hunk: @@ -40,16 +42,24 @@ export interface EntitiesContextResult {
 
 export const EntitiesContext = createContext<EntitiesContextResult | null>(null);
 
-export const EntitiesProvider: FunctionComponent<PropsWithChildren<{ fileExtension?: string }>> = ({
-  fileExtension,
-  children,
-}) => {
+interface EntitiesProviderProps extends PropsWithChildren {
+  fileExtension?: string;
+}
+
+export const EntitiesProvider: FunctionComponent<EntitiesProviderProps> = ({ fileExtension, children }) => {
   const eventNotifier = EventNotifier.getInstance();
-  const [camelResource, setCamelResource] = useState<CamelResource>(
-    CamelResourceFactory.createCamelResource('', { path: fileExtension }),
-  );
-  const [entities, setEntities] = useState<BaseCamelEntity[]>([]);
-  const [visualEntities, setVisualEntities] = useState<BaseVisualCamelEntity[]>([]);
+  const initialSourceCode = useContext(SourceCodeContext);
+
+  let initialCamelResource: CamelResource;
+  try {
+    initialCamelResource = CamelResourceFactory.createCamelResource(initialSourceCode, { path: fileExtension });
+  } catch (error) {
+    initialCamelResource = CamelResourceFactory.createCamelResource('', { path: fileExtension });
+  }
path: packages/ui/src/providers/entities.provider.tsx
user: login: lordrip
body: This `try/catch` guards against a broken `YAML` or `XML`, broken in the sense that it cannot be parsed, for instance:
```yaml
    from:
      uri: timer
      parameters: {
```

That YAML cannot be parsed because it is missing the closing `}`, so in this case, we default to an empty Camel Resource.

Now, in terms of letting the user know this situation, we have this open issue: https://github.com/KaotoIO/kaoto/issues/683 to create a special kind of Camel Resource, like UnknownCamelResource or something like that so we can show in the Canvas what's happening.

created_at: 2025-04-17T14:02:28Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-16T19:06:15Z
body: ## [Codecov](https://app.codecov.io/gh/KaotoIO/kaoto/pull/2194?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=KaotoIO) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 81.97%. Comparing base [(`a539d4b`)](https://app.codecov.io/gh/KaotoIO/kaoto/commit/a539d4b81fc15fd7f51f13f38cdc5a05268226d9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=KaotoIO) to head [(`cebdd99`)](https://app.codecov.io/gh/KaotoIO/kaoto/commit/cebdd9952e9c5b1aa581ae62afab8abeb735d153?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=KaotoIO).
> Report is 2 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main    #2194      +/-   ##
============================================
+ Coverage     81.64%   81.97%   +0.32%     
  Complexity      456      456              
============================================
  Files           523      525       +2     
  Lines         16786    16846      +60     
  Branches       3377     3389      +12     
============================================
+ Hits          13705    13809     +104     
+ Misses         2996     2952      -44     
  Partials         85       85              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/KaotoIO/kaoto/pull/2194?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=KaotoIO).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=KaotoIO).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-17T13:22:47Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=KaotoIO_kaoto&pullRequest=2194) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0 New issues](https://sonarcloud.io/project/issues?id=KaotoIO_kaoto&pullRequest=2194&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted.svg '') [4 Accepted issues](https://sonarcloud.io/project/issues?id=KaotoIO_kaoto&pullRequest=2194&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=KaotoIO_kaoto&pullRequest=2194&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data.svg '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [6.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=KaotoIO_kaoto&pullRequest=2194&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=KaotoIO_kaoto&pullRequest=2194)


author_association: NONE