import { debounce } from 'lodash'; import { nanoid } from 'nanoid'; import { StateCreator } from 'zustand'; import { ISplitViewTab, TabItem, TabState, TabType } from '../../../types/tab'; import { TabsState } from '../types'; /** * Split view operations middleware */ export const splitViewActionsMiddleware: StateCreator< TabsState, [], [], Pick > = (set, get) => { // Create debounced function to update database const debouncedDatabaseUpdate = debounce( (tabId: string, boundedRatio: number) => { get().updateTabData(tabId, { splitRatio: boundedRatio }); }, 300, { maxWait: 1000 }, ); // Helper function to create a deep copy of a tab const createTabCopy = (tab: TabItem): TabItem => { // Deep clone the tab and assign a new ID const tabCopy = JSON.parse(JSON.stringify(tab)) as TabItem; tabCopy.id = nanoid(); return tabCopy; }; return { // Create a new split view from two tabs createSplitViewFromTabs: async (tabId: string) => { const state = get(); // Find the active tab const activeTabId = state.activeTabId; if (!activeTabId || activeTabId === tabId) return; // Get the active tab and the target tab const activeTab = state.tabs.find(tab => tab.id === activeTabId); const targetTab = state.tabs.find(tab => tab.id === tabId); if (!activeTab || !targetTab) return; try { // Create a new split view tab const timestamp = Date.now(); const newSplitViewTab: ISplitViewTab = { id: nanoid(), type: TabType.SPLIT_VIEW, title: `${activeTab.title} | ${targetTab.title}`, state: TabState.ACTIVE, isPinned: false, createdAt: timestamp, updatedAt: timestamp, childTabs: [activeTab, targetTab], // Original references are fine here because we'll close these tabs splitRatio: 50, // Default to 50/50 split }; // Add the new tab directly using the backend service await window.service.agentBrowser.addTab(newSplitViewTab); // Close the original tabs await window.service.agentBrowser.closeTab(activeTabId); await window.service.agentBrowser.closeTab(tabId); // Set the new split view tab as active await window.service.agentBrowser.setActiveTab(newSplitViewTab.id); // Update the zustand store with the latest data from the backend const updatedTabs = await window.service.agentBrowser.getAllTabs(); const newActiveId = await window.service.agentBrowser.getActiveTabId(); set({ tabs: updatedTabs, activeTabId: newActiveId, }); } catch (error) { console.error('Failed to create split view tab:', error); } }, // Remove from split view removeFromSplitView: async (tabId: string) => { const state = get(); // Find the split view tab that contains this tab const splitViewTab = state.tabs.find( tab => tab.type === TabType.SPLIT_VIEW && (tab).childTabs.some(childTab => childTab.id === tabId), ) as ISplitViewTab | undefined; if (!splitViewTab) return; try { // Get the tab to remove const tabToRemove = splitViewTab.childTabs.find(tab => tab.id === tabId); if (!tabToRemove) return; // Get the remaining tabs const remainingTabs = splitViewTab.childTabs.filter(tab => tab.id !== tabId); // Create a standalone tab for the removed one (but don't activate it) const removedTabCopy = createTabCopy(tabToRemove); // Add the removed tab as a standalone tab, but don't make it active await window.service.agentBrowser.addTab(removedTabCopy); // Check if this was the last tab in the split view if (remainingTabs.length === 0) { // If no tabs remain, close the split view tab entirely await window.service.agentBrowser.closeTab(splitViewTab.id); // Find another tab to activate if available const allTabs = await window.service.agentBrowser.getAllTabs(); if (allTabs.length > 0) { await window.service.agentBrowser.setActiveTab(allTabs[0].id); } } else { // Otherwise, update the split view with remaining tabs await window.service.agentBrowser.updateTab(splitViewTab.id, { childTabs: remainingTabs, title: remainingTabs.map(tab => tab.title).join(' | '), }); // Make sure the split view tab stays active await window.service.agentBrowser.setActiveTab(splitViewTab.id); } // Update the zustand store with the latest data from the backend const tabs = await window.service.agentBrowser.getAllTabs(); const activeTabId = await window.service.agentBrowser.getActiveTabId(); set({ tabs, activeTabId, }); } catch (error) { console.error('Failed to remove tab from split view:', error); } }, // Update split ratio - Optimized with immediate UI update and debounced database updates updateSplitRatio: async (ratio: number): Promise => { const state = get(); // Find active split view tab const activeSplitViewTab = state.tabs.find( tab => tab.id === state.activeTabId && tab.type === TabType.SPLIT_VIEW, ) as ISplitViewTab | undefined; if (activeSplitViewTab) { // Calculate the bounded ratio value (between 20 and 80) const boundedRatio = Math.max(20, Math.min(80, ratio)); // Immediately update the UI by modifying the tab in state const updatedTabs = state.tabs.map(tab => tab.id === activeSplitViewTab.id ? { ...tab, splitRatio: boundedRatio } : tab ); // Update the state for immediate UI feedback set({ tabs: updatedTabs }); // Use debounced function for database updates debouncedDatabaseUpdate(activeSplitViewTab.id, boundedRatio); } // Return resolved promise to satisfy interface return Promise.resolve(); }, // Convert a regular tab to split view tab convertToSplitView: async (tabId: string) => { const state = get(); // Get the tab to convert const tabToConvert = state.tabs.find(tab => tab.id === tabId); if (!tabToConvert) return; try { // Create a new split view tab containing only the original tab const timestamp = Date.now(); const newSplitViewTab: ISplitViewTab = { id: nanoid(), type: TabType.SPLIT_VIEW, title: tabToConvert.title, state: TabState.ACTIVE, isPinned: tabToConvert.isPinned, createdAt: timestamp, updatedAt: timestamp, childTabs: [tabToConvert], // Original reference is fine here because we'll close this tab splitRatio: 50, // Default to 50/50 split }; // Add the new tab directly using the backend service await window.service.agentBrowser.addTab(newSplitViewTab); // Close the original tab await window.service.agentBrowser.closeTab(tabId); // Set the new split view tab as active await window.service.agentBrowser.setActiveTab(newSplitViewTab.id); // Update the zustand store with the latest data from the backend const tabs = await window.service.agentBrowser.getAllTabs(); const activeTabId = await window.service.agentBrowser.getActiveTabId(); set({ tabs, activeTabId, }); } catch (error) { console.error('Failed to convert tab to split view:', error); } }, // Add a tab to an existing split view addTabToSplitView: async (splitViewTabId: string, tabId: string) => { const state = get(); // Get the split view tab and the tab to add const splitViewTab = state.tabs.find(tab => tab.id === splitViewTabId && tab.type === TabType.SPLIT_VIEW) as ISplitViewTab | undefined; const tabToAdd = state.tabs.find(tab => tab.id === tabId); if (!splitViewTab || !tabToAdd) return; // Don't add if already in the split view const isAlreadyInSplitView = splitViewTab.childTabs.some(tab => tab.id === tabId); if (isAlreadyInSplitView) return; try { // Create a deep copy of the tab with a new ID const tabCopy = createTabCopy(tabToAdd); // Maximum of two tabs can be displayed side by side const updatedChildTabs = splitViewTab.childTabs.length >= 2 ? [splitViewTab.childTabs[0], tabCopy] // Replace second tab : [...splitViewTab.childTabs, tabCopy]; // Add as new tab // First directly call the backend updateTab service to ensure database update // This ensures the backend is updated before UI changes await window.service.agentBrowser.updateTab(splitViewTabId, { childTabs: updatedChildTabs, title: updatedChildTabs.map(tab => tab.title).join(' | '), }); // Set this tab as active using the direct backend call await window.service.agentBrowser.setActiveTab(splitViewTabId); // Close the original tab that was added await window.service.agentBrowser.closeTab(tabId); // Update the zustand store with the latest data from the backend const tabs = await window.service.agentBrowser.getAllTabs(); const activeTabId = await window.service.agentBrowser.getActiveTabId(); set({ tabs, activeTabId, }); } catch (error) { console.error('Failed to add tab to split view:', error); } }, }; };