- {bentoBoxContainers.length > 0 ? (
-
}
- rightContent={
-
- }
- initialLeftWidth={chatWidth}
- minLeftWidth={30}
- maxLeftWidth={80}
- onResize={setChatWidth}
- className="h-full"
- floatingRight={true}
- />
+ {panels.length > 0 ? (
+ useMultiPanel && panels.length > 1 ? (
+
}
+ panels={panels}
+ layoutMode={layoutMode}
+ onLayoutModeChange={handleLayoutModeChange}
+ onPanelResize={handlePanelResize}
+ onPanelReorder={handlePanelReorder}
+ initialLeftWidth={chatWidth}
+ className="h-full"
+ />
+ ) : (
+
}
+ rightContent={
+
+ }
+ initialLeftWidth={chatWidth}
+ minLeftWidth={30}
+ maxLeftWidth={80}
+ onResize={setChatWidth}
+ className="h-full"
+ floatingRight={true}
+ />
+ )
) : (
)}
diff --git a/ui/desktop/src/components/Layout/MultiPanelSplitter.tsx b/ui/desktop/src/components/Layout/MultiPanelSplitter.tsx
new file mode 100644
index 0000000000..df9aa5c923
--- /dev/null
+++ b/ui/desktop/src/components/Layout/MultiPanelSplitter.tsx
@@ -0,0 +1,352 @@
+import React, { useState, useCallback, useRef, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ GripVertical,
+ GripHorizontal,
+ Grid2X2,
+ Columns2,
+ Rows2,
+ LayoutGrid,
+ Maximize2,
+ Minimize2,
+ MoreVertical
+} from 'lucide-react';
+import { Button } from '../ui/button';
+import { Tooltip, TooltipTrigger, TooltipContent } from '../ui/Tooltip';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '../ui/dropdown-menu';
+
+export type LayoutMode = 'single' | 'columns' | 'rows' | 'grid' | 'custom';
+
+export interface PanelConfig {
+ id: string;
+ content: React.ReactNode;
+ title?: string;
+ minWidth?: number;
+ minHeight?: number;
+ position?: { row: number; col: number };
+ size?: { width: number; height: number }; // Percentages
+}
+
+interface MultiPanelSplitterProps {
+ leftContent: React.ReactNode;
+ panels: PanelConfig[];
+ layoutMode?: LayoutMode;
+ onLayoutModeChange?: (mode: LayoutMode) => void;
+ onPanelResize?: (panelId: string, size: { width: number; height: number }) => void;
+ onPanelReorder?: (panels: PanelConfig[]) => void;
+ className?: string;
+ initialLeftWidth?: number; // Percentage for main content
+}
+
+interface ResizeHandle {
+ type: 'vertical' | 'horizontal';
+ position: number; // Percentage
+ onResize: (delta: number) => void;
+}
+
+export const MultiPanelSplitter: React.FC
= ({
+ leftContent,
+ panels,
+ layoutMode = 'single',
+ onLayoutModeChange,
+ onPanelResize,
+ onPanelReorder,
+ className = '',
+ initialLeftWidth = 60
+}) => {
+ const [leftWidth, setLeftWidth] = useState(initialLeftWidth);
+ const [isDragging, setIsDragging] = useState(false);
+ const [dragHandle, setDragHandle] = useState(null);
+ const containerRef = useRef(null);
+
+ // Panel layout state for different modes
+ const [panelSizes, setPanelSizes] = useState>({});
+
+ // Initialize panel sizes
+ useEffect(() => {
+ const initialSizes: Record = {};
+ panels.forEach(panel => {
+ if (!panelSizes[panel.id]) {
+ initialSizes[panel.id] = panel.size || { width: 50, height: 50 };
+ }
+ });
+ if (Object.keys(initialSizes).length > 0) {
+ setPanelSizes(prev => ({ ...prev, ...initialSizes }));
+ }
+ }, [panels, panelSizes]);
+
+ const handleMainSplitterResize = useCallback((e: MouseEvent) => {
+ if (!isDragging || !containerRef.current) return;
+
+ const containerRect = containerRef.current.getBoundingClientRect();
+ const containerWidth = containerRect.width;
+ const mouseX = e.clientX - containerRect.left;
+
+ const newLeftWidth = Math.min(Math.max((mouseX / containerWidth) * 100, 20), 80);
+ setLeftWidth(newLeftWidth);
+ }, [isDragging]);
+
+ const handleMouseDown = useCallback((handleId: string) => {
+ setIsDragging(true);
+ setDragHandle(handleId);
+ }, []);
+
+ const handleMouseUp = useCallback(() => {
+ setIsDragging(false);
+ setDragHandle(null);
+ }, []);
+
+ useEffect(() => {
+ if (isDragging) {
+ document.addEventListener('mousemove', handleMainSplitterResize);
+ document.addEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = 'col-resize';
+ document.body.style.userSelect = 'none';
+ }
+
+ return () => {
+ document.removeEventListener('mousemove', handleMainSplitterResize);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ };
+ }, [isDragging, handleMainSplitterResize, handleMouseUp]);
+
+ const renderLayoutControls = () => (
+
+
+
+
+
+ Two Columns
+
+
+
+
+
+
+ Two Rows
+
+
+
+
+
+
+ 2x2 Grid
+
+
+
+
+
+
+
+ onLayoutModeChange?.('single')}>
+ Single Panel
+
+ onLayoutModeChange?.('custom')}>
+ Custom Layout
+
+
+
+
+ );
+
+ const renderPanelContent = (panel: PanelConfig, style?: React.CSSProperties) => (
+
+ {/* Panel Header */}
+ {panel.title && (
+
+ {panel.title}
+
+ )}
+
+ {/* Panel Content */}
+
+ {panel.content}
+
+
+ );
+
+ const renderRightPanel = () => {
+ if (panels.length === 0) {
+ return (
+
+ );
+ }
+
+ if (panels.length === 1) {
+ return renderPanelContent(panels[0], { height: '100%' });
+ }
+
+ switch (layoutMode) {
+ case 'columns':
+ return (
+
+
+ {panels.slice(0, 2).map((panel, index) => (
+ renderPanelContent(panel, {
+ flex: '1 1 0%',
+ height: '100%',
+ minWidth: panel.minWidth || 200
+ })
+ ))}
+
+
+ );
+
+ case 'rows':
+ return (
+
+
+ {panels.slice(0, 2).map((panel, index) => (
+ renderPanelContent(panel, {
+ flex: '1 1 0%',
+ width: '100%',
+ minHeight: panel.minHeight || 150
+ })
+ ))}
+
+
+ );
+
+ case 'grid':
+ return (
+
+
+ {panels.slice(0, 4).map((panel, index) => (
+ renderPanelContent(panel, {
+ minHeight: panel.minHeight || 150,
+ minWidth: panel.minWidth || 200
+ })
+ ))}
+
+
+ );
+
+ case 'custom':
+ // For custom layout, use absolute positioning based on panel positions
+ return (
+
+
+ {panels.map((panel) => {
+ const size = panelSizes[panel.id] || { width: 50, height: 50 };
+ const position = panel.position || { row: 0, col: 0 };
+
+ return renderPanelContent(panel, {
+ position: 'absolute',
+ left: `${position.col * 50}%`,
+ top: `${position.row * 50}%`,
+ width: `${size.width}%`,
+ height: `${size.height}%`,
+ minWidth: panel.minWidth || 200,
+ minHeight: panel.minHeight || 150
+ });
+ })}
+
+
+ );
+
+ default:
+ return renderPanelContent(panels[0], { height: '100%' });
+ }
+ };
+
+ const rightWidth = 100 - leftWidth;
+
+ return (
+
+ {/* Left Panel (Main Content) */}
+
+ {leftContent}
+
+
+ {/* Main Splitter */}
+ {panels.length > 0 && (
+
handleMouseDown('main')}
+ >
+ {/* Visual grip indicator */}
+
+
+
+
+ {/* Invisible wider hit area for easier dragging */}
+
+
+ )}
+
+ {/* Right Panel (Multi-Panel Area) */}
+ {panels.length > 0 && (
+
+
+
+ {renderRightPanel()}
+ {renderLayoutControls()}
+
+
+
+ )}
+
+ );
+};
+
+export default MultiPanelSplitter;
diff --git a/ui/desktop/src/components/Layout/ResizableSplitter.tsx b/ui/desktop/src/components/Layout/ResizableSplitter.tsx
index b5055816ee..3b66f446c0 100644
--- a/ui/desktop/src/components/Layout/ResizableSplitter.tsx
+++ b/ui/desktop/src/components/Layout/ResizableSplitter.tsx
@@ -116,9 +116,7 @@ export const ResizableSplitter: React.FC = ({
>
{floatingRight ? (
-
- {rightContent}
-
+ {rightContent}
) : (
rightContent
diff --git a/ui/desktop/src/components/Layout/SidecarInvoker.tsx b/ui/desktop/src/components/Layout/SidecarInvoker.tsx
index a4de498ddc..9daf102435 100644
--- a/ui/desktop/src/components/Layout/SidecarInvoker.tsx
+++ b/ui/desktop/src/components/Layout/SidecarInvoker.tsx
@@ -112,28 +112,13 @@ export const SidecarInvoker: React.FC = ({
const handleMouseEnter = async () => {
setIsHovering(true);
- // Create iframe backdrops to show live content behind the dock
- try {
- const result = await window.electron.createIframeBackdrop();
- if (result.success && result.backdropData) {
- setIframeBackdrops(result.backdropData);
- console.log('🎬 Created iframe backdrops:', result.backdropData.length);
- }
- } catch (error) {
- console.error('Failed to create iframe backdrop:', error);
- }
+ // Disabled screenshot backdrop functionality to prevent WebBrowser interference
+ // The screenshot capture was causing weird behavior when hovering over dock
};
const handleMouseLeave = async () => {
setIsHovering(false);
- // Remove iframe backdrops and restore BrowserViews
- try {
- await window.electron.removeIframeBackdrop();
- setIframeBackdrops([]);
- console.log('🎬 Removed iframe backdrops');
- } catch (error) {
- console.error('Failed to remove iframe backdrop:', error);
- }
+ // Disabled screenshot backdrop functionality to prevent WebBrowser interference
};
// Define dock apps with proper icons and colors
diff --git a/ui/desktop/src/components/MultiPanelTabSidecar.tsx b/ui/desktop/src/components/MultiPanelTabSidecar.tsx
new file mode 100644
index 0000000000..781ffd16c3
--- /dev/null
+++ b/ui/desktop/src/components/MultiPanelTabSidecar.tsx
@@ -0,0 +1,704 @@
+import React, { useState, useCallback, useRef } from 'react';
+import { motion, AnimatePresence, useDragControls } from 'framer-motion';
+import {
+ X,
+ SquareSplitHorizontal,
+ BetweenHorizontalStart,
+ FileDiff,
+ Globe,
+ FileText,
+ Edit,
+ Monitor,
+ MoreVertical,
+ Plus,
+ GripVertical,
+ Move
+} from 'lucide-react';
+import { Button } from './ui/button';
+import { Tooltip, TooltipTrigger, TooltipContent } from './ui/Tooltip';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from './ui/dropdown-menu';
+import { TabSidecarState, TabSidecarView } from './TabBar';
+import DocumentEditor from './DocumentEditor';
+import WebBrowser from './WebBrowser';
+
+export type SidecarLayoutMode = 'single' | 'columns' | 'rows' | 'grid' | 'custom';
+
+interface MultiPanelTabSidecarProps {
+ sidecarState: TabSidecarState;
+ onHideView: (viewId: string) => void;
+ onShowView?: (view: TabSidecarView) => void;
+ tabId: string;
+ className?: string;
+}
+
+// Component renderers for different sidecar types
+const MonacoDiffViewer: React.FC<{ diffContent: string }> = ({ diffContent }) => (
+
+);
+
+const LocalhostViewer: React.FC<{ url: string; title: string }> = ({ url, title }) => (
+
+
+
+);
+
+const SimpleFileViewer: React.FC<{ path: string }> = ({ path }) => (
+
+
File: {path}
+
+ File viewer for: {path}
+
+ (Content loading would be implemented here)
+
+
+);
+
+const RichDocumentEditor: React.FC<{ path?: string; content?: string }> = ({ path, content }) => (
+
+
+
+);
+
+// Icon renderer
+const renderIcon = (iconType: string) => {
+ switch (iconType) {
+ case 'diff':
+ return ;
+ case 'localhost':
+ return ;
+ case 'web':
+ return ;
+ case 'file':
+ return ;
+ case 'editor':
+ return ;
+ default:
+ return ;
+ }
+};
+
+// Content renderer
+const renderContent = (contentType: string, contentProps: Record, tabId: string, viewId: string) => {
+ const key = `${contentType}-${tabId}-${viewId}`;
+
+ switch (contentType) {
+ case 'diff':
+ return ;
+ case 'localhost':
+ return ;
+ case 'web':
+ return ;
+ case 'file':
+ return ;
+ case 'editor':
+ return ;
+ default:
+ return Unknown content type: {contentType}
;
+ }
+};
+
+// Drop zone types
+type DropZoneType = 'left' | 'right' | 'top' | 'bottom' | 'center' | 'grid-1' | 'grid-2' | 'grid-3' | 'grid-4';
+
+interface DropZone {
+ type: DropZoneType;
+ bounds: { x: number; y: number; width: number; height: number };
+ layoutMode: SidecarLayoutMode;
+ position?: number;
+}
+
+// Individual Panel Component with drag functionality
+interface PanelProps {
+ view: TabSidecarView;
+ tabId: string;
+ onHideView: (viewId: string) => void;
+ onPanelDrop?: (draggedViewId: string, dropZone: DropZone) => void;
+ layoutMode: SidecarLayoutMode;
+ style?: React.CSSProperties;
+ className?: string;
+ isDragging?: boolean;
+ position?: number;
+}
+
+const Panel: React.FC = ({
+ view,
+ tabId,
+ onHideView,
+ onPanelDrop,
+ layoutMode,
+ style,
+ className,
+ isDragging = false,
+ position = 0
+}) => {
+ const [viewMode, setViewMode] = useState<'split' | 'unified'>('unified');
+ const [isLocalDragging, setIsLocalDragging] = useState(false);
+ const dragControls = useDragControls();
+ const panelRef = useRef(null);
+
+ const isDiffViewer = view.contentType === 'diff';
+
+ // Update the diff viewer when view mode changes
+ React.useEffect(() => {
+ if (isDiffViewer && (window as any).diffViewerControls) {
+ (window as any).diffViewerControls.setViewMode(viewMode);
+ }
+ }, [viewMode, isDiffViewer]);
+
+ const handleDragStart = () => {
+ setIsLocalDragging(true);
+ };
+
+ const handleDragEnd = (event: any, info: any) => {
+ setIsLocalDragging(false);
+
+ if (!onPanelDrop || !panelRef.current) return;
+
+ // Get the drop position
+ const dropX = info.point.x;
+ const dropY = info.point.y;
+
+ // Find the sidecar container to determine drop zones
+ const sidecarContainer = panelRef.current.closest('.sidecar-container');
+ if (!sidecarContainer) return;
+
+ const containerRect = sidecarContainer.getBoundingClientRect();
+ const relativeX = dropX - containerRect.left;
+ const relativeY = dropY - containerRect.top;
+
+ // Determine drop zone based on position
+ const centerX = containerRect.width / 2;
+ const centerY = containerRect.height / 2;
+ const threshold = 100; // pixels from edge to trigger zone
+
+ let dropZone: DropZone | null = null;
+
+ if (relativeX < threshold) {
+ // Left zone - columns layout
+ dropZone = {
+ type: 'left',
+ bounds: { x: 0, y: 0, width: centerX, height: containerRect.height },
+ layoutMode: 'columns',
+ position: 0
+ };
+ } else if (relativeX > containerRect.width - threshold) {
+ // Right zone - columns layout
+ dropZone = {
+ type: 'right',
+ bounds: { x: centerX, y: 0, width: centerX, height: containerRect.height },
+ layoutMode: 'columns',
+ position: 1
+ };
+ } else if (relativeY < threshold) {
+ // Top zone - rows layout
+ dropZone = {
+ type: 'top',
+ bounds: { x: 0, y: 0, width: containerRect.width, height: centerY },
+ layoutMode: 'rows',
+ position: 0
+ };
+ } else if (relativeY > containerRect.height - threshold) {
+ // Bottom zone - rows layout
+ dropZone = {
+ type: 'bottom',
+ bounds: { x: 0, y: centerY, width: containerRect.width, height: centerY },
+ layoutMode: 'rows',
+ position: 1
+ };
+ } else {
+ // Center zone - determine grid position
+ const gridX = relativeX < centerX ? 0 : 1;
+ const gridY = relativeY < centerY ? 0 : 1;
+ const gridPosition = gridY * 2 + gridX;
+
+ dropZone = {
+ type: `grid-${gridPosition + 1}` as DropZoneType,
+ bounds: {
+ x: gridX * centerX,
+ y: gridY * centerY,
+ width: centerX,
+ height: centerY
+ },
+ layoutMode: 'grid',
+ position: gridPosition
+ };
+ }
+
+ if (dropZone) {
+ onPanelDrop(view.id, dropZone);
+ }
+ };
+
+ // Render as a sidecar component (matching TabSidecar structure)
+ return (
+
+
+ {/* Sidecar Header */}
+
+
+ {/* Drag Handle */}
+
dragControls.start(e)}
+ whileHover={{ scale: 1.1 }}
+ whileTap={{ scale: 0.9 }}
+ >
+
+
+
+ {renderIcon(view.iconType)}
+
+ {view.title}
+ {view.fileName && (
+ {view.fileName}
+ )}
+
+
+
+
+ {/* View Mode Toggle - Only show for diff viewer */}
+ {isDiffViewer && (
+
+
+
+
+
+
+ Unified View
+
+
+
+
+
+
+
+
+ Split View
+
+
+
+ )}
+
+ {/* Close Button */}
+
+
+
+
+ Close
+
+
+
+
+ {/* Sidecar Content */}
+
+ {renderContent(view.contentType, view.contentProps, tabId)}
+
+
+
+ );
+};
+
+export const MultiPanelTabSidecar: React.FC = ({
+ sidecarState,
+ onHideView,
+ onShowView,
+ tabId,
+ className = ''
+}) => {
+ const [layoutMode, setLayoutMode] = useState('single');
+ const [panelOrder, setPanelOrder] = useState([]);
+ const [isDragActive, setIsDragActive] = useState(false);
+ const [dropZones, setDropZones] = useState([]);
+ const containerRef = useRef(null);
+
+ // Get active views - memoize to prevent recreation on every render
+ const activeViews = React.useMemo(() =>
+ sidecarState.views.filter(view =>
+ sidecarState.activeViews.includes(view.id)
+ ),
+ [sidecarState.views, sidecarState.activeViews]
+ );
+
+ // Initialize panel order when views change
+ React.useEffect(() => {
+ const currentViewIds = activeViews.map(v => v.id);
+ setPanelOrder(prev => {
+ // Keep existing order for panels that still exist, add new ones at the end
+ const existingOrder = prev.filter(id => currentViewIds.includes(id));
+ const newPanels = currentViewIds.filter(id => !prev.includes(id));
+ return [...existingOrder, ...newPanels];
+ });
+ }, [activeViews]);
+
+ // Auto-adjust layout mode based on number of active views
+ React.useEffect(() => {
+ if (activeViews.length <= 1 && layoutMode !== 'single') {
+ setLayoutMode('single');
+ } else if (activeViews.length === 2 && layoutMode === 'single') {
+ setLayoutMode('columns');
+ } else if (activeViews.length > 2 && (layoutMode === 'single' || layoutMode === 'columns')) {
+ setLayoutMode('grid');
+ }
+ }, [activeViews.length]); // Remove layoutMode from dependencies to prevent infinite loop
+
+ const handlePanelDrop = useCallback((draggedViewId: string, dropZone: DropZone) => {
+ console.log('Panel dropped:', draggedViewId, 'in zone:', dropZone);
+
+ // Update layout mode based on drop zone
+ setLayoutMode(prev => {
+ if (dropZone.layoutMode !== prev) {
+ return dropZone.layoutMode;
+ }
+ return prev;
+ });
+
+ // Reorder panels based on drop position
+ if (dropZone.position !== undefined) {
+ setPanelOrder(prev => {
+ const newOrder = prev.filter(id => id !== draggedViewId);
+ newOrder.splice(dropZone.position!, 0, draggedViewId);
+ return newOrder;
+ });
+ }
+
+ setIsDragActive(false);
+ setDropZones([]);
+ }, []); // Remove layoutMode dependency
+
+ // Calculate drop zones when dragging starts
+ const calculateDropZones = useCallback(() => {
+ if (!containerRef.current) return [];
+
+ const container = containerRef.current;
+ const rect = container.getBoundingClientRect();
+ const zones: DropZone[] = [];
+
+ const threshold = 80;
+ const centerX = rect.width / 2;
+ const centerY = rect.height / 2;
+
+ // Left zone (columns)
+ zones.push({
+ type: 'left',
+ bounds: { x: 0, y: 0, width: threshold, height: rect.height },
+ layoutMode: 'columns',
+ position: 0
+ });
+
+ // Right zone (columns)
+ zones.push({
+ type: 'right',
+ bounds: { x: rect.width - threshold, y: 0, width: threshold, height: rect.height },
+ layoutMode: 'columns',
+ position: 1
+ });
+
+ // Top zone (rows)
+ zones.push({
+ type: 'top',
+ bounds: { x: 0, y: 0, width: rect.width, height: threshold },
+ layoutMode: 'rows',
+ position: 0
+ });
+
+ // Bottom zone (rows)
+ zones.push({
+ type: 'bottom',
+ bounds: { x: 0, y: rect.height - threshold, width: rect.width, height: threshold },
+ layoutMode: 'rows',
+ position: 1
+ });
+
+ // Grid zones (center area)
+ if (activeViews.length > 2) {
+ for (let i = 0; i < 4; i++) {
+ const gridX = (i % 2) * centerX;
+ const gridY = Math.floor(i / 2) * centerY;
+
+ zones.push({
+ type: `grid-${i + 1}` as DropZoneType,
+ bounds: {
+ x: gridX + threshold/2,
+ y: gridY + threshold/2,
+ width: centerX - threshold,
+ height: centerY - threshold
+ },
+ layoutMode: 'grid',
+ position: i
+ });
+ }
+ }
+
+ return zones;
+ }, [activeViews.length]);
+
+ // Handle drag start globally
+ React.useEffect(() => {
+ const handleDragStart = () => {
+ setIsDragActive(true);
+ setDropZones(calculateDropZones());
+ };
+
+ const handleDragEnd = () => {
+ setIsDragActive(false);
+ setDropZones([]);
+ };
+
+ // Listen for drag events on panels
+ document.addEventListener('dragstart', handleDragStart);
+ document.addEventListener('dragend', handleDragEnd);
+
+ return () => {
+ document.removeEventListener('dragstart', handleDragStart);
+ document.removeEventListener('dragend', handleDragEnd);
+ };
+ }, []); // Empty dependency array to prevent infinite loop
+
+ if (activeViews.length === 0) {
+ return null;
+ }
+
+ const renderDropZones = () => {
+ if (!isDragActive || dropZones.length === 0) return null;
+
+ return (
+
+ {dropZones.map((zone, index) => (
+
+
+
+ {zone.type === 'left' ? 'Left Column' :
+ zone.type === 'right' ? 'Right Column' :
+ zone.type === 'top' ? 'Top Row' :
+ zone.type === 'bottom' ? 'Bottom Row' :
+ zone.type.startsWith('grid-') ? `Grid ${zone.type.split('-')[1]}` :
+ 'Drop Here'}
+
+
+
+ ))}
+
+ );
+ };
+
+ // Render individual sidecar panels directly without wrapping container
+ const renderPanels = () => {
+ if (activeViews.length === 0) return null;
+
+ if (activeViews.length === 1 || layoutMode === 'single') {
+ return (
+
+ );
+ }
+
+ switch (layoutMode) {
+ case 'columns':
+ return (
+
+
+ {activeViews.slice(0, 2).map((view) => (
+
+ ))}
+
+
+ );
+
+ case 'rows':
+ return (
+
+
+ {activeViews.slice(0, 2).map((view) => (
+
+ ))}
+
+
+ );
+
+ case 'grid':
+ return (
+
+
+ {activeViews.slice(0, 4).map((view) => (
+
+ ))}
+
+
+ );
+
+ case 'custom':
+ // For custom layout, we could implement drag-and-drop positioning
+ return (
+
+
+ {activeViews.map((view, index) => (
+
+ ))}
+
+
+ );
+
+ default:
+ return (
+
+ );
+ }
+ };
+
+ // Return the panels directly with drop zones overlay, no additional container
+ return (
+
+ {renderPanels()}
+ {renderDropZones()}
+
+ );
+};
+
+export default MultiPanelTabSidecar;
diff --git a/ui/desktop/src/components/TabSidecar.tsx b/ui/desktop/src/components/TabSidecar.tsx
index 770814cedf..459494a1b3 100644
--- a/ui/desktop/src/components/TabSidecar.tsx
+++ b/ui/desktop/src/components/TabSidecar.tsx
@@ -115,9 +115,9 @@ export const TabSidecar: React.FC = ({
}, [viewMode, isDiffViewer]);
return (
-
+
{/* Sidecar Header */}
diff --git a/ui/desktop/src/components/TabSidecarInvoker.tsx b/ui/desktop/src/components/TabSidecarInvoker.tsx
index d837084833..b2d5afce7b 100644
--- a/ui/desktop/src/components/TabSidecarInvoker.tsx
+++ b/ui/desktop/src/components/TabSidecarInvoker.tsx
@@ -118,28 +118,13 @@ export const TabSidecarInvoker: React.FC = ({
const handleMouseEnter = async () => {
setIsHovering(true);
- // Create iframe backdrops to show live content behind the dock
- try {
- const result = await window.electron.createIframeBackdrop();
- if (result.success && result.backdropData) {
- setIframeBackdrops(result.backdropData);
- console.log('🎬 Created iframe backdrops:', result.backdropData.length);
- }
- } catch (error) {
- console.error('Failed to create iframe backdrop:', error);
- }
+ // Disabled screenshot backdrop functionality to prevent WebBrowser interference
+ // The screenshot capture was causing weird behavior when hovering over dock
};
const handleMouseLeave = async () => {
setIsHovering(false);
- // Remove iframe backdrops and restore BrowserViews
- try {
- await window.electron.removeIframeBackdrop();
- setIframeBackdrops([]);
- console.log('🎬 Removed iframe backdrops');
- } catch (error) {
- console.error('Failed to remove iframe backdrop:', error);
- }
+ // Disabled screenshot backdrop functionality to prevent WebBrowser interference
};
// Define dock apps with proper icons and colors
diff --git a/ui/desktop/src/components/TabbedChatContainer.tsx b/ui/desktop/src/components/TabbedChatContainer.tsx
index 4558cfdda7..5647c5ff85 100644
--- a/ui/desktop/src/components/TabbedChatContainer.tsx
+++ b/ui/desktop/src/components/TabbedChatContainer.tsx
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { TabBar } from './TabBar';
import BaseChat2 from './BaseChat2';
import { TabSidecar } from './TabSidecar';
+import MultiPanelTabSidecar from './MultiPanelTabSidecar';
import { useTabContext } from '../contexts/TabContext';
import { ResizableSplitter } from './Layout/ResizableSplitter';
@@ -221,11 +222,19 @@ export const TabbedChatContainer: React.FC = ({
/>
}
rightContent={
- hideSidecarView(activeTabState.tab.id, viewId)}
- tabId={activeTabState.tab.id}
- />
+ sidecarState && sidecarState.activeViews.length > 1 ? (
+ hideSidecarView(activeTabState.tab.id, viewId)}
+ tabId={activeTabState.tab.id}
+ />
+ ) : (
+ hideSidecarView(activeTabState.tab.id, viewId)}
+ tabId={activeTabState.tab.id}
+ />
+ )
}
initialLeftWidth={chatWidth}
minLeftWidth={30}