/** * SidebarContainer — Shared sidebar shell * * Houses the Table of Contents, Version Browser, File Browser, and Archive Browser views. * Tab bar at top switches between them. */ import React from "react"; import type { SidebarTab } from "../../hooks/useSidebar"; import type { Block, Annotation } from "../../types"; import type { VersionInfo, VersionEntry } from "../../hooks/usePlanDiff"; import type { UseFileBrowserReturn } from "../../hooks/useFileBrowser"; import { TableOfContents } from "../TableOfContents"; import { VersionBrowser } from "./VersionBrowser"; import { FileBrowser, type FileEditStatus } from "./FileBrowser"; import { ArchiveBrowser, type ArchivedPlan } from "./ArchiveBrowser"; import { MessagesBrowser, type PickerMessage } from "./MessagesBrowser"; import { MessagesIcon } from "../icons/MessagesIcon"; import { OverlayScrollArea } from "../OverlayScrollArea"; import { ReviewAgentsIcon } from "../ReviewAgentsIcon"; interface SidebarContainerProps { /** Desktop preserves the incumbent sticky rail. Compact Plan uses the same * content as a safe, visible-viewport-bounded foreground surface. */ presentation?: "desktop" | "overlay"; activeTab: SidebarTab; onTabChange: (tab: SidebarTab) => void; onClose: () => void; width: number | string; showAgentTerminalButton?: boolean; isAgentTerminalOpen?: boolean; isAgentTerminalRunning?: boolean; onToggleAgentTerminal?: () => void; // TOC props showContentsTab?: boolean; blocks: Block[]; annotations: Annotation[]; activeSection: string | null; onTocNavigate: (blockId: string) => void; linkedDocFilepath?: string | null; onLinkedDocBack?: () => void; backLabel?: string; // File Browser props showFilesTab?: boolean; fileAnnotationCounts?: Map; highlightedFiles?: Set; fileEditStatuses?: Map; fileBrowser?: UseFileBrowserReturn; onFilesSelectFile?: (absolutePath: string, dirPath: string) => void; onFilesFetchAll?: () => void; onFilesRetryVaultDir?: (vaultPath: string) => void; /** Compact-only file activation feedback; desktop does not pass this. */ pendingFileLabel?: string | null; // Version Browser props showVersionsTab?: boolean; versionInfo: VersionInfo | null; versions: VersionEntry[]; selectedBaseVersion: number | null; onSelectBaseVersion: (version: number) => void; isPlanDiffActive: boolean; hasPreviousVersion: boolean; onActivatePlanDiff: () => void; isLoadingVersions: boolean; isSelectingVersion: boolean; fetchingVersion: number | null; onFetchVersions: () => void; // Annotation indicators hasFileAnnotations?: boolean; // Archive Browser props showArchiveTab?: boolean; archivePlans: ArchivedPlan[]; selectedArchiveFile: string | null; onArchiveSelect: (filename: string) => void; isLoadingArchive: boolean; showMessagesTab?: boolean; messages?: PickerMessage[]; selectedMessageId?: string | null; onSelectMessage?: (messageId: string) => void; messageAnnotationCounts?: Map; } export const SidebarContainer: React.FC = ({ presentation = "desktop", activeTab, onTabChange, onClose, width, showAgentTerminalButton, isAgentTerminalOpen, isAgentTerminalRunning, onToggleAgentTerminal, showContentsTab = true, blocks, annotations, activeSection, onTocNavigate, linkedDocFilepath, onLinkedDocBack, backLabel, showFilesTab, fileAnnotationCounts, highlightedFiles, fileEditStatuses, fileBrowser, onFilesSelectFile, onFilesFetchAll, onFilesRetryVaultDir, pendingFileLabel, showVersionsTab, versionInfo, versions, selectedBaseVersion, onSelectBaseVersion, isPlanDiffActive, hasPreviousVersion, onActivatePlanDiff, isLoadingVersions, isSelectingVersion, fetchingVersion, onFetchVersions, hasFileAnnotations, showArchiveTab, archivePlans, selectedArchiveFile, onArchiveSelect, isLoadingArchive, showMessagesTab, messages, selectedMessageId, onSelectMessage, messageAnnotationCounts, }) => { const compact = presentation === "overlay"; const closeButtonRef = React.useRef(null); React.useEffect(() => { if (compact) closeButtonRef.current?.focus({ preventScroll: true }); }, [compact]); const handleCompactKeyDown = (event: React.KeyboardEvent) => { if (!compact) return; if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); onClose(); return; } if (event.key !== "Tab") return; const focusable = Array.from( event.currentTarget.querySelectorAll( 'button:not(:disabled), input:not(:disabled), [href], [tabindex]:not([tabindex="-1"])', ), ); if (focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); } }; return ( ); }; const TabButton: React.FC<{ active: boolean; onClick: () => void; icon: React.ReactNode; label: string; badge?: boolean; touch?: boolean; }> = ({ active, onClick, icon, label, badge, touch = false }) => ( ); const ActionButton: React.FC<{ active: boolean; running?: boolean; onClick: () => void; icon: React.ReactNode; label: string; }> = ({ active, running, onClick, icon, label }) => ( );