diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 55dcc09..cdc6516 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -9,6 +9,7 @@ import { Session } from '@/pages/Session';
import { Toaster } from '@/components/ui/sonner';
import { useUserEvents } from '@/hooks/useUserEvents';
import { SidebarDrawerProvider, useSidebarDrawer } from '@/hooks/useSidebarDrawer';
+import { RightRailDrawerProvider, useRightRailDrawer } from '@/hooks/useRightRailDrawer';
import { useViewport } from '@/hooks/useViewport';
function SessionRightRail() {
@@ -26,13 +27,11 @@ function RightRailForSession({ sessionId }: { sessionId: string }) {
.catch((err) => console.warn('RightRail: failed to fetch session', err));
}, [sessionId]);
if (!projectId) return null;
- // Hidden entirely below md breakpoint; mobile users get the file browser
- // via the FileBrowserPane infrastructure if/when it lands in workspace panes.
- return (
-
-
-
- );
+ // v1.6.2: rendered on all viewports. On mobile, RightRail itself renders as
+ // a right-side drawer toggled by the header's FolderTree button (via
+ // useRightRailDrawer). On desktop, it renders inline as before with its
+ // own internal open/close state.
+ return ;
}
function MobileBackdrop() {
@@ -48,6 +47,19 @@ function MobileBackdrop() {
);
}
+function MobileRightRailBackdrop() {
+ const { open, setOpen } = useRightRailDrawer();
+ const { isMobile } = useViewport();
+ if (!isMobile || !open) return null;
+ return (
+ setOpen(false)}
+ aria-hidden="true"
+ />
+ );
+}
+
function AppShell() {
useUserEvents();
return (
@@ -61,6 +73,7 @@ function AppShell() {
} />
+
} />
@@ -73,7 +86,9 @@ export default function App() {
return (
-
+
+
+
);
diff --git a/apps/web/src/components/RightRail.tsx b/apps/web/src/components/RightRail.tsx
index ecdf026..12f9629 100644
--- a/apps/web/src/components/RightRail.tsx
+++ b/apps/web/src/components/RightRail.tsx
@@ -4,8 +4,11 @@ import { api } from '@/api/client';
import type { FileEntry } from '@/api/types';
import { inferLanguage } from '@/lib/attachments';
import { sessionEvents } from '@/hooks/sessionEvents';
+import { useRightRailDrawer } from '@/hooks/useRightRailDrawer';
+import { useViewport } from '@/hooks/useViewport';
import { FileViewerOverlay } from '@/components/FileViewerOverlay';
import { Input } from '@/components/ui/input';
+import { cn } from '@/lib/utils';
interface Props {
projectId: string;
@@ -25,6 +28,8 @@ function joinPath(parent: string, name: string): string {
}
export function RightRail({ projectId }: Props) {
+ const { isMobile } = useViewport();
+ const { open: drawerOpen, setOpen: setDrawerOpen } = useRightRailDrawer();
const [open, setOpen] = useState(() => {
try { return localStorage.getItem(`${STORAGE_KEY}.open`) !== 'false'; } catch { return true; }
});
@@ -34,6 +39,19 @@ export function RightRail({ projectId }: Props) {
const [fullFileList, setFullFileList] = useState
(null);
const [viewerFile, setViewerFile] = useState<{ path: string; content: string } | null>(null);
+ // Combined open state: on mobile use the global drawer state (toggled by
+ // the Session header's FolderTree button); on desktop use the persistent
+ // internal state.
+ const isOpen = isMobile ? drawerOpen : open;
+ const closeRail = useCallback(() => {
+ if (isMobile) setDrawerOpen(false);
+ else setOpen(false);
+ }, [isMobile, setDrawerOpen]);
+ const openRail = useCallback(() => {
+ if (isMobile) setDrawerOpen(true);
+ else setOpen(true);
+ }, [isMobile, setDrawerOpen]);
+
useEffect(() => {
// best-effort; ignore failure because localStorage may be unavailable (quota, private mode)
try { localStorage.setItem(`${STORAGE_KEY}.open`, String(open)); } catch {}
@@ -56,9 +74,9 @@ export function RightRail({ projectId }: Props) {
}, [projectId]);
useEffect(() => {
- if (!open) return;
+ if (!isOpen) return;
if (!cache.has('')) void loadDir('');
- }, [open, cache, loadDir]);
+ }, [isOpen, cache, loadDir]);
function toggleDir(dirPath: string) {
setExpandedDirs((prev) => {
@@ -108,12 +126,14 @@ export function RightRail({ projectId }: Props) {
useEffect(() => {
return sessionEvents.subscribe((event) => {
if (event.type !== 'open_file_in_browser') return;
- if (!open) setOpen(true);
+ if (!isOpen) openRail();
void openFile(event.path);
});
- }, [open, projectId]);
+ }, [isOpen, openRail, projectId]);
- if (!open) {
+ // Desktop closed state: render the floating chevron handle. Mobile never
+ // shows the handle — the toggle lives in the Session header on mobile.
+ if (!isMobile && !open) {
return (
+
+ {/* File browser toggle — mobile only */}
+ {isMobile && (
+
+ )}
{id && session && (