mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat: implement bento box sidecar layout with expanding containers
- Add support for multiple horizontal sidecar containers - Containers expand to fill available space equally - Chat area is full width when no containers exist - SidecarInvoker creates containers directly (localhost, file, sidecar) - Hover plus buttons on container edges for adding more - Clean UI with no empty space - sidecars fill containers completely - Event-based communication between AppLayout and MainPanelLayout - Resizable chat panel and containers with drag handles - Remove containers with X button on hover
This commit is contained in:
@@ -1891,7 +1891,7 @@ export default function ChatInput({
|
||||
<div className="w-px h-4 bg-border-default mx-2" />
|
||||
|
||||
{/* Model selector, mode selector, alerts, summarize button */}
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center min-w-0 flex-shrink overflow-hidden">
|
||||
{/* Cost Tracker */}
|
||||
{COST_TRACKING_ENABLED && (
|
||||
<>
|
||||
@@ -1903,7 +1903,7 @@ export default function ChatInput({
|
||||
</>
|
||||
)}
|
||||
<Tooltip>
|
||||
<div>
|
||||
<div className="flex-shrink-0">
|
||||
<ModelsBottomBar
|
||||
sessionId={sessionId}
|
||||
dropdownRef={dropdownRef}
|
||||
@@ -1914,16 +1914,18 @@ export default function ChatInput({
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="w-px h-4 bg-border-default mx-2" />
|
||||
<BottomMenuModeSelection />
|
||||
<div className="w-px h-4 bg-border-default mx-2" />
|
||||
<div className="w-px h-4 bg-border-default mx-2 flex-shrink-0" />
|
||||
<div className="flex-shrink-0">
|
||||
<BottomMenuModeSelection />
|
||||
</div>
|
||||
<div className="w-px h-4 bg-border-default mx-2 flex-shrink-0" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setIsGoosehintsModalOpen?.(true)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex items-center text-text-default/70 hover:text-text-default text-xs cursor-pointer transition-colors px-0"
|
||||
className="flex items-center text-text-default/70 hover:text-text-default text-xs cursor-pointer transition-colors px-0 flex-shrink-0"
|
||||
>
|
||||
<FolderKey size={16} className="min-[1050px]:mr-1" />
|
||||
<span className="text-xs hidden min-[1050px]:inline">Hints</span>
|
||||
|
||||
@@ -108,6 +108,12 @@ const AppLayoutContent: React.FC<AppLayoutProps> = ({ setIsGoosehintsModalOpen }
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddContainer = (type: 'sidecar' | 'localhost' | 'file', filePath?: string) => {
|
||||
console.log('Add container requested:', type, filePath);
|
||||
// This will be handled by MainPanelLayout
|
||||
window.dispatchEvent(new CustomEvent('add-container', { detail: { type, filePath } }));
|
||||
};
|
||||
|
||||
// Listen for programmatic request to show the sidecar localhost viewer
|
||||
React.useEffect(() => {
|
||||
const handler = (e: globalThis.Event) => {
|
||||
@@ -120,10 +126,9 @@ const AppLayoutContent: React.FC<AppLayoutProps> = ({ setIsGoosehintsModalOpen }
|
||||
return () => window.removeEventListener('open-sidecar-localhost', handler);
|
||||
}, [sidecar]);
|
||||
|
||||
// Show sidecar invoker on chat-related pages when sidecar is not open
|
||||
// Show sidecar invoker on chat-related pages - now always show it since we support multiple sidecars
|
||||
const shouldShowSidecarInvoker =
|
||||
(location.pathname === '/' || location.pathname === '/chat' || location.pathname === '/pair') &&
|
||||
!(sidecar?.activeView && sidecar?.views.find((v) => v.id === sidecar.activeView));
|
||||
(location.pathname === '/' || location.pathname === '/chat' || location.pathname === '/pair');
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 w-full relative animate-fade-in">
|
||||
@@ -155,6 +160,7 @@ const AppLayoutContent: React.FC<AppLayoutProps> = ({ setIsGoosehintsModalOpen }
|
||||
<SidecarInvoker
|
||||
onShowLocalhost={handleShowLocalhost}
|
||||
onShowFileViewer={handleShowFileViewer}
|
||||
onAddContainer={handleAddContainer}
|
||||
isVisible={shouldShowSidecarInvoker}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,78 +1,230 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import { useSidecar, Sidecar } from '../SidecarLayout';
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { Plus, X, Globe, FileText } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
interface Container {
|
||||
interface SidecarContainer {
|
||||
id: string;
|
||||
position: 'right-top' | 'right-main' | 'right-bottom';
|
||||
content: React.ReactNode;
|
||||
contentType: 'sidecar' | 'localhost' | 'file' | null;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface ContainerContentProps {
|
||||
container: Container;
|
||||
onRemove: (id: string) => void;
|
||||
onSetContent: (id: string, content: React.ReactNode) => void;
|
||||
interface ContainerPopoverProps {
|
||||
onSelect: (type: 'sidecar' | 'localhost' | 'file') => void;
|
||||
onClose: () => void;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
function IndividualContainer({ container, onRemove, onSetContent }: ContainerContentProps) {
|
||||
const [selectedContent, setSelectedContent] = useState<'localhost' | 'sidecar' | null>(null);
|
||||
const ContainerPopover: React.FC<ContainerPopoverProps> = ({ onSelect, onClose, position }) => {
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleContentSelect = (contentType: 'localhost' | 'sidecar') => {
|
||||
setSelectedContent(contentType);
|
||||
|
||||
if (contentType === 'localhost') {
|
||||
onSetContent(container.id, (
|
||||
<div className="h-full bg-background-default rounded-lg border border-border-subtle overflow-hidden">
|
||||
<iframe
|
||||
src="http://localhost:3000"
|
||||
className="w-full h-full border-0"
|
||||
title="Localhost Viewer"
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
} else if (contentType === 'sidecar') {
|
||||
onSetContent(container.id, <Sidecar className="h-full" />);
|
||||
// Close on click outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [onClose]);
|
||||
|
||||
const handleLocalhostClick = async () => {
|
||||
onSelect('localhost');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleFileViewerClick = async () => {
|
||||
try {
|
||||
const filePath = await window.electron.selectFileOrDirectory();
|
||||
if (filePath) {
|
||||
onSelect('file');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening file dialog:', error);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSidecarClick = () => {
|
||||
onSelect('sidecar');
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full bg-background-muted rounded-lg border border-border-subtle relative group">
|
||||
<div className="absolute top-2 right-2 z-10">
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className="fixed z-50 bg-background-default border border-border-subtle rounded-lg shadow-xl p-2 min-w-[160px] animate-in fade-in slide-in-from-right-2 duration-200"
|
||||
style={{
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
transform: 'translate(-100%, -50%)'
|
||||
}}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Button
|
||||
onClick={() => onRemove(container.id)}
|
||||
onClick={handleSidecarClick}
|
||||
className="w-full justify-start text-left hover:bg-background-medium transition-colors duration-150"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-6 h-6 rounded-full bg-background-default/80 hover:bg-background-default opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Sidecar View
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleLocalhostClick}
|
||||
className="w-full justify-start text-left hover:bg-background-medium transition-colors duration-150"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Localhost Viewer
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleFileViewerClick}
|
||||
className="w-full justify-start text-left hover:bg-background-medium transition-colors duration-150"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Open File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ResizeHandle component for horizontal resizing between containers
|
||||
const ResizeHandle: React.FC<{
|
||||
onResize: (delta: number) => void;
|
||||
isResizing: boolean;
|
||||
}> = ({ onResize, isResizing }) => {
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
let startX = e.clientX;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta = e.clientX - startX;
|
||||
onResize(delta);
|
||||
startX = e.clientX;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}, [onResize]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-1 cursor-col-resize hover:bg-borderSubtle transition-colors group ${
|
||||
isResizing ? 'bg-borderProminent' : ''
|
||||
}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div
|
||||
className={`h-8 w-0.5 bg-border-subtle group-hover:bg-border-strong rounded-full transition-colors my-auto ml-0.5 ${
|
||||
isResizing ? 'bg-border-strong' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Individual container component
|
||||
const ContainerComponent: React.FC<{
|
||||
container: SidecarContainer;
|
||||
onRemove: () => void;
|
||||
width: number;
|
||||
isLast: boolean;
|
||||
onAddAfter: () => void;
|
||||
}> = ({ container, onRemove, width, isLast, onAddAfter }) => {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverPosition, setPopoverPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const handleAddClick = (e: React.MouseEvent) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
setPopoverPosition({
|
||||
x: rect.right,
|
||||
y: rect.top + rect.height / 2
|
||||
});
|
||||
setShowPopover(true);
|
||||
};
|
||||
|
||||
const handlePopoverSelect = (type: 'sidecar' | 'localhost' | 'file') => {
|
||||
onAddAfter();
|
||||
// The parent will handle creating the container with the selected type
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full relative group flex-shrink-0 overflow-hidden"
|
||||
style={{ width: `${width}px` }}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
{/* Container header with remove button - only show on hover */}
|
||||
<div className="absolute top-2 right-2 z-30">
|
||||
<Button
|
||||
onClick={onRemove}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-6 h-6 rounded-full bg-background-default/90 hover:bg-background-default opacity-0 group-hover:opacity-100 transition-opacity shadow-lg border border-border-subtle"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{container.content || (
|
||||
<div className="h-full flex flex-col items-center justify-center p-4 space-y-3">
|
||||
<p className="text-text-muted text-sm text-center">Select content for this container:</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => handleContentSelect('localhost')}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Localhost Viewer
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleContentSelect('sidecar')}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Sidecar
|
||||
</Button>
|
||||
{/* Container content - fills entire container */}
|
||||
<div className="h-full w-full relative">
|
||||
{container.content || (
|
||||
<div className="h-full w-full flex flex-col items-center justify-center p-4 space-y-3 bg-background-muted border border-border-subtle rounded-lg">
|
||||
<p className="text-text-muted text-sm text-center">Empty container</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right edge hover zone for adding new container */}
|
||||
{isLast && (
|
||||
<div
|
||||
className="absolute top-0 right-0 w-4 h-full z-10 pointer-events-auto"
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
{isHovering && (
|
||||
<div className="absolute right-0 top-1/2 transform translate-x-1/2 -translate-y-1/2">
|
||||
<Button
|
||||
onClick={handleAddClick}
|
||||
className="w-8 h-8 rounded-full bg-background-default border border-border-subtle shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200 pointer-events-auto"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Popover for content selection */}
|
||||
{showPopover && (
|
||||
<ContainerPopover
|
||||
onSelect={handlePopoverSelect}
|
||||
onClose={() => setShowPopover(false)}
|
||||
position={popoverPosition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const MainPanelLayout: React.FC<{
|
||||
children: React.ReactNode;
|
||||
@@ -80,349 +232,158 @@ export const MainPanelLayout: React.FC<{
|
||||
backgroundColor?: string;
|
||||
}> = ({ children, removeTopPadding = false, backgroundColor = 'bg-background-default' }) => {
|
||||
const sidecar = useSidecar();
|
||||
const [containers, setContainers] = useState<Container[]>([]);
|
||||
const [hoveredEdge, setHoveredEdge] = useState<string | null>(null);
|
||||
const [horizontalSplitRatio, setHorizontalSplitRatio] = useState(0.7); // 70% left, 30% right
|
||||
const [rightColumnHeights, setRightColumnHeights] = useState({
|
||||
rightTopHeight: '33.33%',
|
||||
rightMainHeight: '33.33%',
|
||||
rightBottomHeight: '33.33%'
|
||||
});
|
||||
|
||||
const [isHorizontalResizing, setIsHorizontalResizing] = useState(false);
|
||||
const [isVerticalResizing, setIsVerticalResizing] = useState(false);
|
||||
// State for sidecar containers
|
||||
const [containers, setContainers] = useState<SidecarContainer[]>([]);
|
||||
const [chatWidth, setChatWidth] = useState(600); // Fixed width for chat panel
|
||||
const [resizingIndex, setResizingIndex] = useState<number | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get containers by position
|
||||
const rightTopContainer = containers.find(c => c.position === 'right-top');
|
||||
const rightMainContainer = containers.find(c => c.position === 'right-main');
|
||||
const rightBottomContainer = containers.find(c => c.position === 'right-bottom');
|
||||
|
||||
// Check if main sidecar is visible
|
||||
const mainSidecarVisible = sidecar?.activeView && sidecar?.views.find(v => v.id === sidecar.activeView);
|
||||
const mainSidecarVisible = sidecar?.activeViews && sidecar?.activeViews.length > 0;
|
||||
|
||||
const addContainer = useCallback((position: 'right-top' | 'right-main' | 'right-bottom') => {
|
||||
const newContainer: Container = {
|
||||
// Calculate container widths (equal distribution)
|
||||
const containerWidth = useMemo(() => {
|
||||
if (containers.length === 0) return 0;
|
||||
|
||||
const availableWidth = (containerRef.current?.clientWidth || window.innerWidth) - chatWidth - (containers.length * 4); // 4px per resize handle
|
||||
return Math.max(300, availableWidth / containers.length);
|
||||
}, [containers.length, chatWidth]);
|
||||
|
||||
// Add a new container
|
||||
const addContainer = useCallback((contentType: 'sidecar' | 'localhost' | 'file' = 'sidecar', afterIndex?: number, filePath?: string) => {
|
||||
const newContainer: SidecarContainer = {
|
||||
id: `container-${Date.now()}`,
|
||||
position,
|
||||
content: null
|
||||
};
|
||||
setContainers(prev => [...prev, newContainer]);
|
||||
}, []);
|
||||
|
||||
const removeContainer = useCallback((id: string) => {
|
||||
setContainers(prev => prev.filter(c => c.id !== id));
|
||||
}, []);
|
||||
|
||||
const setContainerContent = useCallback((id: string, content: React.ReactNode) => {
|
||||
setContainers(prev => prev.map(c =>
|
||||
c.id === id ? { ...c, content } : c
|
||||
));
|
||||
}, []);
|
||||
|
||||
// Horizontal resize handlers
|
||||
const handleHorizontalMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsHorizontalResizing(true);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const newRatio = (e.clientX - rect.left) / rect.width;
|
||||
const clampedRatio = Math.max(0.2, Math.min(0.8, newRatio));
|
||||
setHorizontalSplitRatio(clampedRatio);
|
||||
content: null,
|
||||
contentType: null
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsHorizontalResizing(false);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}, []);
|
||||
|
||||
// Vertical resize handlers
|
||||
const handleVerticalMouseDown = useCallback((e: React.MouseEvent, resizeType: 'right-top-main' | 'right-main-bottom') => {
|
||||
e.preventDefault();
|
||||
setIsVerticalResizing(true);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const relativeY = (e.clientY - rect.top) / rect.height;
|
||||
|
||||
if (resizeType === 'right-top-main') {
|
||||
const newTopHeight = Math.max(0.1, Math.min(0.8, relativeY));
|
||||
const remainingHeight = 1 - newTopHeight;
|
||||
|
||||
setRightColumnHeights(prev => ({
|
||||
...prev,
|
||||
rightTopHeight: `${newTopHeight * 100}%`,
|
||||
rightMainHeight: rightBottomContainer
|
||||
? `${remainingHeight * 0.5 * 100}%`
|
||||
: `${remainingHeight * 100}%`,
|
||||
rightBottomHeight: rightBottomContainer
|
||||
? `${remainingHeight * 0.5 * 100}%`
|
||||
: prev.rightBottomHeight
|
||||
}));
|
||||
} else if (resizeType === 'right-main-bottom') {
|
||||
const topHeight = parseFloat(rightColumnHeights.rightTopHeight) / 100;
|
||||
const availableHeight = 1 - topHeight;
|
||||
const mainRelativeHeight = (relativeY - topHeight) / availableHeight;
|
||||
const clampedMainHeight = Math.max(0.1, Math.min(0.9, mainRelativeHeight));
|
||||
|
||||
setRightColumnHeights(prev => ({
|
||||
...prev,
|
||||
rightMainHeight: `${clampedMainHeight * availableHeight * 100}%`,
|
||||
rightBottomHeight: `${(1 - clampedMainHeight) * availableHeight * 100}%`
|
||||
}));
|
||||
// Create content based on type
|
||||
if (contentType === 'sidecar') {
|
||||
newContainer.content = <Sidecar className="h-full" />;
|
||||
newContainer.contentType = 'sidecar';
|
||||
newContainer.title = 'Sidecar';
|
||||
} else if (contentType === 'localhost') {
|
||||
const instanceId = `container-${newContainer.id}`;
|
||||
if (sidecar) {
|
||||
sidecar.showLocalhostViewer('http://localhost:3000', 'Localhost Viewer', instanceId);
|
||||
// Give the sidecar context time to create the view, then render it
|
||||
setTimeout(() => {
|
||||
setContainers(prev => prev.map(c =>
|
||||
c.id === newContainer.id
|
||||
? { ...c, content: <Sidecar className="h-full" viewId={`localhost-${instanceId}`} /> }
|
||||
: c
|
||||
));
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
newContainer.content = <div className="h-full flex items-center justify-center text-text-muted">Loading localhost viewer...</div>;
|
||||
newContainer.contentType = 'localhost';
|
||||
newContainer.title = 'Localhost Viewer';
|
||||
} else if (contentType === 'file' && filePath) {
|
||||
const instanceId = `container-${newContainer.id}`;
|
||||
if (sidecar) {
|
||||
sidecar.showFileViewer(filePath, instanceId);
|
||||
// Give the sidecar context time to create the view, then render it
|
||||
setTimeout(() => {
|
||||
setContainers(prev => prev.map(c =>
|
||||
c.id === newContainer.id
|
||||
? { ...c, content: <Sidecar className="h-full" viewId={`file-${instanceId}`} /> }
|
||||
: c
|
||||
));
|
||||
}, 100);
|
||||
}
|
||||
newContainer.content = <div className="h-full flex items-center justify-center text-text-muted">Loading file viewer...</div>;
|
||||
newContainer.contentType = 'file';
|
||||
newContainer.title = filePath?.split('/').pop() || 'File Viewer';
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsVerticalResizing(false);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}, [rightColumnHeights, rightBottomContainer]);
|
||||
|
||||
// Auto-adjust right column when containers are added/removed
|
||||
useEffect(() => {
|
||||
const rightContainers = containers.filter(c => c.position.startsWith('right-'));
|
||||
const count = rightContainers.length;
|
||||
|
||||
if (count === 0) return;
|
||||
|
||||
const equalHeight = `${100 / count}%`;
|
||||
setRightColumnHeights({
|
||||
rightTopHeight: rightTopContainer ? equalHeight : '0%',
|
||||
rightMainHeight: rightMainContainer ? equalHeight : '0%',
|
||||
rightBottomHeight: rightBottomContainer ? equalHeight : '0%'
|
||||
setContainers(prev => {
|
||||
if (afterIndex !== undefined) {
|
||||
const newContainers = [...prev];
|
||||
newContainers.splice(afterIndex + 1, 0, newContainer);
|
||||
return newContainers;
|
||||
}
|
||||
return [...prev, newContainer];
|
||||
});
|
||||
}, [containers.length, rightTopContainer, rightMainContainer, rightBottomContainer]);
|
||||
}, [sidecar]);
|
||||
|
||||
const hasRightContainers = containers.some(c => c.position.startsWith('right-'));
|
||||
// Remove a container
|
||||
const removeContainer = useCallback((containerId: string) => {
|
||||
setContainers(prev => prev.filter(c => c.id !== containerId));
|
||||
}, []);
|
||||
|
||||
// Handle chat panel resize
|
||||
const updateChatWidth = useCallback((delta: number) => {
|
||||
setChatWidth(prev => Math.max(300, Math.min(1000, prev + delta)));
|
||||
}, []);
|
||||
|
||||
// Handle container resize
|
||||
const updateContainerWidth = useCallback((index: number, delta: number) => {
|
||||
// For now, we'll keep equal widths and just trigger a re-render
|
||||
// In a more advanced implementation, you could have individual container widths
|
||||
console.log(`Resize container ${index} by ${delta}px`);
|
||||
}, []);
|
||||
|
||||
// Listen for add-container events from SidecarInvoker
|
||||
useEffect(() => {
|
||||
const handleAddContainer = (e: CustomEvent<{ type: 'sidecar' | 'localhost' | 'file'; filePath?: string }>) => {
|
||||
console.log('🔍 MainPanelLayout: Received add-container event:', e.detail.type, e.detail.filePath);
|
||||
addContainer(e.detail.type, undefined, e.detail.filePath);
|
||||
};
|
||||
|
||||
window.addEventListener('add-container', handleAddContainer as EventListener);
|
||||
return () => window.removeEventListener('add-container', handleAddContainer as EventListener);
|
||||
}, [addContainer]);
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
console.log('🔍 MainPanelLayout: containers:', containers.length);
|
||||
console.log('🔍 MainPanelLayout: mainSidecarVisible:', mainSidecarVisible);
|
||||
}, [containers.length, mainSidecarVisible]);
|
||||
|
||||
const hasContainers = containers.length > 0;
|
||||
|
||||
return (
|
||||
<div className={`h-dvh`} ref={containerRef}>
|
||||
<div
|
||||
className={`flex ${backgroundColor} flex-1 min-w-0 h-full min-h-0 ${removeTopPadding ? '' : 'pt-[32px]'}`}
|
||||
>
|
||||
{/* Left Panel (Main Content) */}
|
||||
{/* Chat Panel - Full width when no containers, fixed width when containers exist */}
|
||||
<div
|
||||
className="flex flex-col flex-1 min-w-0"
|
||||
style={{
|
||||
width: hasRightContainers || mainSidecarVisible ? `${horizontalSplitRatio * 100}%` : '100%'
|
||||
}}
|
||||
className={hasContainers ? "flex flex-col flex-shrink-0" : "flex flex-col flex-1"}
|
||||
style={hasContainers ? { width: `${chatWidth}px` } : {}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Horizontal Resize Handle */}
|
||||
{(hasRightContainers || mainSidecarVisible) && (
|
||||
<div
|
||||
className={`w-1 cursor-col-resize hover:bg-borderSubtle transition-colors group ${
|
||||
isHorizontalResizing ? 'bg-borderProminent' : ''
|
||||
}`}
|
||||
onMouseDown={handleHorizontalMouseDown}
|
||||
>
|
||||
<div
|
||||
className={`h-8 w-0.5 bg-border-subtle group-hover:bg-border-strong rounded-full transition-colors my-auto ml-0.5 ${
|
||||
isHorizontalResizing ? 'bg-border-strong' : ''
|
||||
}`}
|
||||
{/* Chat Resize Handle - only show when containers exist */}
|
||||
{hasContainers && (
|
||||
<ResizeHandle
|
||||
onResize={updateChatWidth}
|
||||
isResizing={resizingIndex === -1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Container Layout - only show when containers exist */}
|
||||
{hasContainers && containers.map((container, index) => (
|
||||
<React.Fragment key={container.id}>
|
||||
<ContainerComponent
|
||||
container={container}
|
||||
onRemove={() => removeContainer(container.id)}
|
||||
width={containerWidth}
|
||||
isLast={index === containers.length - 1}
|
||||
onAddAfter={() => addContainer('sidecar', index)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Panel */}
|
||||
{(hasRightContainers || mainSidecarVisible) && (
|
||||
<div
|
||||
className="flex flex-col relative"
|
||||
style={{
|
||||
width: `${(1 - horizontalSplitRatio) * 100}%`,
|
||||
minWidth: '200px'
|
||||
}}
|
||||
>
|
||||
{/* Main Sidecar (takes precedence when visible) */}
|
||||
{mainSidecarVisible && !hasRightContainers && (
|
||||
<div className="h-full">
|
||||
<Sidecar />
|
||||
</div>
|
||||
{/* Resize Handle between containers */}
|
||||
{index < containers.length - 1 && (
|
||||
<ResizeHandle
|
||||
onResize={(delta) => updateContainerWidth(index, delta)}
|
||||
isResizing={resizingIndex === index}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Multi-container layout */}
|
||||
{hasRightContainers && (
|
||||
<div className="h-full flex flex-col relative">
|
||||
{/* Right Top Container */}
|
||||
{rightTopContainer && (
|
||||
<div
|
||||
className="relative"
|
||||
style={{ height: rightColumnHeights.rightTopHeight }}
|
||||
>
|
||||
<IndividualContainer
|
||||
container={rightTopContainer}
|
||||
onRemove={removeContainer}
|
||||
onSetContent={setContainerContent}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Top-Main Resize Handle */}
|
||||
{rightTopContainer && rightMainContainer && (
|
||||
<div
|
||||
className={`h-1 cursor-row-resize hover:bg-borderSubtle transition-colors group ${
|
||||
isVerticalResizing ? 'bg-borderProminent' : ''
|
||||
}`}
|
||||
onMouseDown={(e) => handleVerticalMouseDown(e, 'right-top-main')}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-0.5 bg-border-subtle group-hover:bg-border-strong rounded-full transition-colors mx-auto mt-0.5 ${
|
||||
isVerticalResizing ? 'bg-border-strong' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Main Container */}
|
||||
{rightMainContainer && (
|
||||
<div
|
||||
className="relative"
|
||||
style={{ height: rightColumnHeights.rightMainHeight }}
|
||||
>
|
||||
<IndividualContainer
|
||||
container={rightMainContainer}
|
||||
onRemove={removeContainer}
|
||||
onSetContent={setContainerContent}
|
||||
/>
|
||||
|
||||
{/* Container addition zones for right main */}
|
||||
{!rightTopContainer && (
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 h-4 z-20 pointer-events-auto"
|
||||
onMouseEnter={() => setHoveredEdge('right-top')}
|
||||
onMouseLeave={() => setHoveredEdge(null)}
|
||||
>
|
||||
{hoveredEdge === 'right-top' && (
|
||||
<div className="absolute left-1/2 top-1 transform -translate-x-1/2">
|
||||
<Button
|
||||
onClick={() => addContainer('right-top')}
|
||||
className="w-6 h-6 rounded-full bg-background-default border border-border-subtle shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!rightBottomContainer && (
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 h-4 z-20 pointer-events-auto"
|
||||
onMouseEnter={() => setHoveredEdge('right-bottom')}
|
||||
onMouseLeave={() => setHoveredEdge(null)}
|
||||
>
|
||||
{hoveredEdge === 'right-bottom' && (
|
||||
<div className="absolute left-1/2 bottom-1 transform -translate-x-1/2">
|
||||
<Button
|
||||
onClick={() => addContainer('right-bottom')}
|
||||
className="w-6 h-6 rounded-full bg-background-default border border-border-subtle shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right-only container hover zones when no main sidecar */}
|
||||
{!rightTopContainer && !mainSidecarVisible && (
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 h-4 z-20 pointer-events-auto"
|
||||
onMouseEnter={() => setHoveredEdge('right-top')}
|
||||
onMouseLeave={() => setHoveredEdge(null)}
|
||||
>
|
||||
{hoveredEdge === 'right-top' && (
|
||||
<div className="absolute left-1/2 top-1 transform -translate-x-1/2">
|
||||
<Button
|
||||
onClick={() => addContainer('right-top')}
|
||||
className="w-6 h-6 rounded-full bg-background-default border border-border-subtle shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!rightBottomContainer && !mainSidecarVisible && (
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 h-4 z-20 pointer-events-auto"
|
||||
onMouseEnter={() => setHoveredEdge('right-bottom')}
|
||||
onMouseLeave={() => setHoveredEdge(null)}
|
||||
>
|
||||
{hoveredEdge === 'right-bottom' && (
|
||||
<div className="absolute left-1/2 bottom-1 transform -translate-x-1/2">
|
||||
<Button
|
||||
onClick={() => addContainer('right-bottom')}
|
||||
className="w-6 h-6 rounded-full bg-background-default border border-border-subtle shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-200"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Main-Bottom Resize Handle */}
|
||||
{rightMainContainer && rightBottomContainer && (
|
||||
<div
|
||||
className={`h-1 cursor-row-resize hover:bg-borderSubtle transition-colors group ${
|
||||
isVerticalResizing ? 'bg-borderProminent' : ''
|
||||
}`}
|
||||
onMouseDown={(e) => handleVerticalMouseDown(e, 'right-main-bottom')}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-0.5 bg-border-subtle group-hover:bg-border-strong rounded-full transition-colors mx-auto mt-0.5 ${
|
||||
isVerticalResizing ? 'bg-border-strong' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Bottom Container */}
|
||||
{rightBottomContainer && (
|
||||
<div
|
||||
className="relative"
|
||||
style={{ height: rightColumnHeights.rightBottomHeight }}
|
||||
>
|
||||
<IndividualContainer
|
||||
container={rightBottomContainer}
|
||||
onRemove={removeContainer}
|
||||
onSetContent={setContainerContent}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Plus, Globe, FileText } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { useSidecar } from '../SidecarLayout';
|
||||
|
||||
interface SidecarInvokerProps {
|
||||
onShowLocalhost: () => void;
|
||||
onShowFileViewer: (filePath: string) => void;
|
||||
onAddContainer: (type: 'sidecar' | 'localhost' | 'file', filePath?: string) => void;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export const SidecarInvoker: React.FC<SidecarInvokerProps> = ({
|
||||
onShowLocalhost,
|
||||
onShowFileViewer,
|
||||
onAddContainer,
|
||||
isVisible
|
||||
}) => {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Move the hook call to the top level of the component
|
||||
const sidecar = useSidecar();
|
||||
|
||||
// Handle click outside to close menu
|
||||
useEffect(() => {
|
||||
@@ -39,7 +45,8 @@ export const SidecarInvoker: React.FC<SidecarInvokerProps> = ({
|
||||
};
|
||||
|
||||
const handleLocalhostClick = () => {
|
||||
onShowLocalhost();
|
||||
console.log('🔍 SidecarInvoker: Localhost button clicked');
|
||||
onAddContainer('localhost');
|
||||
setShowMenu(false);
|
||||
setIsHovering(false);
|
||||
};
|
||||
@@ -54,7 +61,7 @@ export const SidecarInvoker: React.FC<SidecarInvokerProps> = ({
|
||||
console.log('Selected file path:', filePath);
|
||||
|
||||
if (filePath) {
|
||||
onShowFileViewer(filePath);
|
||||
onAddContainer('file', filePath);
|
||||
} else {
|
||||
console.log('No file selected');
|
||||
}
|
||||
@@ -66,6 +73,13 @@ export const SidecarInvoker: React.FC<SidecarInvokerProps> = ({
|
||||
setIsHovering(false);
|
||||
};
|
||||
|
||||
const handleSidecarClick = () => {
|
||||
console.log('🔍 SidecarInvoker: Sidecar button clicked');
|
||||
onAddContainer('sidecar');
|
||||
setShowMenu(false);
|
||||
setIsHovering(false);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovering(true);
|
||||
};
|
||||
@@ -114,6 +128,16 @@ export const SidecarInvoker: React.FC<SidecarInvokerProps> = ({
|
||||
className="absolute right-full mr-3 top-1/2 transform -translate-y-1/2 bg-background-default border border-border-subtle rounded-lg shadow-xl p-2 min-w-[160px] pointer-events-auto animate-in fade-in slide-in-from-right-2 duration-200"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Button
|
||||
onClick={handleSidecarClick}
|
||||
className="w-full justify-start text-left hover:bg-background-medium transition-colors duration-150"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Sidecar View
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleLocalhostClick}
|
||||
className="w-full justify-start text-left hover:bg-background-medium transition-colors duration-150"
|
||||
|
||||
@@ -11,19 +11,21 @@ interface SidecarView {
|
||||
icon: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
fileName?: string; // Optional fileName for diff viewer
|
||||
instanceId?: string; // Unique instance identifier for multiple views of same type
|
||||
}
|
||||
|
||||
interface SidecarContextType {
|
||||
activeView: string | null;
|
||||
activeViews: string[]; // Array of active view IDs instead of single activeView
|
||||
views: SidecarView[];
|
||||
showView: (view: SidecarView) => void;
|
||||
hideView: () => void;
|
||||
showDiffViewer: (diffContent: string, fileName?: string) => void;
|
||||
hideDiffViewer: () => void;
|
||||
showLocalhostViewer: (url?: string, title?: string) => void;
|
||||
hideLocalhostViewer: () => void;
|
||||
showFileViewer: (filePath: string) => void;
|
||||
hideFileViewer: () => void;
|
||||
hideView: (viewId: string) => void; // Hide specific view by ID
|
||||
hideAllViews: () => void; // Hide all views
|
||||
showDiffViewer: (diffContent: string, fileName?: string, instanceId?: string) => void;
|
||||
hideDiffViewer: (instanceId?: string) => void;
|
||||
showLocalhostViewer: (url?: string, title?: string, instanceId?: string) => void;
|
||||
hideLocalhostViewer: (instanceId?: string) => void;
|
||||
showFileViewer: (filePath: string, instanceId?: string) => void;
|
||||
hideFileViewer: (instanceId?: string) => void;
|
||||
}
|
||||
|
||||
const SidecarContext = createContext<SidecarContextType | null>(null);
|
||||
@@ -304,85 +306,106 @@ function MonacoDiffViewer({ diffContent, _fileName }: { diffContent: string; _fi
|
||||
}
|
||||
|
||||
export function SidecarProvider({ children, showSidecar = true }: SidecarProviderProps) {
|
||||
const [activeView, setActiveView] = useState<string | null>(null);
|
||||
const [activeViews, setActiveViews] = useState<string[]>([]);
|
||||
const [views, setViews] = useState<SidecarView[]>([]);
|
||||
|
||||
const showView = async (view: SidecarView) => {
|
||||
console.log('🔍 SidecarProvider: showView called with view:', view.id, view.title);
|
||||
|
||||
setViews((prev) => {
|
||||
const existing = prev.find((v) => v.id === view.id);
|
||||
if (existing) {
|
||||
console.log('🔍 SidecarProvider: Updating existing view:', view.id);
|
||||
return prev.map((v) => (v.id === view.id ? view : v));
|
||||
}
|
||||
console.log('🔍 SidecarProvider: Adding new view:', view.id);
|
||||
return [...prev, view];
|
||||
});
|
||||
|
||||
setActiveView(view.id);
|
||||
setActiveViews((prev) => {
|
||||
if (!prev.includes(view.id)) {
|
||||
console.log('🔍 SidecarProvider: Adding view to activeViews:', view.id);
|
||||
const newActiveViews = [...prev, view.id];
|
||||
console.log('🔍 SidecarProvider: New activeViews:', newActiveViews);
|
||||
return newActiveViews;
|
||||
}
|
||||
console.log('🔍 SidecarProvider: View already in activeViews:', view.id);
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const hideView = () => {
|
||||
setActiveView(null);
|
||||
const hideView = (viewId: string) => {
|
||||
setActiveViews((prev) => prev.filter((id) => id !== viewId));
|
||||
};
|
||||
|
||||
const showDiffViewer = (content: string, fileName = 'File') => {
|
||||
const hideAllViews = () => {
|
||||
setActiveViews([]);
|
||||
};
|
||||
|
||||
const showDiffViewer = (content: string, fileName = 'File', instanceId?: string) => {
|
||||
const id = instanceId ? `diff-${instanceId}` : 'diff';
|
||||
const diffView: SidecarView = {
|
||||
id: 'diff',
|
||||
id,
|
||||
title: 'Diff Viewer',
|
||||
icon: <FileDiff size={16} />,
|
||||
content: <MonacoDiffViewer diffContent={content} _fileName={fileName} />,
|
||||
fileName: fileName, // Store fileName for header display
|
||||
instanceId,
|
||||
};
|
||||
showView(diffView);
|
||||
};
|
||||
|
||||
const hideDiffViewer = () => {
|
||||
setViews((prev) => prev.filter((v) => v.id !== 'diff'));
|
||||
if (activeView === 'diff') {
|
||||
setActiveView(null);
|
||||
}
|
||||
const hideDiffViewer = (instanceId?: string) => {
|
||||
const id = instanceId ? `diff-${instanceId}` : 'diff';
|
||||
setViews((prev) => prev.filter((v) => v.id !== id));
|
||||
hideView(id);
|
||||
};
|
||||
|
||||
const showLocalhostViewer = (url = 'http://localhost:3000', title = 'Localhost Viewer') => {
|
||||
const showLocalhostViewer = (url = 'http://localhost:3000', title = 'Localhost Viewer', instanceId?: string) => {
|
||||
const id = instanceId ? `localhost-${instanceId}` : 'localhost';
|
||||
const localhostView: SidecarView = {
|
||||
id: 'localhost',
|
||||
id,
|
||||
title: title,
|
||||
icon: <Globe size={16} />,
|
||||
content: <SidecarTabs initialUrl={url} />,
|
||||
fileName: url,
|
||||
instanceId,
|
||||
};
|
||||
showView(localhostView);
|
||||
};
|
||||
|
||||
const hideLocalhostViewer = () => {
|
||||
setViews((prev) => prev.filter((v) => v.id !== 'localhost'));
|
||||
if (activeView === 'localhost') {
|
||||
setActiveView(null);
|
||||
}
|
||||
const hideLocalhostViewer = (instanceId?: string) => {
|
||||
const id = instanceId ? `localhost-${instanceId}` : 'localhost';
|
||||
setViews((prev) => prev.filter((v) => v.id !== id));
|
||||
hideView(id);
|
||||
};
|
||||
|
||||
const showFileViewer = (filePath: string) => {
|
||||
const showFileViewer = (filePath: string, instanceId?: string) => {
|
||||
const fileName = filePath.split('/').pop() || filePath;
|
||||
const id = instanceId ? `file-${instanceId}` : 'file';
|
||||
const fileView: SidecarView = {
|
||||
id: 'file',
|
||||
id,
|
||||
title: 'File Viewer',
|
||||
icon: <FileText size={16} />,
|
||||
content: <FileViewer filePath={filePath} />,
|
||||
fileName: fileName,
|
||||
instanceId,
|
||||
};
|
||||
showView(fileView);
|
||||
};
|
||||
|
||||
const hideFileViewer = () => {
|
||||
setViews((prev) => prev.filter((v) => v.id !== 'file'));
|
||||
if (activeView === 'file') {
|
||||
setActiveView(null);
|
||||
}
|
||||
const hideFileViewer = (instanceId?: string) => {
|
||||
const id = instanceId ? `file-${instanceId}` : 'file';
|
||||
setViews((prev) => prev.filter((v) => v.id !== id));
|
||||
hideView(id);
|
||||
};
|
||||
|
||||
const contextValue: SidecarContextType = {
|
||||
activeView,
|
||||
activeViews,
|
||||
views,
|
||||
showView,
|
||||
hideView,
|
||||
hideAllViews,
|
||||
showDiffViewer,
|
||||
hideDiffViewer,
|
||||
showLocalhostViewer,
|
||||
@@ -401,16 +424,18 @@ export function SidecarProvider({ children, showSidecar = true }: SidecarProvide
|
||||
}
|
||||
|
||||
// Separate Sidecar component that can be used as a sibling
|
||||
export function Sidecar({ className = '' }: { className?: string }) {
|
||||
export function Sidecar({ className = '', viewId }: { className?: string; viewId?: string }) {
|
||||
const sidecar = useSidecar();
|
||||
const [viewMode, setViewMode] = useState<'split' | 'unified'>('unified');
|
||||
|
||||
// Update the diff viewer when view mode changes
|
||||
useEffect(() => {
|
||||
if (sidecar) {
|
||||
const { activeView, views } = sidecar;
|
||||
const currentView = views.find((v) => v.id === activeView);
|
||||
const isDiffViewer = currentView?.id === 'diff';
|
||||
const { activeViews, views } = sidecar;
|
||||
const currentView = viewId
|
||||
? views.find((v) => v.id === viewId)
|
||||
: views.find((v) => activeViews.includes(v.id));
|
||||
const isDiffViewer = currentView?.id?.startsWith('diff');
|
||||
|
||||
if (
|
||||
isDiffViewer &&
|
||||
@@ -433,18 +458,33 @@ export function Sidecar({ className = '' }: { className?: string }) {
|
||||
).diffViewerControls!.setViewMode(viewMode);
|
||||
}
|
||||
}
|
||||
}, [viewMode, sidecar]);
|
||||
}, [viewMode, sidecar, viewId]);
|
||||
|
||||
if (!sidecar) return null;
|
||||
|
||||
const { activeView, views, hideView } = sidecar;
|
||||
const currentView = views.find((v) => v.id === activeView);
|
||||
const isVisible = activeView && currentView;
|
||||
const { activeViews, views, hideView } = sidecar;
|
||||
|
||||
// Determine which view to show
|
||||
const currentView = viewId
|
||||
? views.find((v) => v.id === viewId && activeViews.includes(v.id))
|
||||
: views.find((v) => activeViews.includes(v.id));
|
||||
|
||||
const isVisible = currentView && activeViews.includes(currentView.id);
|
||||
|
||||
if (!isVisible) return null;
|
||||
console.log('🔍 Sidecar: Render check');
|
||||
console.log('🔍 Sidecar: viewId prop:', viewId);
|
||||
console.log('🔍 Sidecar: activeViews:', activeViews);
|
||||
console.log('🔍 Sidecar: views:', views?.map(v => ({ id: v.id, title: v.title })));
|
||||
console.log('🔍 Sidecar: currentView:', currentView ? { id: currentView.id, title: currentView.title } : null);
|
||||
console.log('🔍 Sidecar: isVisible:', isVisible);
|
||||
|
||||
if (!isVisible) {
|
||||
console.log('🔍 Sidecar: Not visible, returning null');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if current view is diff viewer
|
||||
const isDiffViewer = currentView.id === 'diff';
|
||||
const isDiffViewer = currentView.id.startsWith('diff');
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -520,7 +560,7 @@ export function Sidecar({ className = '' }: { className?: string }) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={hideView}
|
||||
onClick={() => hideView(currentView.id)}
|
||||
className="text-textSubtle hover:text-textStandard cursor-pointer focus:outline-none focus:ring-2 focus:ring-borderProminent focus:ring-offset-1"
|
||||
>
|
||||
<X size={16} />
|
||||
|
||||
Reference in New Issue
Block a user