diff --git a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx
index b35d0560ee..83a1d4896b 100644
--- a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx
+++ b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx
@@ -608,6 +608,13 @@ export const GalleryModal = React.memo(
[folders, activeFolderId],
);
+ // Root-level folders for the filter sidebar — always roots, regardless of
+ // which folder is open in the browser tab (mirrors the webapp sidebar).
+ const rootFolders = useMemo(
+ () => folders.filter((f) => !f.parentId).sort(compareFolders),
+ [folders],
+ );
+
// Breadcrumb trail from root → active folder (inclusive), guarded against
// cycles so a corrupted parent chain can never loop forever.
const folderPath = useMemo(() => {
@@ -1280,6 +1287,16 @@ export const GalleryModal = React.memo(
setContextMenu(null);
}, []);
+ // Open a folder from the filter sidebar: jump to the folder browser tab
+ // with that folder active.
+ const handleOpenFolderFromSidebar = useCallback(
+ (folderId: string) => {
+ setGalleryTab("folders");
+ handleOpenFolder(folderId);
+ },
+ [handleOpenFolder],
+ );
+
// Close any open folder menus
const closeFolderMenus = useCallback(() => {
setFolderMenuOpen(false);
@@ -2170,46 +2187,109 @@ export const GalleryModal = React.memo(
{/* ── Filter sidebar ── (Unsorted tab / picker only; the Folders
tab is a full-width browser) */}
{!hideFilter && galleryTab === "unsorted" && (
-
- {/* Filter items — hidden when the gallery is used as a constrained picker */}
- {!hideFilter && (
- <>
-
- {SIDEBAR_FILTERS.map((f) => {
- const isActive = activeFilter === f.id;
- return (
-
- );
- })}
+
+ {/* Filter items — pinned; they never scroll out of view */}
+
+ {SIDEBAR_FILTERS.map((f) => {
+ const isActive = activeFilter === f.id;
+ return (
+
+ );
+ })}
+
+ {/* Folders header — pinned alongside the filters */}
+
+
+ Folders
+
+ {mode === "view" && (
+
+ )}
+
+ {/* Folder list — the only scrolling region; fills the rest of the column */}
+
+ {rootFolders.length === 0 ? (
+
+ No folders yet
- >
- )}
+ ) : (
+ rootFolders.map((folder) => (
+
+ ))
+ )}
+
)}
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx
index e12e9861f3..0b0a0681b9 100644
--- a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx
+++ b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx
@@ -18,7 +18,11 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
-import { Prompt2DStore, RefImage, useEnterToGenerateStore } from "./promptStore";
+import {
+ Prompt2DStore,
+ RefImage,
+ useEnterToGenerateStore,
+} from "./promptStore";
import { ImageModel } from "@storyteller/model-list";
import { ImagePromptRow, UploadImageFn } from "./ImagePromptRow";
import { twMerge } from "tailwind-merge";
@@ -376,7 +380,7 @@ export const PromptBox2D = ({
)}
{
- base64Snapshot: string;
- file: File;
- } | null)
- | undefined;
+ | ((shouldDownload?: boolean) => {
+ base64Snapshot: string;
+ file: File;
+ } | null)
+ | undefined;
credits?: number | null;
/** Optional model-picker slot rendered at the start of the toolbar
* (left of the aspect-ratio picker). Tauri leaves this unset and
@@ -298,7 +302,9 @@ export const PromptBox3D = ({
return;
}
if (isFinite(maxLen) && prompt.length > maxLen) {
- toast.error(`Prompt exceeds the ${maxLen} character limit for this model`);
+ toast.error(
+ `Prompt exceeds the ${maxLen} character limit for this model`,
+ );
return;
}
gtagEvent("enqueue_3d");
@@ -479,8 +485,7 @@ export const PromptBox3D = ({
?.map((image) => image.url)
.filter(Boolean),
modelType:
- (selectedImageModel as any)?.tauriId ||
- String(selectedImageModel),
+ (selectedImageModel as any)?.tauriId || String(selectedImageModel),
timestamp: Date.now(),
});
@@ -628,11 +633,11 @@ export const PromptBox3D = ({
)}
e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
@@ -693,7 +698,9 @@ export const PromptBox3D = ({
setIsPromptBoxFocused(false);
}}
/>
- maxLen ? "text-red-500" : "text-base-fg/40"}`}>
+ maxLen ? "text-red-500" : "text-base-fg/40"}`}
+ >
{prompt.length} / {isFinite(maxLen) ? maxLen : "∞"}
@@ -836,13 +843,20 @@ export const PromptBox3D = ({
-
+
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx
index b1c326636d..fc0a90f96d 100644
--- a/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx
+++ b/frontend/libs/components/promptbox/src/lib/PromptBoxImage.tsx
@@ -6,10 +6,7 @@ import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Button, ToggleButton, GenerateButton } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
-import {
- GenerateImage,
- GenerateImageRequest,
-} from "@storyteller/tauri-api";
+import { GenerateImage, GenerateImageRequest } from "@storyteller/tauri-api";
import {
faMessageXmark,
faMessageCheck,
@@ -19,7 +16,11 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CommonAspectRatio, ImageModel } from "@storyteller/model-list";
-import { usePromptImageStore, RefImage, useEnterToGenerateStore } from "./promptStore";
+import {
+ usePromptImageStore,
+ RefImage,
+ useEnterToGenerateStore,
+} from "./promptStore";
import { gtagEvent } from "@storyteller/google-analytics";
import { twMerge } from "tailwind-merge";
import { ImagePromptRow } from "./ImagePromptRow";
@@ -68,7 +69,11 @@ export const PromptBoxImage = ({
}: PromptBoxImageProps) => {
useSignals();
- console.debug("Selected model and provider:", selectedModel, selectedProvider);
+ console.debug(
+ "Selected model and provider:",
+ selectedModel,
+ selectedProvider,
+ );
useEffect(() => {
if (imageMediaId && url) {
@@ -414,7 +419,7 @@ export const PromptBoxImage = ({