+
{hasThread && (
)}
+ {showExpandButton && (
+
+
+
+
+ Expand panel
+
+ )}
);
diff --git a/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
new file mode 100644
index 000000000..98f14b247
--- /dev/null
+++ b/surfsense_web/components/layout/ui/right-panel/RightPanel.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import { useAtom, useAtomValue, useSetAtom } from "jotai";
+import { PanelRight, PanelRightClose } from "lucide-react";
+import { useEffect } from "react";
+import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
+import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
+import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right-panel.atom";
+import { ReportPanelContent } from "@/components/report-panel/report-panel";
+import { Button } from "@/components/ui/button";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { DocumentsSidebar } from "../sidebar";
+
+interface RightPanelProps {
+ documentsPanel?: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ };
+}
+
+function CollapseButton({ onClick }: { onClick: () => void }) {
+ return (
+
+
+
+
+ Collapse panel
+
+ );
+}
+
+/**
+ * Absolutely positioned expand button — renders at top-right of the main
+ * container so it occupies the same screen position as the collapse button
+ * inside the Documents header.
+ */
+export function RightPanelExpandButton() {
+ const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
+ const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
+ const reportState = useAtomValue(reportPanelAtom);
+ const reportOpen = reportState.isOpen && !!reportState.reportId;
+ const hasContent = documentsOpen || reportOpen;
+
+ if (!collapsed || !hasContent) return null;
+
+ return (
+
+
+
+
+
+ Expand panel
+
+
+ );
+}
+
+export function RightPanel({ documentsPanel }: RightPanelProps) {
+ const [activeTab] = useAtom(rightPanelTabAtom);
+ const reportState = useAtomValue(reportPanelAtom);
+ const closeReport = useSetAtom(closeReportPanelAtom);
+ const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
+
+ const documentsOpen = documentsPanel?.open ?? false;
+ const reportOpen = reportState.isOpen && !!reportState.reportId;
+
+ // Close report on Escape key (works on Windows, macOS, and Linux)
+ // Must be called before early returns to satisfy Rules of Hooks.
+ useEffect(() => {
+ if (!reportOpen) return;
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ closeReport();
+ }
+ };
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [reportOpen, closeReport]);
+
+ if (!documentsOpen && !reportOpen) return null;
+ if (collapsed) return null;
+
+ const effectiveTab =
+ activeTab === "report" && !reportOpen
+ ? "sources"
+ : activeTab === "sources" && !documentsOpen
+ ? "report"
+ : activeTab;
+
+ const collapseButton =
setCollapsed(true)} />;
+
+ const panelWidth = effectiveTab === "sources" ? "w-[420px]" : "w-[640px]";
+
+ return (
+
+ );
+}
diff --git a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx
index 5bfbcfee0..8ac897fd4 100644
--- a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx
+++ b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx
@@ -10,6 +10,7 @@ import { useSidebarResize } from "../../hooks/useSidebarResize";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
import { Header } from "../header";
import { IconRail } from "../icon-rail";
+import { RightPanel } from "../right-panel/RightPanel";
import {
AllPrivateChatsSidebar,
AllSharedChatsSidebar,
@@ -275,46 +276,36 @@ export function LayoutShell({
{/* Main container with sidebar and content - relative for inbox positioning */}
-
-
- {/* Docked Documents Sidebar - renders as flex sibling between sidebar and content */}
- {documentsPanel?.isDocked && (
-
- )}
+
@@ -324,6 +315,16 @@ export function LayoutShell({
+ {/* Right panel — tabbed Sources/Report (desktop only) */}
+ {documentsPanel && (
+
+ )}
+
{/* Inbox Sidebar - slide-out panel */}
{inbox && (
)}
- {/* Documents Sidebar - floating slide-out panel (non-docked mode) */}
- {documentsPanel && !documentsPanel.isDocked && (
-
- )}
-
{/* Announcements Sidebar */}
{announcementsPanel && (
void;
isDocked?: boolean;
onDockedChange?: (docked: boolean) => void;
+ /** When true, renders content without any wrapper — parent provides the container */
+ embedded?: boolean;
+ /** Optional action element rendered in the header row (e.g. collapse button) */
+ headerAction?: React.ReactNode;
}
export function DocumentsSidebar({
@@ -45,6 +53,8 @@ export function DocumentsSidebar({
onOpenChange,
isDocked = false,
onDockedChange,
+ embedded = false,
+ headerAction,
}: DocumentsSidebarProps) {
const t = useTranslations("documents");
const tSidebar = useTranslations("sidebar");
@@ -168,8 +178,8 @@ export function DocumentsSidebar({
const documentsContent = (
<>
-
-
+
+
{isMobile && (
{/* Connected tools strip */}
-
+