diff --git a/packages/bruno-app/src/components/AiChatSidebar/Popout/index.js b/packages/bruno-app/src/components/AiChatSidebar/Popout/index.js new file mode 100644 index 000000000..6b05bcb46 --- /dev/null +++ b/packages/bruno-app/src/components/AiChatSidebar/Popout/index.js @@ -0,0 +1,23 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { closeAiSidebar, dockAiChat } from 'providers/ReduxStore/slices/chat'; +import PopoutWindow from '../PopoutWindow'; +import AiChatSidebar from '../index'; + +const AiChatPopout = ({ collection }) => { + const dispatch = useDispatch(); + + const handleClose = useCallback(({ blocked } = {}) => { + // Closing the OS window closes the assistant (like undocked devtools). + // If window.open was blocked, fall back to the docked sidebar instead. + dispatch(blocked ? dockAiChat() : closeAiSidebar()); + }, [dispatch]); + + return ( + + + + ); +}; + +export default AiChatPopout; diff --git a/packages/bruno-app/src/components/AiChatSidebar/PopoutWindow/index.js b/packages/bruno-app/src/components/AiChatSidebar/PopoutWindow/index.js new file mode 100644 index 000000000..2bf59511a --- /dev/null +++ b/packages/bruno-app/src/components/AiChatSidebar/PopoutWindow/index.js @@ -0,0 +1,128 @@ +import { useState, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { StyleSheetManager } from 'styled-components'; + +// Must match the frameName prefix allow-listed in bruno-electron's +// setWindowOpenHandler, any other window.open is denied there. Each open +// gets a unique suffix so a remount (e.g. StrictMode's dev double-effect) +// never receives a handle to the previous, already-closing named window. +export const AI_POPOUT_WINDOW_NAME_PREFIX = 'bruno-ai-assistant'; +let popoutSeq = 0; + +// Copy every stylesheet already loaded in the main window into the popout +// document. We read cssRules instead of cloning the