@@ -160,7 +160,6 @@ export function CardExpansionModal({
{footer}
diff --git a/frontend/src/core/components/smartFolders/SmartFolderCard.tsx b/frontend/src/core/components/smartFolders/SmartFolderCard.tsx
index 4d05df73a9..808175ebb1 100644
--- a/frontend/src/core/components/smartFolders/SmartFolderCard.tsx
+++ b/frontend/src/core/components/smartFolders/SmartFolderCard.tsx
@@ -15,18 +15,49 @@ interface SmartFolderCardProps {
onSelect: () => void;
onEdit: (e: React.MouseEvent) => void;
onDelete: (e: React.MouseEvent) => void;
+ onFileDrop?: (fileIds: string[]) => void;
}
-export function SmartFolderCard({ folder, isActive, status, onSelect, onEdit, onDelete }: SmartFolderCardProps) {
+export function SmartFolderCard({ folder, isActive, status, onSelect, onEdit, onDelete, onFileDrop }: SmartFolderCardProps) {
const { t } = useTranslation();
const [isHovered, setIsHovered] = useState(false);
+ const [isDragOver, setIsDragOver] = useState(false);
const IconComponent = iconMap[folder.icon as keyof typeof iconMap] || iconMap.FolderIcon;
+ const handleDragOver = (e: React.DragEvent) => {
+ const types = e.dataTransfer.types;
+ if (!types.includes('watchfolderfileid') && !types.includes('watchfolderfileids')) return;
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'copy';
+ setIsDragOver(true);
+ };
+
+ const handleDragLeave = () => setIsDragOver(false);
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragOver(false);
+ const multiRaw = e.dataTransfer.getData('watchFolderFileIds');
+ if (multiRaw) {
+ try {
+ const ids: string[] = JSON.parse(multiRaw);
+ if (ids.length > 0 && onFileDrop) onFileDrop(ids);
+ return;
+ } catch { /* fall through */ }
+ }
+ const fileId = e.dataTransfer.getData('watchFolderFileId');
+ if (fileId && onFileDrop) onFileDrop([fileId]);
+ };
+
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
+ onDragOver={handleDragOver}
+ onDragLeave={handleDragLeave}
+ onDrop={handleDrop}
+ style={isDragOver ? { backgroundColor: `${folder.accentColor}18`, borderRadius: 'var(--mantine-radius-sm)' } : undefined}
>
);
}
diff --git a/frontend/src/core/components/smartFolders/SmartFolderWorkbenchView.tsx b/frontend/src/core/components/smartFolders/SmartFolderWorkbenchView.tsx
index a5ee18145b..b1a68d6e1b 100644
--- a/frontend/src/core/components/smartFolders/SmartFolderWorkbenchView.tsx
+++ b/frontend/src/core/components/smartFolders/SmartFolderWorkbenchView.tsx
@@ -43,14 +43,18 @@ import { SmartFolderHomePage } from '@app/components/smartFolders/SmartFolderHom
import { useNavigationActions } from '@app/contexts/NavigationContext';
interface SmartFolderWorkbenchViewProps {
- data: { folderId: string | null; pendingFileId?: string };
+ data: { folderId: string | null; pendingFileId?: string; pendingFileIds?: string[] };
}
-interface StepProgress {
- stepIndex: number;
- operationName: string;
- status: 'running' | 'completed' | 'error';
- error?: string;
+function timeAgo(date: Date): string {
+ const secs = Math.floor((Date.now() - date.getTime()) / 1000);
+ if (secs < 60) return 'just now';
+ const mins = Math.floor(secs / 60);
+ if (mins < 60) return `${mins}m ago`;
+ const hours = Math.floor(mins / 60);
+ if (hours < 24) return `${hours}h ago`;
+ const days = Math.floor(hours / 24);
+ return `${days}d ago`;
}
function humaniseOp(op: string): string {
@@ -85,7 +89,6 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
const { recentRuns, setRecentRuns } = useFolderRunState(folderId);
const [isDragOver, setIsDragOver] = useState(false);
- const [stepProgresses, setStepProgresses] = useState
([]);
const [outputFiles, setOutputFiles] = useState([]);
const [inputFiles, setInputFiles] = useState([]);
const [automation, setAutomation] = useState(null);
@@ -96,9 +99,15 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
const handledPendingRef = useRef(null);
useEffect(() => {
- folderStorage.getOutputFilesByFolder(folderId).then(setOutputFiles);
- folderStorage.getInputFilesByFolder(folderId).then(setInputFiles);
- }, [folderId, recentRuns]);
+ const load = () => {
+ if (!folderId) return;
+ folderStorage.getOutputFilesByFolder(folderId).then(setOutputFiles);
+ folderStorage.getInputFilesByFolder(folderId).then(setInputFiles);
+ };
+ load();
+ const unsub = folderStorage.onFolderChange((id) => { if (id === folderId) load(); });
+ return unsub;
+ }, [folderId]);
useEffect(() => {
if (folder?.automationId) {
@@ -122,31 +131,13 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
await updateFileMetadata(inputFileId, { status: 'processing' });
- setStepProgresses(prev => [
- ...prev,
- { stepIndex: 0, operationName: auto.operations[0]?.operation ?? '', status: 'running' },
- ]);
-
const resultFiles = await executeAutomationSequence(
auto,
[inputFile],
toolRegistry,
- (stepIndex, operationName) => {
- setStepProgresses(prev => {
- const updated = prev.filter(p => !(p.stepIndex === stepIndex && p.operationName === operationName));
- return [...updated, { stepIndex, operationName, status: 'running' }];
- });
- },
- (stepIndex) => {
- setStepProgresses(prev =>
- prev.map(p => p.stepIndex === stepIndex ? { ...p, status: 'completed' } : p)
- );
- },
- (stepIndex, error) => {
- setStepProgresses(prev =>
- prev.map(p => p.stepIndex === stepIndex ? { ...p, status: 'error', error } : p)
- );
- }
+ () => {},
+ () => {},
+ () => {}
);
const newRuns: SmartFolderRunEntry[] = [...recentRuns];
@@ -177,12 +168,13 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
);
const handleFiles = useCallback(
- async (files: FileList | File[]) => {
+ async (files: FileList | File[], sourceFileId?: string) => {
const fileArray = Array.from(files);
for (const file of fileArray) {
if (!file.name.toLowerCase().endsWith('.pdf')) continue;
const inputFileId = `input-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
- await addFile(inputFileId, { status: 'pending', inputFileId, name: file.name });
+ const originalFileId = sourceFileId ?? (file as any).fileId as string | undefined;
+ await addFile(inputFileId, { status: 'pending', inputFileId, name: file.name, ...(originalFileId ? { originalFileId } : {}) });
await folderStorage.storeInputFile(folderId, inputFileId, file, file.name);
runAutomation(file, inputFileId);
}
@@ -191,12 +183,30 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
);
useEffect(() => {
- const { pendingFileId } = data;
+ const { pendingFileId, pendingFileIds } = data;
+
+ // Multi-file pending (from sidebar multi-select drag to sidebar folder card)
+ if (pendingFileIds && pendingFileIds.length > 0) {
+ const key = pendingFileIds.join(',');
+ if (handledPendingRef.current === key) return;
+ handledPendingRef.current = key;
+ setCustomWorkbenchViewData(SMART_FOLDER_VIEW_ID, { folderId });
+ Promise.all(
+ pendingFileIds.map(id => fileStorage.getStirlingFile(id as FileId))
+ ).then(async results => {
+ for (let i = 0; i < results.length; i++) {
+ if (results[i]) await handleFiles([results[i]!], pendingFileIds[i]);
+ }
+ });
+ return;
+ }
+
+ // Single pending file (legacy path)
if (!pendingFileId || handledPendingRef.current === pendingFileId) return;
handledPendingRef.current = pendingFileId;
setCustomWorkbenchViewData(SMART_FOLDER_VIEW_ID, { folderId });
fileStorage.getStirlingFile(pendingFileId as FileId).then((stirlingFile) => {
- if (stirlingFile) handleFiles([stirlingFile]);
+ if (stirlingFile) handleFiles([stirlingFile], pendingFileId);
});
}, [data, folderId, handleFiles, setCustomWorkbenchViewData]);
@@ -204,7 +214,30 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
(e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
- if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files);
+
+ // Multi-file sidebar drag
+ const multiRaw = e.dataTransfer.getData('watchFolderFileIds');
+ if (multiRaw) {
+ try {
+ const ids: string[] = JSON.parse(multiRaw);
+ Promise.all(ids.map(id => fileStorage.getStirlingFile(id as FileId))).then(async results => {
+ for (let i = 0; i < results.length; i++) {
+ if (results[i]) await handleFiles([results[i]!], ids[i]);
+ }
+ });
+ return;
+ } catch { /* fall through */ }
+ }
+
+ // Single sidebar drag
+ const sidebarFileId = e.dataTransfer.getData('watchFolderFileId');
+ if (sidebarFileId) {
+ fileStorage.getStirlingFile(sidebarFileId as FileId).then((stirlingFile) => {
+ if (stirlingFile) handleFiles([stirlingFile], sidebarFileId);
+ });
+ } else if (e.dataTransfer.files.length > 0) {
+ handleFiles(e.dataTransfer.files);
+ }
},
[handleFiles]
);
@@ -482,43 +515,13 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
- {stepProgresses.length === 0 && fileIds.length === 0 ? (
+ {fileIds.length === 0 ? (
{t('smartFolders.workbench.noActivity', 'No activity yet — drop a PDF to start')}
) : (
<>
- {/* Live step progress */}
- {[...stepProgresses].reverse().map((step, i) => (
-
-
- {step.status === 'running' && }
- {step.status === 'completed' && (
-
- )}
- {step.status === 'error' && (
-
- )}
-
- {`Step ${step.stepIndex + 1}: ${humaniseOp(step.operationName)}`}
-
-
- {step.error && (
- {step.error}
- )}
-
- ))}
-
- {/* File status list */}
- {fileIds.map((fileId) => {
+ {[...fileIds].reverse().map((fileId) => {
const meta = folderRecord?.files[fileId];
const status = meta?.status ?? 'pending';
const run = recentRuns.find(r => r.inputFileId === fileId);
@@ -569,9 +572,9 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps
)}
-
+
{(meta?.processedAt || meta?.addedAt)
- ? new Date((meta.processedAt ?? meta.addedAt)!).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ ? timeAgo(new Date((meta.processedAt ?? meta.addedAt)!))
: ''}
diff --git a/frontend/src/core/components/smartFolders/WatchFolderFileList.tsx b/frontend/src/core/components/smartFolders/WatchFolderFileList.tsx
new file mode 100644
index 0000000000..fb073100c9
--- /dev/null
+++ b/frontend/src/core/components/smartFolders/WatchFolderFileList.tsx
@@ -0,0 +1,426 @@
+import { useState, useRef, useCallback, useEffect } from 'react';
+import { Box, Text, ActionIcon, Group, Popover, Stack, Button, Tooltip } from '@mantine/core';
+import { useTranslation } from 'react-i18next';
+import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
+import AddIcon from '@mui/icons-material/Add';
+import CheckIcon from '@mui/icons-material/Check';
+import ChevronRightIcon from '@mui/icons-material/ChevronRight';
+import CloseIcon from '@mui/icons-material/Close';
+import { StirlingFileStub } from '@app/types/fileContext';
+import { useFolderMembership } from '@app/hooks/useFolderMembership';
+import { useAllSmartFolders } from '@app/hooks/useAllSmartFolders';
+import { iconMap } from '@app/components/tools/automate/iconMap';
+
+interface WatchFolderFileListProps {
+ files: StirlingFileStub[];
+ /** Currently open folder id, or null if on home page */
+ folderId: string | null;
+ onSendToFolder: (fileId: string, folderId: string) => void;
+ onNavigateToFolder: (folderId: string) => void;
+}
+
+function formatSize(bytes?: number): string {
+ if (!bytes) return '';
+ if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)} KB`;
+ return `${bytes} B`;
+}
+
+function FileRow({
+ file,
+ index,
+ currentFolderId,
+ memberFolderIds,
+ isSelected,
+ onToggleSelect,
+ onSendToFolder,
+ onNavigateToFolder,
+ onDragStart,
+}: {
+ file: StirlingFileStub;
+ index: number;
+ currentFolderId: string | null;
+ memberFolderIds: string[];
+ isSelected: boolean;
+ onToggleSelect: (idx: number, shiftKey: boolean) => void;
+ onSendToFolder: (fileId: string, folderId: string) => void;
+ onNavigateToFolder: (folderId: string) => void;
+ onDragStart: (e: React.DragEvent, file: StirlingFileStub) => void;
+}) {
+ const { t } = useTranslation();
+ const folders = useAllSmartFolders();
+ const [hovered, setHovered] = useState(false);
+ const [pickerOpen, setPickerOpen] = useState(false);
+
+ const isInCurrentFolder = currentFolderId !== null && memberFolderIds.includes(currentFolderId);
+ const otherFolderIds = memberFolderIds.filter(id => id !== currentFolderId);
+ const otherFolders = otherFolderIds.map(id => folders.find(f => f.id === id)).filter(Boolean) as typeof folders;
+
+ const handleRowClick = (e: React.MouseEvent) => {
+ // Don't trigger selection when clicking action areas
+ const target = e.target as HTMLElement;
+ if (target.closest('[data-no-select]')) return;
+ onToggleSelect(index, e.shiftKey);
+ };
+
+ return (
+ onDragStart(e, file)}
+ onMouseEnter={() => setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ onClick={handleRowClick}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.375rem',
+ padding: '0.3rem 0.5rem',
+ borderRadius: 'var(--mantine-radius-sm)',
+ borderBottom: '0.0625rem solid var(--border-subtle)',
+ cursor: 'grab',
+ backgroundColor: isSelected
+ ? 'var(--mantine-color-blue-light)'
+ : hovered
+ ? 'var(--mantine-color-default-hover)'
+ : 'transparent',
+ outline: isSelected ? '0.0625rem solid var(--mantine-color-blue-light-hover)' : 'none',
+ transition: 'background-color 0.1s ease',
+ minWidth: 0,
+ userSelect: 'none',
+ }}
+ >
+ {/* Drag handle */}
+
+
+ {/* File info */}
+
+
+ {file.name}
+
+ {file.size && (
+
+ {formatSize(file.size)}
+
+ )}
+
+
+ {/* Folder badges for files in other folders */}
+ {otherFolders.slice(0, 1).map(memberFolder => (
+
+ onNavigateToFolder(memberFolder.id)}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.2rem',
+ padding: '0.125rem 0.375rem',
+ borderRadius: '1rem',
+ backgroundColor: `${memberFolder.accentColor}18`,
+ border: `0.0625rem solid ${memberFolder.accentColor}30`,
+ cursor: 'pointer',
+ flexShrink: 0,
+ maxWidth: '5rem',
+ }}
+ >
+
+
+ {memberFolder.name}
+
+
+
+
+ ))}
+ {otherFolders.length > 1 && (
+
+ +{otherFolders.length - 1}
+
+ )}
+
+ {/* Action button */}
+ {isInCurrentFolder ? (
+
+
+
+
+
+ ) : currentFolderId !== null ? (
+ // Inside a folder — directly add
+
+ onSendToFolder(file.id, currentFolderId)}
+ >
+
+
+
+ ) : (
+ // Home page — folder picker popover
+
+
+ { e.stopPropagation(); setPickerOpen(o => !o); }}
+ >
+
+
+
+
+
+ {t('smartFolders.fileList.addToFolder', 'Add to folder')}
+
+
+ {folders.filter(f => !memberFolderIds.includes(f.id)).map(folder => {
+ const FolderIconComp = iconMap[folder.icon as keyof typeof iconMap] || iconMap.FolderIcon;
+ return (
+
+ }
+ onClick={() => {
+ onSendToFolder(file.id, folder.id);
+ setPickerOpen(false);
+ }}
+ >
+
+ {folder.name}
+
+
+ );
+ })}
+ {folders.filter(f => !memberFolderIds.includes(f.id)).length === 0 && (
+
+ {memberFolderIds.length > 0
+ ? t('smartFolders.fileList.inAllFolders', 'Already in all folders')
+ : t('smartFolders.noFolders', 'No watch folders yet')}
+
+ )}
+
+
+
+ )}
+
+ );
+}
+
+function SectionHeader({ label, count }: { label: string; count: number }) {
+ return (
+
+
+ {label}
+
+
+ {count}
+
+
+ );
+}
+
+export function WatchFolderFileList({ files, folderId, onSendToFolder, onNavigateToFolder }: WatchFolderFileListProps) {
+ const { t } = useTranslation();
+ const membership = useFolderMembership();
+ const [selectedIds, setSelectedIds] = useState>(new Set());
+ const lastSelectedIdxRef = useRef(null);
+ // Ref for drag ghost cleanup
+ const ghostRef = useRef(null);
+
+ const unassigned = files.filter(f => !membership.has(f.id));
+ const inFolders = files.filter(f => membership.has(f.id));
+
+ // All files in display order (unassigned first, then in-folders)
+ const orderedFiles = [...unassigned, ...inFolders];
+
+ const handleToggleSelect = useCallback((idx: number, shiftKey: boolean) => {
+ const file = orderedFiles[idx];
+ if (!file) return;
+ setSelectedIds(prev => {
+ const next = new Set(prev);
+ if (shiftKey && lastSelectedIdxRef.current !== null) {
+ // Range select
+ const start = Math.min(lastSelectedIdxRef.current, idx);
+ const end = Math.max(lastSelectedIdxRef.current, idx);
+ for (let i = start; i <= end; i++) {
+ if (orderedFiles[i]) next.add(orderedFiles[i].id);
+ }
+ } else {
+ if (next.has(file.id)) {
+ next.delete(file.id);
+ } else {
+ next.add(file.id);
+ }
+ }
+ return next;
+ });
+ lastSelectedIdxRef.current = idx;
+ }, [orderedFiles]);
+
+ const clearSelection = useCallback(() => {
+ setSelectedIds(new Set());
+ lastSelectedIdxRef.current = null;
+ }, []);
+
+ // Escape key to clear selection
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') clearSelection();
+ };
+ window.addEventListener('keydown', onKeyDown);
+ return () => window.removeEventListener('keydown', onKeyDown);
+ }, [clearSelection]);
+
+ const handleDragStart = useCallback((e: React.DragEvent, file: StirlingFileStub) => {
+ const isFileSelected = selectedIds.has(file.id);
+ const isMulti = isFileSelected && selectedIds.size > 1;
+
+ if (isMulti) {
+ const ids = Array.from(selectedIds);
+ e.dataTransfer.setData('watchFolderFileIds', JSON.stringify(ids));
+ // Also write single as fallback for older handlers
+ e.dataTransfer.setData('watchFolderFileId', ids[0]);
+
+ // Custom ghost showing "N files"
+ const ghost = document.createElement('div');
+ ghost.style.cssText = [
+ 'position:fixed',
+ 'top:-9999px',
+ 'left:-9999px',
+ 'padding:0.25rem 0.75rem',
+ 'border-radius:0.5rem',
+ 'background:var(--mantine-color-blue-filled,#3b82f6)',
+ 'color:#fff',
+ 'font-size:0.75rem',
+ 'font-weight:600',
+ 'white-space:nowrap',
+ 'pointer-events:none',
+ 'z-index:9999',
+ 'font-family:var(--mantine-font-family,sans-serif)',
+ ].join(';');
+ ghost.textContent = `${ids.length} files`;
+ document.body.appendChild(ghost);
+ ghostRef.current = ghost;
+ e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2);
+
+ e.dataTransfer.effectAllowed = 'copy';
+ } else {
+ e.dataTransfer.setData('watchFolderFileId', file.id);
+ e.dataTransfer.effectAllowed = 'copy';
+ }
+ }, [selectedIds]);
+
+ // Clean up ghost and clear selection after drag ends
+ const handleDragEnd = useCallback(() => {
+ if (ghostRef.current) {
+ document.body.removeChild(ghostRef.current);
+ ghostRef.current = null;
+ }
+ clearSelection();
+ }, [clearSelection]);
+
+ if (files.length === 0) {
+ return (
+
+ {t('smartFolders.fileList.empty', 'No files yet — upload a PDF to get started')}
+
+ );
+ }
+
+ return (
+
+ {/* Selection count pill */}
+ {selectedIds.size >= 2 && (
+
+
+ {selectedIds.size} {t('smartFolders.fileList.selected', 'selected')}
+
+
+
+
+
+ )}
+
+ {unassigned.length > 0 && (
+ <>
+ {inFolders.length > 0 && (
+
+ )}
+ {unassigned.map((file, i) => (
+
+ ))}
+ >
+ )}
+
+ {inFolders.length > 0 && (
+ <>
+
+ {inFolders.map((file, i) => (
+
+ ))}
+ >
+ )}
+
+ );
+}
diff --git a/frontend/src/core/components/tools/automate/ToolSelector.tsx b/frontend/src/core/components/tools/automate/ToolSelector.tsx
index 2d87ae69a9..f6fb6716ff 100644
--- a/frontend/src/core/components/tools/automate/ToolSelector.tsx
+++ b/frontend/src/core/components/tools/automate/ToolSelector.tsx
@@ -1,4 +1,5 @@
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
+import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { Stack, Text, ScrollArea } from '@mantine/core';
import { ToolRegistryEntry, ToolRegistry, getToolSupportsAutomate } from '@app/data/toolsTaxonomy';
@@ -28,7 +29,9 @@ export default function ToolSelector({
const [opened, setOpened] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
+ const [dropdownRect, setDropdownRect] = useState(null);
const containerRef = useRef(null);
+ const dropdownRef = useRef(null);
// Filter out excluded tools (like 'automate' itself) and tools that don't support automation
const baseFilteredTools = useMemo(() => {
@@ -111,10 +114,28 @@ export default function ToolSelector({
setShouldAutoFocus(true); // Request auto-focus for the input
};
+ // Track container rect for portal positioning
+ useEffect(() => {
+ if (!opened) return;
+ const updateRect = () => {
+ if (containerRef.current) setDropdownRect(containerRef.current.getBoundingClientRect());
+ };
+ updateRect();
+ window.addEventListener('scroll', updateRect, true);
+ window.addEventListener('resize', updateRect);
+ return () => {
+ window.removeEventListener('scroll', updateRect, true);
+ window.removeEventListener('resize', updateRect);
+ };
+ }, [opened]);
+
// Handle click outside to close dropdown
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
- if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
+ const target = event.target as Node;
+ const inContainer = containerRef.current?.contains(target);
+ const inDropdown = dropdownRef.current?.contains(target);
+ if (!inContainer && !inDropdown) {
setOpened(false);
setSearchTerm('');
}
@@ -181,24 +202,31 @@ export default function ToolSelector({
/>
)}
- {/* Custom dropdown */}
- {opened && (
-
-
+ {/* Custom dropdown — rendered in a portal so it escapes modal overflow */}
+ {opened && dropdownRect && createPortal(
+ (() => {
+ const spaceBelow = window.innerHeight - dropdownRect.bottom - 8;
+ const spaceAbove = dropdownRect.top - 8;
+ const maxH = Math.max(120, Math.min(350, spaceBelow > 120 ? spaceBelow : spaceAbove));
+ const openUpward = spaceBelow < 120 && spaceAbove > spaceBelow;
+ const top = openUpward ? dropdownRect.top - maxH - 4 : dropdownRect.bottom + 4;
+ return (
+
+
{displayGroups.length === 0 ? (
@@ -212,7 +240,10 @@ export default function ToolSelector({
)}
-
+
+ );
+ })(),
+ document.body
)}
);
diff --git a/frontend/src/core/hooks/useFolderMembership.ts b/frontend/src/core/hooks/useFolderMembership.ts
index 8734d0e39d..7d612ff9e0 100644
--- a/frontend/src/core/hooks/useFolderMembership.ts
+++ b/frontend/src/core/hooks/useFolderMembership.ts
@@ -8,9 +8,9 @@ import { useState, useEffect } from 'react';
import { folderStorage } from '@app/services/folderStorage';
import { useAllSmartFolders } from '@app/hooks/useAllSmartFolders';
-export function useFolderMembership(): Map