diff --git a/frontend/src/core/components/fileEditor/AddFileCard.tsx b/frontend/src/core/components/fileEditor/AddFileCard.tsx
index cb2ee56527..5fc62680b3 100644
--- a/frontend/src/core/components/fileEditor/AddFileCard.tsx
+++ b/frontend/src/core/components/fileEditor/AddFileCard.tsx
@@ -68,7 +68,7 @@ const AddFileCard = ({
/>
{activeStirlingFileStubs.length === 0 ? (
-
-
- 📁
-
- No files loaded
-
- Upload PDF files, ZIP archives, or load from storage to get
- started
-
-
+
) : (
0 && h > 0) {
- return isLandscape ? `${h} / ${w}` : `${w} / ${h}`;
+ // width/height are effective (post-rotation) dims from PDFium, so use
+ // them directly — no swapping needed.
+ return `${w} / ${h}`;
}
return isLandscape ? "11 / 8.5" : "8.5 / 11";
})();
diff --git a/frontend/src/core/components/layout/Workbench.module.css b/frontend/src/core/components/layout/Workbench.module.css
index 73d2e402f4..51138d2ac3 100644
--- a/frontend/src/core/components/layout/Workbench.module.css
+++ b/frontend/src/core/components/layout/Workbench.module.css
@@ -1,3 +1,24 @@
+/* WorkbenchBar slide-in/out animation using CSS grid trick */
+.workbenchBarWrapper {
+ display: grid;
+ grid-template-rows: 1fr;
+ transition: grid-template-rows 280ms ease;
+}
+
+.workbenchBarWrapper[data-hidden="true"] {
+ grid-template-rows: 0fr;
+}
+
+.workbenchBarWrapper[data-no-transition="true"] {
+ transition: none;
+}
+
+/* Direct child must have min-height: 0 so the row can collapse below content size */
+.workbenchBarInner {
+ min-height: 0;
+ overflow: hidden;
+}
+
.workbenchScrollable {
overflow-y: auto !important;
overflow-x: hidden !important;
diff --git a/frontend/src/core/components/layout/Workbench.tsx b/frontend/src/core/components/layout/Workbench.tsx
index 0385a7536c..69d6f9f410 100644
--- a/frontend/src/core/components/layout/Workbench.tsx
+++ b/frontend/src/core/components/layout/Workbench.tsx
@@ -1,3 +1,4 @@
+import { useEffect, useState } from "react";
import { Box } from "@mantine/core";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
@@ -19,7 +20,6 @@ import Viewer from "@app/components/viewer/Viewer";
import Footer from "@app/components/shared/Footer";
import DismissAllErrorsButton from "@app/components/shared/DismissAllErrorsButton";
import LandingPage from "@app/components/shared/LandingPage";
-
// No props needed - component uses contexts directly
export default function Workbench() {
const { isRainbowMode } = useRainbowThemeContext();
@@ -50,6 +50,15 @@ export default function Workbench() {
const { toolRegistry } = useToolWorkflow();
const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null;
const { addFiles } = useFileHandler();
+ const hasFiles = activeFiles.length > 0;
+
+ // Enable bar transitions after first paint so the initial hidden state shows
+ // without animating (landing page on load shouldn't animate the bar up).
+ const [barTransitionEnabled, setBarTransitionEnabled] = useState(false);
+ useEffect(() => {
+ const raf = requestAnimationFrame(() => setBarTransitionEnabled(true));
+ return () => cancelAnimationFrame(raf);
+ }, []);
const handlePreviewClose = () => {
setPreviewFile(null);
@@ -82,7 +91,7 @@ export default function Workbench() {
}
}
- if (activeFiles.length === 0) {
+ if (activeFiles.length === 0) {
return
;
}
@@ -168,14 +177,22 @@ export default function Workbench() {
: { backgroundColor: "var(--bg-background)" }
}
>
- {/* Workbench Bar - replaces TopControls and includes RightRail action buttons */}
+ {/* Workbench Bar - animates in/out based on file presence */}
{!customWorkbenchViews.find((v) => v.workbenchId === currentView)
?.hideTopControls && (
-
0}
- />
+
)}
{/* Dismiss All Errors Button */}
diff --git a/frontend/src/core/components/pageEditor/PageEditor.tsx b/frontend/src/core/components/pageEditor/PageEditor.tsx
index 00e8e2ee05..8f31ca62e9 100644
--- a/frontend/src/core/components/pageEditor/PageEditor.tsx
+++ b/frontend/src/core/components/pageEditor/PageEditor.tsx
@@ -168,6 +168,16 @@ const PageEditor = ({ onFunctionsReady }: PageEditorProps) => {
}, [pageEditorFiles]);
const activeFilesSignature = selectors.getFilesSignature();
+ // Check if there are any PDF files in FileContext (bypasses fileOrder, which is
+ // populated asynchronously via effect). Used to avoid a one-render flash of
+ // "No PDF files loaded" when PageEditor first mounts after files are added.
+ const hasPdfFiles = useMemo(() => {
+ return state.files.ids.some((id) => {
+ const stub = selectors.getStirlingFileStub(id);
+ return stub?.name?.toLowerCase().endsWith(".pdf") ?? false;
+ });
+ }, [state.files.ids, selectors]);
+
// UI state
const globalProcessing = state.ui.isProcessing;
@@ -676,7 +686,7 @@ const PageEditor = ({ onFunctionsReady }: PageEditorProps) => {
{!initialDocument &&
!globalProcessing &&
- selectedFileIds.length === 0 && (
+ !hasPdfFiles && (
@@ -690,7 +700,7 @@ const PageEditor = ({ onFunctionsReady }: PageEditorProps) => {
)}
- {!initialDocument && globalProcessing && (
+ {!initialDocument && (globalProcessing || hasPdfFiles) && (
diff --git a/frontend/src/core/components/shared/FileSidebar.css b/frontend/src/core/components/shared/FileSidebar.css
index bf2f232846..144d316de4 100644
--- a/frontend/src/core/components/shared/FileSidebar.css
+++ b/frontend/src/core/components/shared/FileSidebar.css
@@ -82,6 +82,10 @@
flex-shrink: 0;
}
+.file-sidebar-search-close {
+ cursor: pointer;
+}
+
.file-sidebar-search-input {
flex: 1;
background: transparent;
diff --git a/frontend/src/core/components/shared/FileSidebar.tsx b/frontend/src/core/components/shared/FileSidebar.tsx
index 872571de21..821aba8cb5 100644
--- a/frontend/src/core/components/shared/FileSidebar.tsx
+++ b/frontend/src/core/components/shared/FileSidebar.tsx
@@ -66,7 +66,8 @@ const FileSidebar = forwardRef(
const { state } = useFileState();
const { actions: fileActions } = useFileActions();
const { actions: navActions } = useNavigationActions();
- const { workbench: currentWorkbench } = useNavigationState();
+ const { workbench: currentWorkbench, selectedTool } = useNavigationState();
+ const isMultiTool = currentWorkbench === "pageEditor" && selectedTool === "multiTool";
const { requestNavigation } = useNavigationGuard();
const { activeFileId, setActiveFileId } = useViewer();
const { addFiles } = useFileHandler();
@@ -158,9 +159,11 @@ const FileSidebar = forwardRef(
const files = await openGoogleDrivePicker({ multiple: true });
if (files.length > 0) {
await addFiles(files);
- navActions.setWorkbench(files.length === 1 ? "viewer" : "fileEditor");
+ if (!isMultiTool) {
+ navActions.setWorkbench(files.length === 1 ? "viewer" : "fileEditor");
+ }
}
- }, [isGoogleDriveEnabled, openGoogleDrivePicker, addFiles, navActions]);
+ }, [isGoogleDriveEnabled, openGoogleDrivePicker, addFiles, navActions, isMultiTool]);
// Toggle file in/out of workbench
const handleFileClick = useCallback(
@@ -193,21 +196,30 @@ const FileSidebar = forwardRef(
await fileActions.addStirlingFileStubs([stub]);
- if (workbenchCount === 0) {
- navActions.setWorkbench("viewer");
+ if (isMultiTool) {
+ fileActions.setSelectedFiles([
+ ...state.ui.selectedFileIds,
+ stub.id,
+ ]);
} else {
- navActions.setWorkbench("fileEditor");
+ if (workbenchCount === 0) {
+ navActions.setWorkbench("viewer");
+ } else {
+ navActions.setWorkbench("fileEditor");
+ }
}
}
},
[
allFileStubs,
state.files.ids,
+ state.ui.selectedFileIds,
fileActions,
navActions,
currentWorkbench,
activeFileId,
requestNavigation,
+ isMultiTool,
],
);
@@ -274,11 +286,13 @@ const FileSidebar = forwardRef(
const files = Array.from(e.target.files ?? []);
if (files.length > 0) {
await addFiles(files);
- navActions.setWorkbench(files.length === 1 ? "viewer" : "fileEditor");
+ if (!isMultiTool) {
+ navActions.setWorkbench(files.length === 1 ? "viewer" : "fileEditor");
+ }
}
e.target.value = "";
},
- [addFiles, navActions],
+ [addFiles, navActions, isMultiTool],
);
const shouldHideGoogleDrive =
diff --git a/frontend/src/core/components/shared/FileSidebarFileItem.css b/frontend/src/core/components/shared/FileSidebarFileItem.css
index 8de6de4881..3b6e748d80 100644
--- a/frontend/src/core/components/shared/FileSidebarFileItem.css
+++ b/frontend/src/core/components/shared/FileSidebarFileItem.css
@@ -207,6 +207,20 @@
display: block !important;
}
+/* ---- Date group headers ---- */
+.file-sidebar-date-group-header {
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--text-muted);
+ padding: 0 10px 2px 10px;
+ margin-top: 8px;
+ user-select: none;
+}
+
+.file-sidebar-date-group-header:first-child {
+ margin-top: 0;
+}
+
/* ---- Loading state ---- */
.file-sidebar-loading {
display: flex;
diff --git a/frontend/src/core/components/shared/FileSidebarFileItem.tsx b/frontend/src/core/components/shared/FileSidebarFileItem.tsx
index ec1e55cae7..53f49e0cee 100644
--- a/frontend/src/core/components/shared/FileSidebarFileItem.tsx
+++ b/frontend/src/core/components/shared/FileSidebarFileItem.tsx
@@ -56,6 +56,43 @@ export function getFileExtension(name: string): string {
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
}
+export type DateGroup =
+ | "today"
+ | "yesterday"
+ | "thisWeek"
+ | "thisMonth"
+ | "older";
+
+export const DATE_GROUP_ORDER: DateGroup[] = [
+ "today",
+ "yesterday",
+ "thisWeek",
+ "thisMonth",
+ "older",
+];
+
+export function getDateGroup(lastModified: number | undefined): DateGroup {
+ if (!lastModified) return "older";
+ const now = new Date();
+ const today = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ ).getTime();
+ const d = new Date(lastModified);
+ const fileDay = new Date(
+ d.getFullYear(),
+ d.getMonth(),
+ d.getDate(),
+ ).getTime();
+ const daysAgo = (today - fileDay) / 86400000;
+ if (daysAgo < 1) return "today";
+ if (daysAgo < 2) return "yesterday";
+ if (daysAgo < 7) return "thisWeek";
+ if (daysAgo < 30) return "thisMonth";
+ return "older";
+}
+
export function formatFileDate(lastModifiedTs: number): string {
const lastModified = lastModifiedTs ? new Date(lastModifiedTs) : new Date();
const now = new Date();
@@ -102,21 +139,23 @@ function FilePdfIcon({
);
}
@@ -200,7 +239,7 @@ export function FileItem({
const ext = getFileExtension(name);
const isPdf = ext === "pdf";
const dateLabel = lastModified ? formatFileDate(lastModified) : "";
- const typeLabel = ext ? ext.toUpperCase() : "File";
+ const typeLabel = isPdf ? "" : ext ? ext.toUpperCase() : "File";
const resolvedThumbnail = useLazyThumbnail(fileId, size ?? 0, thumbnailUrl);
@@ -245,7 +284,7 @@ export function FileItem({
{isPdf ? (
) : (
,
},
+ ...(selectedTool === "multiTool"
+ ? [
+ {
+ value: "pageEditor" as WorkbenchType,
+ label: t("workbenchBar.multiTool", "Multi-Tool"),
+ icon: (
+
+ ),
+ },
+ ]
+ : []),
+ ...customWorkbenchViews
+ .filter((v) => v.data != null)
+ .map((v) => ({
+ value: v.workbenchId,
+ label: v.label,
+ icon: v.icon ?? ,
+ })),
];
const barRef = useRef(null);
@@ -366,7 +391,7 @@ export default function WorkbenchBar({
)}
- {/* Right: Global buttons (share / delete / download / zoom + page nav) */}
+ {/* Right: Global buttons — export group left, close anchored right */}
{/* Print */}
{currentView === "viewer" &&
@@ -386,29 +411,6 @@ export default function WorkbenchBar({
t("rightRail.print", "Print PDF"),
)}
- {/* Close (context-aware: close all / close viewer file / close page editor) */}
- {renderWithTooltip(
-
-
- ,
- currentView === "fileEditor"
- ? t("rightRail.closeAll", "Close All")
- : t("rightRail.closePdf", "Close PDF"),
- )}
-
{/* Download */}
{renderWithTooltip(
,
t("rightRail.saveAs", "Save As"),
)}
+
+ {/* Separator: export group | close */}
+ {!isCustomView && }
+
+ {/* Close (context-aware: close all / close viewer file / close page editor) */}
+ {!isCustomView && renderWithTooltip(
+
+
+ ,
+ currentView === "fileEditor"
+ ? t("rightRail.closeAll", "Close All")
+ : t("rightRail.closePdf", "Close PDF"),
+ )}
);
diff --git a/frontend/src/core/components/tools/automate/AutomationRun.tsx b/frontend/src/core/components/tools/automate/AutomationRun.tsx
index d988f1a4a8..3e3a4c0756 100644
--- a/frontend/src/core/components/tools/automate/AutomationRun.tsx
+++ b/frontend/src/core/components/tools/automate/AutomationRun.tsx
@@ -11,7 +11,7 @@ import {
} from "@mantine/core";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import CheckIcon from "@mui/icons-material/Check";
-import { useFileSelection } from "@app/contexts/FileContext";
+import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles";
import { useToolRegistry } from "@app/contexts/ToolRegistryContext";
import { AutomationConfig, ExecutionStep } from "@app/types/automation";
import { EXECUTION_STATUS } from "@app/constants/automation";
@@ -29,7 +29,7 @@ export default function AutomationRun({
automateOperation,
}: AutomationRunProps) {
const { t } = useTranslation();
- const { selectedFiles } = useFileSelection();
+ const selectedFiles = useViewScopedFiles();
const { regularTools } = useToolRegistry();
const toolRegistry = regularTools;
const cleanup = useResourceCleanup();
diff --git a/frontend/src/core/components/viewer/NonPdfViewer.tsx b/frontend/src/core/components/viewer/NonPdfViewer.tsx
index 16f08fd171..2f187975dd 100644
--- a/frontend/src/core/components/viewer/NonPdfViewer.tsx
+++ b/frontend/src/core/components/viewer/NonPdfViewer.tsx
@@ -111,7 +111,6 @@ export function NonPdfViewer({ file }: NonPdfViewerProps) {
}}
>