## Summary Complete sidebar redesign and comprehensive UI polish pass with performance optimizations, theme system refinements, and desktop integration improvements. ## Key Changes **Sidebar & Navigation Redesign** - Redesigned sessions sidebar layout with unified button primitives - Added activity sections with project grouping and improved session organization - Refined sidebar corners, spacing, and visual hierarchy - Removed NavRail component in favor of streamlined sidebar - Stabilized sessions bar toggle position in fullscreen mode **Performance Optimizations** - Reduced chat streaming CPU usage and storage churn - Optimized task tool polling and live timers with debouncing - Prevented chat state races and reduced background request load - Debounced draft writes and coalesced session reloads - Optimized message store updates and turn tracking **Theme & Visual System** - Added theme-aware window corners (desktop) and border radius tokens - Introduced glassmorphism effects on desktop sidebar - Added backdrop blur to UI elements **Chat Experience** - Added session-based permission auto-accept toggle in chat input - Polished permission shield UX with improved icon sizing and spacing - Fixed chat scroll-to-bottom behavior and timeline tracking - Enhanced tool output display with better path label detection - Removed duplicate draft context details in chat header - Added text selection menu to chat messages **Git Improvements** - Refreshed git history visual design with cleaner dividers - Added remote removal action in sync selector - Stabilized git polling to prevent excessive requests - Improved tool output rendering for git operations **Settings & Panels** - Fixed mobile scrolling on settings pages - Made outside-click settings close instantly - Reduced settings load churn and CPU spikes - Improved services dropdown layout and spacing - Softened panel resize handles **Desktop Integration** - Synced macOS window theme with app theme - Restored window dragging in sidebar header zones - Fixed system window corners on macOS - Improved header session metadata and action controls **Button & Component Standardization** - Unified button primitives across all components - Standardized destructive action patterns - Removed unused button variants (button-large, button-small) - Aligned context tab close hit areas --------- Co-authored-by: Iuliia Ivashko <yulia.ivashko@gmail.com>
579 lines
18 KiB
TypeScript
579 lines
18 KiB
TypeScript
import React from 'react';
|
|
import { RiArrowLeftRightLine, RiChat4Line, RiCloseLine, RiDonutChartFill, RiFileTextLine, RiFullscreenExitLine, RiFullscreenLine } from '@remixicon/react';
|
|
|
|
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
|
import { Button } from '@/components/ui/button';
|
|
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
|
|
import { DiffView, FilesView, PlanView } from '@/components/views';
|
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
|
import { cn } from '@/lib/utils';
|
|
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
|
import { useUIStore } from '@/stores/useUIStore';
|
|
import { ContextPanelContent } from './ContextSidebarTab';
|
|
|
|
const CONTEXT_PANEL_MIN_WIDTH = 360;
|
|
const CONTEXT_PANEL_MAX_WIDTH = 1400;
|
|
const CONTEXT_PANEL_DEFAULT_WIDTH = 600;
|
|
const CONTEXT_TAB_LABEL_MAX_CHARS = 24;
|
|
|
|
const normalizeDirectoryKey = (value: string): string => {
|
|
if (!value) return '';
|
|
|
|
const raw = value.replace(/\\/g, '/');
|
|
const hadUncPrefix = raw.startsWith('//');
|
|
let normalized = raw.replace(/\/+$/g, '');
|
|
normalized = normalized.replace(/\/+/g, '/');
|
|
|
|
if (hadUncPrefix && !normalized.startsWith('//')) {
|
|
normalized = `/${normalized}`;
|
|
}
|
|
|
|
if (normalized === '') {
|
|
return raw.startsWith('/') ? '/' : '';
|
|
}
|
|
|
|
return normalized;
|
|
};
|
|
|
|
const clampWidth = (width: number): number => {
|
|
if (!Number.isFinite(width)) {
|
|
return CONTEXT_PANEL_DEFAULT_WIDTH;
|
|
}
|
|
|
|
return Math.min(CONTEXT_PANEL_MAX_WIDTH, Math.max(CONTEXT_PANEL_MIN_WIDTH, Math.round(width)));
|
|
};
|
|
|
|
const getRelativePathLabel = (filePath: string | null, directory: string): string => {
|
|
if (!filePath) {
|
|
return '';
|
|
}
|
|
const normalizedFile = filePath.replace(/\\/g, '/');
|
|
const normalizedDir = directory.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
if (normalizedDir && normalizedFile.startsWith(normalizedDir + '/')) {
|
|
return normalizedFile.slice(normalizedDir.length + 1);
|
|
}
|
|
return normalizedFile;
|
|
};
|
|
|
|
const getModeLabel = (mode: 'diff' | 'file' | 'context' | 'plan' | 'chat'): string => {
|
|
if (mode === 'chat') return 'Chat';
|
|
if (mode === 'file') return 'Files';
|
|
if (mode === 'diff') return 'Diff';
|
|
if (mode === 'plan') return 'Plan';
|
|
return 'Context';
|
|
};
|
|
|
|
const getFileNameFromPath = (path: string | null): string | null => {
|
|
if (!path) {
|
|
return null;
|
|
}
|
|
|
|
const normalized = path.replace(/\\/g, '/').trim();
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
|
|
const segments = normalized.split('/').filter(Boolean);
|
|
if (segments.length === 0) {
|
|
return normalized;
|
|
}
|
|
|
|
return segments[segments.length - 1] || null;
|
|
};
|
|
|
|
const getTabLabel = (tab: { mode: 'diff' | 'file' | 'context' | 'plan' | 'chat'; label: string | null; targetPath: string | null }): string => {
|
|
if (tab.label) {
|
|
return tab.label;
|
|
}
|
|
|
|
if (tab.mode === 'file') {
|
|
return getFileNameFromPath(tab.targetPath) || 'Files';
|
|
}
|
|
|
|
return getModeLabel(tab.mode);
|
|
};
|
|
|
|
const getTabIcon = (tab: { mode: 'diff' | 'file' | 'context' | 'plan' | 'chat'; targetPath: string | null }): React.ReactNode | undefined => {
|
|
if (tab.mode === 'file') {
|
|
return tab.targetPath
|
|
? <FileTypeIcon filePath={tab.targetPath} className="h-3.5 w-3.5" />
|
|
: undefined;
|
|
}
|
|
|
|
if (tab.mode === 'diff') {
|
|
return <RiArrowLeftRightLine className="h-3.5 w-3.5" />;
|
|
}
|
|
|
|
if (tab.mode === 'plan') {
|
|
return <RiFileTextLine className="h-3.5 w-3.5" />;
|
|
}
|
|
|
|
if (tab.mode === 'context') {
|
|
return <RiDonutChartFill className="h-3.5 w-3.5" />;
|
|
}
|
|
|
|
if (tab.mode === 'chat') {
|
|
return <RiChat4Line className="h-3.5 w-3.5" />;
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
const getSessionIDFromDedupeKey = (dedupeKey: string | undefined): string | null => {
|
|
if (!dedupeKey || !dedupeKey.startsWith('session:')) {
|
|
return null;
|
|
}
|
|
|
|
const sessionID = dedupeKey.slice('session:'.length).trim();
|
|
return sessionID || null;
|
|
};
|
|
|
|
const buildEmbeddedSessionChatURL = (sessionID: string, directory: string | null): string => {
|
|
if (typeof window === 'undefined') {
|
|
return '';
|
|
}
|
|
|
|
const url = new URL(window.location.pathname, window.location.origin);
|
|
url.searchParams.set('ocPanel', 'session-chat');
|
|
url.searchParams.set('sessionId', sessionID);
|
|
if (directory && directory.trim().length > 0) {
|
|
url.searchParams.set('directory', directory);
|
|
} else {
|
|
url.searchParams.delete('directory');
|
|
}
|
|
|
|
url.hash = '';
|
|
return url.toString();
|
|
};
|
|
|
|
const truncateTabLabel = (value: string, maxChars: number): string => {
|
|
if (value.length <= maxChars) {
|
|
return value;
|
|
}
|
|
|
|
return `${value.slice(0, maxChars - 3)}...`;
|
|
};
|
|
|
|
export const ContextPanel: React.FC = () => {
|
|
const effectiveDirectory = useEffectiveDirectory() ?? '';
|
|
const directoryKey = React.useMemo(() => normalizeDirectoryKey(effectiveDirectory), [effectiveDirectory]);
|
|
|
|
const panelState = useUIStore((state) => (directoryKey ? state.contextPanelByDirectory[directoryKey] : undefined));
|
|
const closeContextPanel = useUIStore((state) => state.closeContextPanel);
|
|
const closeContextPanelTab = useUIStore((state) => state.closeContextPanelTab);
|
|
const toggleContextPanelExpanded = useUIStore((state) => state.toggleContextPanelExpanded);
|
|
const setContextPanelWidth = useUIStore((state) => state.setContextPanelWidth);
|
|
const setActiveContextPanelTab = useUIStore((state) => state.setActiveContextPanelTab);
|
|
const reorderContextPanelTabs = useUIStore((state) => state.reorderContextPanelTabs);
|
|
const setPendingDiffFile = useUIStore((state) => state.setPendingDiffFile);
|
|
const setSelectedFilePath = useFilesViewTabsStore((state) => state.setSelectedPath);
|
|
const { themeMode, lightThemeId, darkThemeId, currentTheme } = useThemeSystem();
|
|
|
|
const tabs = React.useMemo(() => panelState?.tabs ?? [], [panelState?.tabs]);
|
|
const activeTab = tabs.find((tab) => tab.id === panelState?.activeTabId) ?? tabs[tabs.length - 1] ?? null;
|
|
const isOpen = Boolean(panelState?.isOpen && activeTab);
|
|
const isExpanded = Boolean(isOpen && panelState?.expanded);
|
|
const width = clampWidth(panelState?.width ?? CONTEXT_PANEL_DEFAULT_WIDTH);
|
|
|
|
const [isResizing, setIsResizing] = React.useState(false);
|
|
const startXRef = React.useRef(0);
|
|
const startWidthRef = React.useRef(width);
|
|
const resizingWidthRef = React.useRef<number | null>(null);
|
|
const activeResizePointerIDRef = React.useRef<number | null>(null);
|
|
const panelRef = React.useRef<HTMLElement | null>(null);
|
|
const chatFrameRefs = React.useRef<Map<string, HTMLIFrameElement>>(new Map());
|
|
const wasOpenRef = React.useRef(false);
|
|
|
|
React.useEffect(() => {
|
|
if (!isOpen || wasOpenRef.current) {
|
|
wasOpenRef.current = isOpen;
|
|
return;
|
|
}
|
|
|
|
const frame = window.requestAnimationFrame(() => {
|
|
panelRef.current?.focus({ preventScroll: true });
|
|
});
|
|
|
|
wasOpenRef.current = true;
|
|
return () => window.cancelAnimationFrame(frame);
|
|
}, [isOpen]);
|
|
|
|
const applyLiveWidth = React.useCallback((nextWidth: number) => {
|
|
const panel = panelRef.current;
|
|
if (!panel) {
|
|
return;
|
|
}
|
|
|
|
panel.style.setProperty('--oc-context-panel-width', `${nextWidth}px`);
|
|
}, []);
|
|
|
|
const handleResizeStart = React.useCallback((event: React.PointerEvent) => {
|
|
if (!isOpen || isExpanded || !directoryKey) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
} catch {
|
|
// ignore; fallback listeners still handle drag
|
|
}
|
|
|
|
activeResizePointerIDRef.current = event.pointerId;
|
|
setIsResizing(true);
|
|
startXRef.current = event.clientX;
|
|
startWidthRef.current = width;
|
|
resizingWidthRef.current = width;
|
|
applyLiveWidth(width);
|
|
event.preventDefault();
|
|
}, [applyLiveWidth, directoryKey, isExpanded, isOpen, width]);
|
|
|
|
const handleResizeMove = React.useCallback((event: React.PointerEvent) => {
|
|
if (!isResizing || activeResizePointerIDRef.current !== event.pointerId) {
|
|
return;
|
|
}
|
|
|
|
const delta = startXRef.current - event.clientX;
|
|
const nextWidth = clampWidth(startWidthRef.current + delta);
|
|
if (resizingWidthRef.current === nextWidth) {
|
|
return;
|
|
}
|
|
|
|
resizingWidthRef.current = nextWidth;
|
|
applyLiveWidth(nextWidth);
|
|
}, [applyLiveWidth, isResizing]);
|
|
|
|
const handleResizeEnd = React.useCallback((event: React.PointerEvent) => {
|
|
if (activeResizePointerIDRef.current !== event.pointerId || !directoryKey) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
const finalWidth = resizingWidthRef.current ?? width;
|
|
setIsResizing(false);
|
|
activeResizePointerIDRef.current = null;
|
|
resizingWidthRef.current = null;
|
|
setContextPanelWidth(directoryKey, finalWidth);
|
|
}, [directoryKey, setContextPanelWidth, width]);
|
|
|
|
React.useEffect(() => {
|
|
if (!isResizing) {
|
|
resizingWidthRef.current = null;
|
|
}
|
|
}, [isResizing]);
|
|
|
|
const handleClose = React.useCallback(() => {
|
|
if (!directoryKey) {
|
|
return;
|
|
}
|
|
closeContextPanel(directoryKey);
|
|
}, [closeContextPanel, directoryKey]);
|
|
|
|
const handleToggleExpanded = React.useCallback(() => {
|
|
if (!directoryKey) {
|
|
return;
|
|
}
|
|
toggleContextPanelExpanded(directoryKey);
|
|
}, [directoryKey, toggleContextPanelExpanded]);
|
|
|
|
const handlePanelKeyDownCapture = React.useCallback((event: React.KeyboardEvent<HTMLElement>) => {
|
|
if (event.key !== 'Escape') {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
handleClose();
|
|
}, [handleClose]);
|
|
|
|
React.useEffect(() => {
|
|
if (!directoryKey || !activeTab) {
|
|
return;
|
|
}
|
|
|
|
if (activeTab.mode === 'file' && activeTab.targetPath) {
|
|
setSelectedFilePath(directoryKey, activeTab.targetPath);
|
|
return;
|
|
}
|
|
|
|
if (activeTab.mode === 'diff' && activeTab.targetPath) {
|
|
setPendingDiffFile(activeTab.targetPath);
|
|
}
|
|
}, [activeTab, directoryKey, setPendingDiffFile, setSelectedFilePath]);
|
|
|
|
const activeChatTabID = activeTab?.mode === 'chat' ? activeTab.id : null;
|
|
|
|
const postThemeSyncToEmbeddedChat = React.useCallback(() => {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
themeMode,
|
|
lightThemeId,
|
|
darkThemeId,
|
|
currentTheme,
|
|
};
|
|
|
|
for (const frame of chatFrameRefs.current.values()) {
|
|
const frameWindow = frame.contentWindow;
|
|
if (!frameWindow) {
|
|
continue;
|
|
}
|
|
|
|
const directThemeSync = (frameWindow as unknown as {
|
|
__openchamberApplyThemeSync?: (themePayload: typeof payload) => void;
|
|
}).__openchamberApplyThemeSync;
|
|
|
|
if (typeof directThemeSync === 'function') {
|
|
try {
|
|
directThemeSync(payload);
|
|
continue;
|
|
} catch {
|
|
// fallback to postMessage below
|
|
}
|
|
}
|
|
|
|
frameWindow.postMessage(
|
|
{
|
|
type: 'openchamber:theme-sync',
|
|
payload,
|
|
},
|
|
window.location.origin,
|
|
);
|
|
}
|
|
}, [currentTheme, darkThemeId, lightThemeId, themeMode]);
|
|
|
|
const postEmbeddedVisibilityToChats = React.useCallback(() => {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
for (const [tabID, frame] of chatFrameRefs.current.entries()) {
|
|
const frameWindow = frame.contentWindow;
|
|
if (!frameWindow) {
|
|
continue;
|
|
}
|
|
|
|
const payload = { visible: activeChatTabID === tabID };
|
|
const directVisibilitySync = (frameWindow as unknown as {
|
|
__openchamberSetEmbeddedVisibility?: (visibilityPayload: typeof payload) => void;
|
|
}).__openchamberSetEmbeddedVisibility;
|
|
|
|
if (typeof directVisibilitySync === 'function') {
|
|
try {
|
|
directVisibilitySync(payload);
|
|
continue;
|
|
} catch {
|
|
// fallback to postMessage below
|
|
}
|
|
}
|
|
|
|
frameWindow.postMessage(
|
|
{
|
|
type: 'openchamber:embedded-visibility',
|
|
payload,
|
|
},
|
|
window.location.origin,
|
|
);
|
|
}
|
|
}, [activeChatTabID]);
|
|
|
|
React.useLayoutEffect(() => {
|
|
const hasAnyChatTab = tabs.some((tab) => tab.mode === 'chat');
|
|
if (!hasAnyChatTab) {
|
|
return;
|
|
}
|
|
|
|
postThemeSyncToEmbeddedChat();
|
|
postEmbeddedVisibilityToChats();
|
|
}, [darkThemeId, lightThemeId, postEmbeddedVisibilityToChats, postThemeSyncToEmbeddedChat, tabs, themeMode]);
|
|
|
|
const tabItems = React.useMemo(() => tabs.map((tab) => {
|
|
const rawLabel = getTabLabel(tab);
|
|
const label = truncateTabLabel(rawLabel, CONTEXT_TAB_LABEL_MAX_CHARS);
|
|
const tabPathLabel = getRelativePathLabel(tab.targetPath, effectiveDirectory);
|
|
return {
|
|
id: tab.id,
|
|
label,
|
|
icon: getTabIcon(tab),
|
|
title: tabPathLabel ? `${rawLabel}: ${tabPathLabel}` : rawLabel,
|
|
closeLabel: `Close ${label} tab`,
|
|
};
|
|
}), [effectiveDirectory, tabs]);
|
|
|
|
const activeNonChatContent = activeTab?.mode === 'diff'
|
|
? <DiffView hideStackedFileSidebar stackedDefaultCollapsedAll hideFileSelector pinSelectedFileHeaderToTopOnNavigate showOpenInEditorAction />
|
|
: activeTab?.mode === 'context'
|
|
? <ContextPanelContent />
|
|
: activeTab?.mode === 'plan'
|
|
? <PlanView />
|
|
: null;
|
|
|
|
const chatTabs = React.useMemo(
|
|
() => tabs.filter((tab) => tab.mode === 'chat'),
|
|
[tabs],
|
|
);
|
|
const hasFileTabs = React.useMemo(
|
|
() => tabs.some((tab) => tab.mode === 'file'),
|
|
[tabs],
|
|
);
|
|
|
|
const isFileTabActive = activeTab?.mode === 'file';
|
|
|
|
const header = (
|
|
<header className="flex h-10 items-stretch border-b border-transparent">
|
|
<SortableTabsStrip
|
|
items={tabItems}
|
|
activeId={activeTab?.id ?? null}
|
|
onSelect={(tabID) => {
|
|
if (!directoryKey) {
|
|
return;
|
|
}
|
|
setActiveContextPanelTab(directoryKey, tabID);
|
|
}}
|
|
onClose={(tabID) => {
|
|
if (!directoryKey) {
|
|
return;
|
|
}
|
|
closeContextPanelTab(directoryKey, tabID);
|
|
}}
|
|
onReorder={(activeTabID, overTabID) => {
|
|
if (!directoryKey) {
|
|
return;
|
|
}
|
|
reorderContextPanelTabs(directoryKey, activeTabID, overTabID);
|
|
}}
|
|
layoutMode="scrollable"
|
|
variant="active-pill"
|
|
activePillLowercase={false}
|
|
activePillInsetClassName="gap-0.5 pt-0.5 pb-1.5"
|
|
/>
|
|
<div className="flex items-end gap-1 px-1.5 pb-1.5">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleToggleExpanded}
|
|
className="h-7 w-7 p-0"
|
|
title={isExpanded ? 'Collapse panel' : 'Expand panel'}
|
|
aria-label={isExpanded ? 'Collapse panel' : 'Expand panel'}
|
|
>
|
|
{isExpanded ? <RiFullscreenExitLine className="h-3.5 w-3.5" /> : <RiFullscreenLine className="h-3.5 w-3.5" />}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleClose}
|
|
className="h-7 w-7 p-0"
|
|
title="Close panel"
|
|
aria-label="Close panel"
|
|
>
|
|
<RiCloseLine className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</header>
|
|
);
|
|
|
|
if (!isOpen) {
|
|
return null;
|
|
}
|
|
|
|
const panelStyle: React.CSSProperties = isExpanded
|
|
? {
|
|
['--oc-context-panel-width' as string]: '100%',
|
|
width: '100%',
|
|
minWidth: '100%',
|
|
maxWidth: '100%',
|
|
}
|
|
: {
|
|
width: 'var(--oc-context-panel-width)',
|
|
minWidth: 'var(--oc-context-panel-width)',
|
|
maxWidth: 'var(--oc-context-panel-width)',
|
|
['--oc-context-panel-width' as string]: `${isResizing ? (resizingWidthRef.current ?? width) : width}px`,
|
|
};
|
|
|
|
return (
|
|
<aside
|
|
ref={panelRef}
|
|
data-context-panel="true"
|
|
tabIndex={-1}
|
|
className={cn(
|
|
'flex min-h-0 flex-col overflow-hidden bg-background',
|
|
!isExpanded && 'border-l border-border/40',
|
|
isExpanded
|
|
? 'absolute inset-0 z-20 min-w-0'
|
|
: 'relative h-full flex-shrink-0',
|
|
isResizing ? 'transition-none' : 'transition-[width] duration-200 ease-in-out'
|
|
)}
|
|
onKeyDownCapture={handlePanelKeyDownCapture}
|
|
style={panelStyle}
|
|
>
|
|
{!isExpanded && (
|
|
<div
|
|
className={cn(
|
|
'absolute left-0 top-0 z-20 h-full w-[3px] cursor-col-resize transition-colors hover:bg-[var(--interactive-border)]/80',
|
|
isResizing && 'bg-[var(--interactive-border)]'
|
|
)}
|
|
onPointerDown={handleResizeStart}
|
|
onPointerMove={handleResizeMove}
|
|
onPointerUp={handleResizeEnd}
|
|
onPointerCancel={handleResizeEnd}
|
|
role="separator"
|
|
aria-orientation="vertical"
|
|
aria-label="Resize context panel"
|
|
/>
|
|
)}
|
|
{header}
|
|
<div className={cn('relative min-h-0 flex-1 overflow-hidden', isResizing && 'pointer-events-none')}>
|
|
{hasFileTabs ? (
|
|
<div className={cn('absolute inset-0', isFileTabActive ? 'block' : 'hidden')}>
|
|
<FilesView mode="editor-only" />
|
|
</div>
|
|
) : null}
|
|
{chatTabs.map((tab) => {
|
|
const sessionID = getSessionIDFromDedupeKey(tab.dedupeKey);
|
|
if (!sessionID) {
|
|
return null;
|
|
}
|
|
|
|
const src = buildEmbeddedSessionChatURL(sessionID, directoryKey || null);
|
|
if (!src) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<iframe
|
|
key={tab.id}
|
|
ref={(node) => {
|
|
if (!node) {
|
|
chatFrameRefs.current.delete(tab.id);
|
|
return;
|
|
}
|
|
chatFrameRefs.current.set(tab.id, node);
|
|
}}
|
|
src={src}
|
|
title={`Session chat ${sessionID}`}
|
|
className={cn(
|
|
'absolute inset-0 h-full w-full border-0 bg-background',
|
|
activeChatTabID === tab.id ? 'block' : 'hidden'
|
|
)}
|
|
onLoad={() => {
|
|
postThemeSyncToEmbeddedChat();
|
|
postEmbeddedVisibilityToChats();
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
{activeTab?.mode !== 'chat' && !isFileTabActive ? activeNonChatContent : null}
|
|
</div>
|
|
</aside>
|
|
);
|
|
};
|