stirlingFileStub instead of workbench file

This commit is contained in:
Reece Browne 2025-09-05 00:23:55 +01:00
parent bdf2c0397e
commit 9766949f76
13 changed files with 146 additions and 139 deletions

View File

@ -50,7 +50,7 @@ const FileEditor = ({
// Extract needed values from state (memoized to prevent infinite loops) // Extract needed values from state (memoized to prevent infinite loops)
const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]); const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]);
const activeWorkbenchFiles = useMemo(() => selectors.getWorkbenchFiles(), [selectors.getFilesSignature()]); const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [selectors.getFilesSignature()]);
const selectedFileIds = state.ui.selectedFileIds; const selectedFileIds = state.ui.selectedFileIds;
const isProcessing = state.ui.isProcessing; const isProcessing = state.ui.isProcessing;
@ -92,10 +92,10 @@ const FileEditor = ({
const contextSelectedIdsRef = useRef<FileId[]>([]); const contextSelectedIdsRef = useRef<FileId[]>([]);
contextSelectedIdsRef.current = contextSelectedIds; contextSelectedIdsRef.current = contextSelectedIds;
// Use activeWorkbenchFiles directly - no conversion needed // Use activeStirlingFileStubs directly - no conversion needed
const localSelectedIds = contextSelectedIds; const localSelectedIds = contextSelectedIds;
// Helper to convert WorkbenchFile to FileThumbnail format // Helper to convert StirlingFileStub to FileThumbnail format
const recordToFileItem = useCallback((record: any) => { const recordToFileItem = useCallback((record: any) => {
const file = selectors.getFile(record.id); const file = selectors.getFile(record.id);
if (!file) return null; if (!file) return null;
@ -253,26 +253,26 @@ const FileEditor = ({
}, [addFiles]); }, [addFiles]);
const selectAll = useCallback(() => { const selectAll = useCallback(() => {
setSelectedFiles(activeWorkbenchFiles.map(r => r.id)); // Use WorkbenchFile IDs directly setSelectedFiles(activeStirlingFileStubs.map(r => r.id)); // Use StirlingFileStub IDs directly
}, [activeWorkbenchFiles, setSelectedFiles]); }, [activeStirlingFileStubs, setSelectedFiles]);
const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]); const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]);
const closeAllFiles = useCallback(() => { const closeAllFiles = useCallback(() => {
if (activeWorkbenchFiles.length === 0) return; if (activeStirlingFileStubs.length === 0) return;
// Remove all files from context but keep in storage // Remove all files from context but keep in storage
const allFileIds = activeWorkbenchFiles.map(record => record.id); const allFileIds = activeStirlingFileStubs.map(record => record.id);
removeFiles(allFileIds, false); // false = keep in storage removeFiles(allFileIds, false); // false = keep in storage
// Clear selections // Clear selections
setSelectedFiles([]); setSelectedFiles([]);
}, [activeWorkbenchFiles, removeFiles, setSelectedFiles]); }, [activeStirlingFileStubs, removeFiles, setSelectedFiles]);
const toggleFile = useCallback((fileId: FileId) => { const toggleFile = useCallback((fileId: FileId) => {
const currentSelectedIds = contextSelectedIdsRef.current; const currentSelectedIds = contextSelectedIdsRef.current;
const targetRecord = activeWorkbenchFiles.find(r => r.id === fileId); const targetRecord = activeStirlingFileStubs.find(r => r.id === fileId);
if (!targetRecord) return; if (!targetRecord) return;
const contextFileId = fileId; // No need to create a new ID const contextFileId = fileId; // No need to create a new ID
@ -302,7 +302,7 @@ const FileEditor = ({
// Update context (this automatically updates tool selection since they use the same action) // Update context (this automatically updates tool selection since they use the same action)
setSelectedFiles(newSelection); setSelectedFiles(newSelection);
}, [setSelectedFiles, toolMode, setStatus, activeWorkbenchFiles]); }, [setSelectedFiles, toolMode, setStatus, activeStirlingFileStubs]);
const toggleSelectionMode = useCallback(() => { const toggleSelectionMode = useCallback(() => {
setSelectionMode(prev => { setSelectionMode(prev => {
@ -316,7 +316,7 @@ const FileEditor = ({
// File reordering handler for drag and drop // File reordering handler for drag and drop
const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => { const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => {
const currentIds = activeWorkbenchFiles.map(r => r.id); const currentIds = activeStirlingFileStubs.map(r => r.id);
// Find indices // Find indices
const sourceIndex = currentIds.findIndex(id => id === sourceFileId); const sourceIndex = currentIds.findIndex(id => id === sourceFileId);
@ -368,13 +368,13 @@ const FileEditor = ({
// Update status // Update status
const moveCount = filesToMove.length; const moveCount = filesToMove.length;
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`); setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
}, [activeWorkbenchFiles, reorderFiles, setStatus]); }, [activeStirlingFileStubs, reorderFiles, setStatus]);
// File operations using context // File operations using context
const handleDeleteFile = useCallback((fileId: FileId) => { const handleDeleteFile = useCallback((fileId: FileId) => {
const record = activeWorkbenchFiles.find(r => r.id === fileId); const record = activeStirlingFileStubs.find(r => r.id === fileId);
const file = record ? selectors.getFile(record.id) : null; const file = record ? selectors.getFile(record.id) : null;
if (record && file) { if (record && file) {
@ -405,27 +405,27 @@ const FileEditor = ({
const currentSelected = selectedFileIds.filter(id => id !== contextFileId); const currentSelected = selectedFileIds.filter(id => id !== contextFileId);
setSelectedFiles(currentSelected); setSelectedFiles(currentSelected);
} }
}, [activeWorkbenchFiles, selectors, removeFiles, setSelectedFiles, selectedFileIds]); }, [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
const handleViewFile = useCallback((fileId: FileId) => { const handleViewFile = useCallback((fileId: FileId) => {
const record = activeWorkbenchFiles.find(r => r.id === fileId); const record = activeStirlingFileStubs.find(r => r.id === fileId);
if (record) { if (record) {
// Set the file as selected in context and switch to viewer for preview // Set the file as selected in context and switch to viewer for preview
setSelectedFiles([fileId]); setSelectedFiles([fileId]);
navActions.setWorkbench('viewer'); navActions.setWorkbench('viewer');
} }
}, [activeWorkbenchFiles, setSelectedFiles, navActions.setWorkbench]); }, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]);
const handleMergeFromHere = useCallback((fileId: FileId) => { const handleMergeFromHere = useCallback((fileId: FileId) => {
const startIndex = activeWorkbenchFiles.findIndex(r => r.id === fileId); const startIndex = activeStirlingFileStubs.findIndex(r => r.id === fileId);
if (startIndex === -1) return; if (startIndex === -1) return;
const recordsToMerge = activeWorkbenchFiles.slice(startIndex); const recordsToMerge = activeStirlingFileStubs.slice(startIndex);
const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as StirlingFile[]; const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as StirlingFile[];
if (onMergeFiles) { if (onMergeFiles) {
onMergeFiles(filesToMerge); onMergeFiles(filesToMerge);
} }
}, [activeWorkbenchFiles, selectors, onMergeFiles]); }, [activeStirlingFileStubs, selectors, onMergeFiles]);
const handleSplitFile = useCallback((fileId: FileId) => { const handleSplitFile = useCallback((fileId: FileId) => {
const file = selectors.getFile(fileId); const file = selectors.getFile(fileId);
@ -467,7 +467,7 @@ const FileEditor = ({
<Box p="md" pt="xl"> <Box p="md" pt="xl">
{activeWorkbenchFiles.length === 0 && !zipExtractionProgress.isExtracting ? ( {activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? (
<Center h="60vh"> <Center h="60vh">
<Stack align="center" gap="md"> <Stack align="center" gap="md">
<Text size="lg" c="dimmed">📁</Text> <Text size="lg" c="dimmed">📁</Text>
@ -475,7 +475,7 @@ const FileEditor = ({
<Text size="sm" c="dimmed">Upload PDF files, ZIP archives, or load from storage to get started</Text> <Text size="sm" c="dimmed">Upload PDF files, ZIP archives, or load from storage to get started</Text>
</Stack> </Stack>
</Center> </Center>
) : activeWorkbenchFiles.length === 0 && zipExtractionProgress.isExtracting ? ( ) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? (
<Box> <Box>
<SkeletonLoader type="controls" /> <SkeletonLoader type="controls" />
@ -522,7 +522,7 @@ const FileEditor = ({
pointerEvents: 'auto' pointerEvents: 'auto'
}} }}
> >
{activeWorkbenchFiles.map((record, index) => { {activeStirlingFileStubs.map((record, index) => {
const fileItem = recordToFileItem(record); const fileItem = recordToFileItem(record);
if (!fileItem) return null; if (!fileItem) return null;
@ -531,7 +531,7 @@ const FileEditor = ({
key={record.id} key={record.id}
file={fileItem} file={fileItem}
index={index} index={index}
totalFiles={activeWorkbenchFiles.length} totalFiles={activeStirlingFileStubs.length}
selectedFiles={localSelectedIds} selectedFiles={localSelectedIds}
selectionMode={selectionMode} selectionMode={selectionMode}
onToggleFile={toggleFile} onToggleFile={toggleFile}

View File

@ -27,9 +27,9 @@ export function usePageDocument(): PageDocumentHook {
const globalProcessing = state.ui.isProcessing; const globalProcessing = state.ui.isProcessing;
// Get primary file record outside useMemo to track processedFile changes // Get primary file record outside useMemo to track processedFile changes
const primaryWorkbenchFile = primaryFileId ? selectors.getWorkbenchFile(primaryFileId) : null; const primaryStirlingFileStub = primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : null;
const processedFilePages = primaryWorkbenchFile?.processedFile?.pages; const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
const processedFileTotalPages = primaryWorkbenchFile?.processedFile?.totalPages; const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages;
// Compute merged document with stable signature (prevents infinite loops) // Compute merged document with stable signature (prevents infinite loops)
const mergedPdfDocument = useMemo((): PDFDocument | null => { const mergedPdfDocument = useMemo((): PDFDocument | null => {
@ -38,16 +38,16 @@ export function usePageDocument(): PageDocumentHook {
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null; const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
// If we have file IDs but no file record, something is wrong - return null to show loading // If we have file IDs but no file record, something is wrong - return null to show loading
if (!primaryWorkbenchFile) { if (!primaryStirlingFileStub) {
console.log('🎬 PageEditor: No primary file record found, showing loading'); console.log('🎬 PageEditor: No primary file record found, showing loading');
return null; return null;
} }
const name = const name =
activeFileIds.length === 1 activeFileIds.length === 1
? (primaryWorkbenchFile.name ?? 'document.pdf') ? (primaryStirlingFileStub.name ?? 'document.pdf')
: activeFileIds : activeFileIds
.map(id => (selectors.getWorkbenchFile(id)?.name ?? 'file').replace(/\.pdf$/i, '')) .map(id => (selectors.getStirlingFileStub(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
.join(' + '); .join(' + ');
// Build page insertion map from files with insertion positions // Build page insertion map from files with insertion positions
@ -55,7 +55,7 @@ export function usePageDocument(): PageDocumentHook {
const originalFileIds: FileId[] = []; const originalFileIds: FileId[] = [];
activeFileIds.forEach(fileId => { activeFileIds.forEach(fileId => {
const record = selectors.getWorkbenchFile(fileId); const record = selectors.getStirlingFileStub(fileId);
if (record?.insertAfterPageId !== undefined) { if (record?.insertAfterPageId !== undefined) {
if (!insertionMap.has(record.insertAfterPageId)) { if (!insertionMap.has(record.insertAfterPageId)) {
insertionMap.set(record.insertAfterPageId, []); insertionMap.set(record.insertAfterPageId, []);
@ -72,12 +72,12 @@ export function usePageDocument(): PageDocumentHook {
// Helper function to create pages from a file // Helper function to create pages from a file
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => { const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
const workbenchFiles = selectors.getWorkbenchFile(fileId); const stirlingFileStub = selectors.getStirlingFileStub(fileId);
if (!workbenchFiles) { if (!stirlingFileStub) {
return []; return [];
} }
const processedFile = workbenchFiles.processedFile; const processedFile = stirlingFileStub.processedFile;
let filePages: PDFPage[] = []; let filePages: PDFPage[] = [];
if (processedFile?.pages && processedFile.pages.length > 0) { if (processedFile?.pages && processedFile.pages.length > 0) {
@ -159,7 +159,7 @@ export function usePageDocument(): PageDocumentHook {
}; };
return mergedDoc; return mergedDoc;
}, [activeFileIds, primaryFileId, primaryWorkbenchFile, processedFilePages, processedFileTotalPages, selectors, filesSignature]); }, [activeFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
// Large document detection for smart loading // Large document detection for smart loading
const isVeryLargeDocument = useMemo(() => { const isVeryLargeDocument = useMemo(() => {

View File

@ -6,13 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage";
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from "@mui/icons-material/Visibility";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
import { WorkbenchFile } from "../../types/fileContext"; import { StirlingFileStub } from "../../types/fileContext";
import { getFileSize, getFileDate } from "../../utils/fileUtils"; import { getFileSize, getFileDate } from "../../utils/fileUtils";
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail"; import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
interface FileCardProps { interface FileCardProps {
file: File; file: File;
record?: WorkbenchFile; record?: StirlingFileStub;
onRemove: () => void; onRemove: () => void;
onDoubleClick?: () => void; onDoubleClick?: () => void;
onView?: () => void; onView?: () => void;

View File

@ -4,15 +4,15 @@ import { useTranslation } from "react-i18next";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort"; import SortIcon from "@mui/icons-material/Sort";
import FileCard from "./FileCard"; import FileCard from "./FileCard";
import { WorkbenchFile } from "../../types/fileContext"; import { StirlingFileStub } from "../../types/fileContext";
import { FileId } from "../../types/file"; import { FileId } from "../../types/file";
interface FileGridProps { interface FileGridProps {
files: Array<{ file: File; record?: WorkbenchFile }>; files: Array<{ file: File; record?: StirlingFileStub }>;
onRemove?: (index: number) => void; onRemove?: (index: number) => void;
onDoubleClick?: (item: { file: File; record?: WorkbenchFile }) => void; onDoubleClick?: (item: { file: File; record?: StirlingFileStub }) => void;
onView?: (item: { file: File; record?: WorkbenchFile }) => void; onView?: (item: { file: File; record?: StirlingFileStub }) => void;
onEdit?: (item: { file: File; record?: WorkbenchFile }) => void; onEdit?: (item: { file: File; record?: StirlingFileStub }) => void;
onSelect?: (fileId: FileId) => void; onSelect?: (fileId: FileId) => void;
selectedFiles?: FileId[]; selectedFiles?: FileId[];
showSearch?: boolean; showSearch?: boolean;
@ -126,7 +126,7 @@ const FileGrid = ({
{displayFiles {displayFiles
.filter(item => { .filter(item => {
if (!item.record?.id) { if (!item.record?.id) {
console.error('FileGrid: File missing WorkbenchFile with proper ID:', item.file.name); console.error('FileGrid: File missing StirlingFileStub with proper ID:', item.file.name);
return false; return false;
} }
return true; return true;

View File

@ -18,7 +18,7 @@ const FileStatusIndicator = ({
}: FileStatusIndicatorProps) => { }: FileStatusIndicatorProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { openFilesModal, onFilesSelect } = useFilesModalContext(); const { openFilesModal, onFilesSelect } = useFilesModalContext();
const { files: workbenchFiles } = useAllFiles(); const { files: stirlingFileStubs } = useAllFiles();
const { loadRecentFiles } = useFileManager(); const { loadRecentFiles } = useFileManager();
const [hasRecentFiles, setHasRecentFiles] = useState<boolean | null>(null); const [hasRecentFiles, setHasRecentFiles] = useState<boolean | null>(null);
@ -56,7 +56,7 @@ const FileStatusIndicator = ({
} }
// Check if there are no files in the workbench // Check if there are no files in the workbench
if (workbenchFiles.length === 0) { if (stirlingFileStubs.length === 0) {
// If no recent files, show upload button // If no recent files, show upload button
if (!hasRecentFiles) { if (!hasRecentFiles) {
return ( return (

View File

@ -20,7 +20,7 @@ import {
FileContextActionsValue, FileContextActionsValue,
FileContextActions, FileContextActions,
FileId, FileId,
WorkbenchFile, StirlingFileStub,
StirlingFile, StirlingFile,
createStirlingFile createStirlingFile
} from '../types/fileContext'; } from '../types/fileContext';
@ -127,8 +127,8 @@ function FileContextInner({
return consumeFiles(inputFileIds, outputFiles, filesRef, dispatch, indexedDB); return consumeFiles(inputFileIds, outputFiles, filesRef, dispatch, indexedDB);
}, [indexedDB]); }, [indexedDB]);
const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[]): Promise<void> => { const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise<void> => {
return undoConsumeFiles(inputFiles, inputWorkbenchFiles, outputFileIds, stateRef, filesRef, dispatch, indexedDB); return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, stateRef, filesRef, dispatch, indexedDB);
}, [indexedDB]); }, [indexedDB]);
// Helper to find FileId from File object // Helper to find FileId from File object
@ -170,8 +170,8 @@ function FileContextInner({
} }
} }
}, },
updateWorkbenchFile: (fileId: FileId, updates: Partial<WorkbenchFile>) => updateStirlingFileStub: (fileId: FileId, updates: Partial<StirlingFileStub>) =>
lifecycleManager.updateWorkbenchFile(fileId, updates, stateRef), lifecycleManager.updateStirlingFileStub(fileId, updates, stateRef),
reorderFiles: (orderedFileIds: FileId[]) => { reorderFiles: (orderedFileIds: FileId[]) => {
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } }); dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
}, },
@ -295,7 +295,7 @@ export {
useFileSelection, useFileSelection,
useFileManagement, useFileManagement,
useFileUI, useFileUI,
useWorkbenchFile, useStirlingFileStub,
useAllFiles, useAllFiles,
useSelectedFiles, useSelectedFiles,
// Primary API hooks for tools // Primary API hooks for tools

View File

@ -6,7 +6,7 @@ import { FileId } from '../../types/file';
import { import {
FileContextState, FileContextState,
FileContextAction, FileContextAction,
WorkbenchFile StirlingFileStub
} from '../../types/fileContext'; } from '../../types/fileContext';
// Initial state // Initial state
@ -29,7 +29,7 @@ export const initialFileContextState: FileContextState = {
function processFileSwap( function processFileSwap(
state: FileContextState, state: FileContextState,
filesToRemove: FileId[], filesToRemove: FileId[],
filesToAdd: WorkbenchFile[] filesToAdd: StirlingFileStub[]
): FileContextState { ): FileContextState {
// Only remove unpinned files // Only remove unpinned files
const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id)); const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id));
@ -70,11 +70,11 @@ function processFileSwap(
export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState { export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState {
switch (action.type) { switch (action.type) {
case 'ADD_FILES': { case 'ADD_FILES': {
const { workbenchFiles } = action.payload; const { stirlingFileStubs } = action.payload;
const newIds: FileId[] = []; const newIds: FileId[] = [];
const newById: Record<FileId, WorkbenchFile> = { ...state.files.byId }; const newById: Record<FileId, StirlingFileStub> = { ...state.files.byId };
workbenchFiles.forEach(record => { stirlingFileStubs.forEach(record => {
// Only add if not already present (dedupe by stable ID) // Only add if not already present (dedupe by stable ID)
if (!newById[record.id]) { if (!newById[record.id]) {
newIds.push(record.id); newIds.push(record.id);
@ -233,13 +233,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
} }
case 'CONSUME_FILES': { case 'CONSUME_FILES': {
const { inputFileIds, outputWorkbenchFiles } = action.payload; const { inputFileIds, outputStirlingFileStubs } = action.payload;
return processFileSwap(state, inputFileIds, outputWorkbenchFiles); return processFileSwap(state, inputFileIds, outputStirlingFileStubs);
} }
case 'UNDO_CONSUME_FILES': { case 'UNDO_CONSUME_FILES': {
const { inputWorkbenchFiles, outputFileIds } = action.payload; const { inputStirlingFileStubs, outputFileIds } = action.payload;
return processFileSwap(state, outputFileIds, inputWorkbenchFiles); return processFileSwap(state, outputFileIds, inputStirlingFileStubs);
} }
case 'RESET_CONTEXT': { case 'RESET_CONTEXT': {

View File

@ -3,7 +3,7 @@
*/ */
import { import {
WorkbenchFile, StirlingFileStub,
FileContextAction, FileContextAction,
FileContextState, FileContextState,
toWorkbenchFile, toWorkbenchFile,
@ -109,7 +109,7 @@ export async function addFiles(
await addFilesMutex.lock(); await addFilesMutex.lock();
try { try {
const workbenchFiles: WorkbenchFile[] = []; const stirlingFileStubs: StirlingFileStub[] = [];
const addedFiles: AddedFile[] = []; const addedFiles: AddedFile[] = [];
// Build quickKey lookup from existing files for deduplication // Build quickKey lookup from existing files for deduplication
@ -184,7 +184,7 @@ export async function addFiles(
} }
existingQuickKeys.add(quickKey); existingQuickKeys.add(quickKey);
workbenchFiles.push(record); stirlingFileStubs.push(record);
addedFiles.push({ file, id: fileId, thumbnail }); addedFiles.push({ file, id: fileId, thumbnail });
} }
break; break;
@ -226,7 +226,7 @@ export async function addFiles(
} }
existingQuickKeys.add(quickKey); existingQuickKeys.add(quickKey);
workbenchFiles.push(record); stirlingFileStubs.push(record);
addedFiles.push({ file, id: fileId, thumbnail }); addedFiles.push({ file, id: fileId, thumbnail });
} }
break; break;
@ -301,7 +301,7 @@ export async function addFiles(
} }
existingQuickKeys.add(quickKey); existingQuickKeys.add(quickKey);
workbenchFiles.push(record); stirlingFileStubs.push(record);
addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail }); addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail });
} }
@ -310,9 +310,9 @@ export async function addFiles(
} }
// Dispatch ADD_FILES action if we have new files // Dispatch ADD_FILES action if we have new files
if (workbenchFiles.length > 0) { if (stirlingFileStubs.length > 0) {
dispatch({ type: 'ADD_FILES', payload: { workbenchFiles } }); dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs } });
if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${workbenchFiles.length} files`); if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${stirlingFileStubs.length} files`);
} }
return addedFiles; return addedFiles;
@ -328,7 +328,7 @@ export async function addFiles(
async function processFilesIntoRecords( async function processFilesIntoRecords(
files: File[], files: File[],
filesRef: React.MutableRefObject<Map<FileId, File>> filesRef: React.MutableRefObject<Map<FileId, File>>
): Promise<Array<{ record: WorkbenchFile; file: File; fileId: FileId; thumbnail?: string }>> { ): Promise<Array<{ record: StirlingFileStub; file: File; fileId: FileId; thumbnail?: string }>> {
return Promise.all( return Promise.all(
files.map(async (file) => { files.map(async (file) => {
const fileId = createFileId(); const fileId = createFileId();
@ -365,10 +365,10 @@ async function processFilesIntoRecords(
* Helper function to persist files to IndexedDB * Helper function to persist files to IndexedDB
*/ */
async function persistFilesToIndexedDB( async function persistFilesToIndexedDB(
workbenchFiles: Array<{ file: File; fileId: FileId; thumbnail?: string }>, stirlingFileStubs: Array<{ file: File; fileId: FileId; thumbnail?: string }>,
indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> } indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> }
): Promise<void> { ): Promise<void> {
await Promise.all(workbenchFiles.map(async ({ file, fileId, thumbnail }) => { await Promise.all(stirlingFileStubs.map(async ({ file, fileId, thumbnail }) => {
try { try {
await indexedDB.saveFile(file, fileId, thumbnail); await indexedDB.saveFile(file, fileId, thumbnail);
} catch (error) { } catch (error) {
@ -390,11 +390,11 @@ export async function consumeFiles(
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`); if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`);
// Process output files with thumbnails and metadata // Process output files with thumbnails and metadata
const outputWorkbenchFiles = await processFilesIntoRecords(outputFiles, filesRef); const outputStirlingFileStubs = await processFilesIntoRecords(outputFiles, filesRef);
// Persist output files to IndexedDB if available // Persist output files to IndexedDB if available
if (indexedDB) { if (indexedDB) {
await persistFilesToIndexedDB(outputWorkbenchFiles, indexedDB); await persistFilesToIndexedDB(outputStirlingFileStubs, indexedDB);
} }
// Dispatch the consume action // Dispatch the consume action
@ -402,21 +402,21 @@ export async function consumeFiles(
type: 'CONSUME_FILES', type: 'CONSUME_FILES',
payload: { payload: {
inputFileIds, inputFileIds,
outputWorkbenchFiles: outputWorkbenchFiles.map(({ record }) => record) outputStirlingFileStubs: outputStirlingFileStubs.map(({ record }) => record)
} }
}); });
if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputWorkbenchFiles.length} outputs`); if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputStirlingFileStubs.length} outputs`);
// Return the output file IDs for undo tracking // Return the output file IDs for undo tracking
return outputWorkbenchFiles.map(({ fileId }) => fileId); return outputStirlingFileStubs.map(({ fileId }) => fileId);
} }
/** /**
* Helper function to restore files to filesRef and manage IndexedDB cleanup * Helper function to restore files to filesRef and manage IndexedDB cleanup
*/ */
async function restoreFilesAndCleanup( async function restoreFilesAndCleanup(
filesToRestore: Array<{ file: File; record: WorkbenchFile }>, filesToRestore: Array<{ file: File; record: StirlingFileStub }>,
fileIdsToRemove: FileId[], fileIdsToRemove: FileId[],
filesRef: React.MutableRefObject<Map<FileId, File>>, filesRef: React.MutableRefObject<Map<FileId, File>>,
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
@ -465,18 +465,18 @@ async function restoreFilesAndCleanup(
*/ */
export async function undoConsumeFiles( export async function undoConsumeFiles(
inputFiles: File[], inputFiles: File[],
inputWorkbenchFiles: WorkbenchFile[], inputStirlingFileStubs: StirlingFileStub[],
outputFileIds: FileId[], outputFileIds: FileId[],
stateRef: React.MutableRefObject<FileContextState>, stateRef: React.MutableRefObject<FileContextState>,
filesRef: React.MutableRefObject<Map<FileId, File>>, filesRef: React.MutableRefObject<Map<FileId, File>>,
dispatch: React.Dispatch<FileContextAction>, dispatch: React.Dispatch<FileContextAction>,
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
): Promise<void> { ): Promise<void> {
if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputWorkbenchFiles.length} input files, removing ${outputFileIds.length} output files`); if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputStirlingFileStubs.length} input files, removing ${outputFileIds.length} output files`);
// Validate inputs // Validate inputs
if (inputFiles.length !== inputWorkbenchFiles.length) { if (inputFiles.length !== inputStirlingFileStubs.length) {
throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputWorkbenchFiles.length})`); throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputStirlingFileStubs.length})`);
} }
// Create a backup of current filesRef state for rollback // Create a backup of current filesRef state for rollback
@ -486,7 +486,7 @@ export async function undoConsumeFiles(
// Prepare files to restore // Prepare files to restore
const filesToRestore = inputFiles.map((file, index) => ({ const filesToRestore = inputFiles.map((file, index) => ({
file, file,
record: inputWorkbenchFiles[index] record: inputStirlingFileStubs[index]
})); }));
// Restore input files and clean up output files // Restore input files and clean up output files
@ -501,12 +501,12 @@ export async function undoConsumeFiles(
dispatch({ dispatch({
type: 'UNDO_CONSUME_FILES', type: 'UNDO_CONSUME_FILES',
payload: { payload: {
inputWorkbenchFiles, inputStirlingFileStubs,
outputFileIds outputFileIds
} }
}); });
if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputWorkbenchFiles.length} inputs, removed ${outputFileIds.length} outputs`); if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputStirlingFileStubs.length} inputs, removed ${outputFileIds.length} outputs`);
} catch (error) { } catch (error) {
// Rollback filesRef to previous state // Rollback filesRef to previous state

View File

@ -9,7 +9,7 @@ import {
FileContextStateValue, FileContextStateValue,
FileContextActionsValue FileContextActionsValue
} from './contexts'; } from './contexts';
import { WorkbenchFile, StirlingFile } from '../../types/fileContext'; import { StirlingFileStub, StirlingFile } from '../../types/fileContext';
import { FileId } from '../../types/file'; import { FileId } from '../../types/file';
/** /**
@ -38,13 +38,13 @@ export function useFileActions(): FileContextActionsValue {
/** /**
* Hook for current/primary file (first in list) * Hook for current/primary file (first in list)
*/ */
export function useCurrentFile(): { file?: File; record?: WorkbenchFile } { export function useCurrentFile(): { file?: File; record?: StirlingFileStub } {
const { state, selectors } = useFileState(); const { state, selectors } = useFileState();
const primaryFileId = state.files.ids[0]; const primaryFileId = state.files.ids[0];
return useMemo(() => ({ return useMemo(() => ({
file: primaryFileId ? selectors.getFile(primaryFileId) : undefined, file: primaryFileId ? selectors.getFile(primaryFileId) : undefined,
record: primaryFileId ? selectors.getWorkbenchFile(primaryFileId) : undefined record: primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : undefined
}), [primaryFileId, selectors]); }), [primaryFileId, selectors]);
} }
@ -87,7 +87,7 @@ export function useFileManagement() {
addFiles: actions.addFiles, addFiles: actions.addFiles,
removeFiles: actions.removeFiles, removeFiles: actions.removeFiles,
clearAllFiles: actions.clearAllFiles, clearAllFiles: actions.clearAllFiles,
updateWorkbenchFile: actions.updateWorkbenchFile, updateStirlingFileStub: actions.updateStirlingFileStub,
reorderFiles: actions.reorderFiles reorderFiles: actions.reorderFiles
}), [actions]); }), [actions]);
} }
@ -111,24 +111,24 @@ export function useFileUI() {
/** /**
* Hook for specific file by ID (optimized for individual file access) * Hook for specific file by ID (optimized for individual file access)
*/ */
export function useWorkbenchFile(fileId: FileId): { file?: File; record?: WorkbenchFile } { export function useStirlingFileStub(fileId: FileId): { file?: File; record?: StirlingFileStub } {
const { selectors } = useFileState(); const { selectors } = useFileState();
return useMemo(() => ({ return useMemo(() => ({
file: selectors.getFile(fileId), file: selectors.getFile(fileId),
record: selectors.getWorkbenchFile(fileId) record: selectors.getStirlingFileStub(fileId)
}), [fileId, selectors]); }), [fileId, selectors]);
} }
/** /**
* Hook for all files (use sparingly - causes re-renders on file list changes) * Hook for all files (use sparingly - causes re-renders on file list changes)
*/ */
export function useAllFiles(): { files: StirlingFile[]; records: WorkbenchFile[]; fileIds: FileId[] } { export function useAllFiles(): { files: StirlingFile[]; records: StirlingFileStub[]; fileIds: FileId[] } {
const { state, selectors } = useFileState(); const { state, selectors } = useFileState();
return useMemo(() => ({ return useMemo(() => ({
files: selectors.getFiles(), files: selectors.getFiles(),
records: selectors.getWorkbenchFiles(), records: selectors.getStirlingFileStubs(),
fileIds: state.files.ids fileIds: state.files.ids
}), [state.files.ids, selectors]); }), [state.files.ids, selectors]);
} }
@ -136,12 +136,12 @@ export function useAllFiles(): { files: StirlingFile[]; records: WorkbenchFile[]
/** /**
* Hook for selected files (optimized for selection-based UI) * Hook for selected files (optimized for selection-based UI)
*/ */
export function useSelectedFiles(): { files: StirlingFile[]; records: WorkbenchFile[]; fileIds: FileId[] } { export function useSelectedFiles(): { files: StirlingFile[]; records: StirlingFileStub[]; fileIds: FileId[] } {
const { state, selectors } = useFileState(); const { state, selectors } = useFileState();
return useMemo(() => ({ return useMemo(() => ({
files: selectors.getSelectedFiles(), files: selectors.getSelectedFiles(),
records: selectors.getSelectedWorkbenchFiles(), records: selectors.getSelectedStirlingFileStubs(),
fileIds: state.ui.selectedFileIds fileIds: state.ui.selectedFileIds
}), [state.ui.selectedFileIds, selectors]); }), [state.ui.selectedFileIds, selectors]);
} }

View File

@ -4,7 +4,7 @@
import { FileId } from '../../types/file'; import { FileId } from '../../types/file';
import { import {
WorkbenchFile, StirlingFileStub,
FileContextState, FileContextState,
FileContextSelectors, FileContextSelectors,
StirlingFile, StirlingFile,
@ -34,9 +34,9 @@ export function createFileSelectors(
.filter(Boolean) as StirlingFile[]; .filter(Boolean) as StirlingFile[];
}, },
getWorkbenchFile: (id: FileId) => stateRef.current.files.byId[id], getStirlingFileStub: (id: FileId) => stateRef.current.files.byId[id],
getWorkbenchFiles: (ids?: FileId[]) => { getStirlingFileStubs: (ids?: FileId[]) => {
const currentIds = ids || stateRef.current.files.ids; const currentIds = ids || stateRef.current.files.ids;
return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean); return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean);
}, },
@ -52,7 +52,7 @@ export function createFileSelectors(
.filter(Boolean) as StirlingFile[]; .filter(Boolean) as StirlingFile[];
}, },
getSelectedWorkbenchFiles: () => { getSelectedStirlingFileStubs: () => {
return stateRef.current.ui.selectedFileIds return stateRef.current.ui.selectedFileIds
.map(id => stateRef.current.files.byId[id]) .map(id => stateRef.current.files.byId[id])
.filter(Boolean); .filter(Boolean);
@ -72,7 +72,7 @@ export function createFileSelectors(
.filter(Boolean) as StirlingFile[]; .filter(Boolean) as StirlingFile[];
}, },
getPinnedWorkbenchFiles: () => { getPinnedStirlingFileStubs: () => {
return Array.from(stateRef.current.pinnedFiles) return Array.from(stateRef.current.pinnedFiles)
.map(id => stateRef.current.files.byId[id]) .map(id => stateRef.current.files.byId[id])
.filter(Boolean); .filter(Boolean);
@ -98,9 +98,9 @@ export function createFileSelectors(
/** /**
* Helper for building quickKey sets for deduplication * Helper for building quickKey sets for deduplication
*/ */
export function buildQuickKeySet(workbenchFiles: Record<FileId, WorkbenchFile>): Set<string> { export function buildQuickKeySet(stirlingFileStubs: Record<FileId, StirlingFileStub>): Set<string> {
const quickKeys = new Set<string>(); const quickKeys = new Set<string>();
Object.values(workbenchFiles).forEach(record => { Object.values(stirlingFileStubs).forEach(record => {
if (record.quickKey) { if (record.quickKey) {
quickKeys.add(record.quickKey); quickKeys.add(record.quickKey);
} }
@ -127,7 +127,7 @@ export function buildQuickKeySetFromMetadata(metadata: Array<{ name: string; siz
export function getPrimaryFile( export function getPrimaryFile(
stateRef: React.MutableRefObject<FileContextState>, stateRef: React.MutableRefObject<FileContextState>,
filesRef: React.MutableRefObject<Map<FileId, File>> filesRef: React.MutableRefObject<Map<FileId, File>>
): { file?: File; record?: WorkbenchFile } { ): { file?: File; record?: StirlingFileStub } {
const primaryFileId = stateRef.current.files.ids[0]; const primaryFileId = stateRef.current.files.ids[0];
if (!primaryFileId) return {}; if (!primaryFileId) return {};

View File

@ -3,7 +3,7 @@
*/ */
import { FileId } from '../../types/file'; import { FileId } from '../../types/file';
import { FileContextAction, WorkbenchFile, ProcessedFilePage } from '../../types/fileContext'; import { FileContextAction, StirlingFileStub, ProcessedFilePage } from '../../types/fileContext';
const DEBUG = process.env.NODE_ENV === 'development'; const DEBUG = process.env.NODE_ENV === 'development';
@ -166,7 +166,7 @@ export class FileLifecycleManager {
/** /**
* Update file record with race condition guards * Update file record with race condition guards
*/ */
updateWorkbenchFile = (fileId: FileId, updates: Partial<WorkbenchFile>, stateRef?: React.MutableRefObject<any>): void => { updateStirlingFileStub = (fileId: FileId, updates: Partial<StirlingFileStub>, stateRef?: React.MutableRefObject<any>): void => {
// Guard against updating removed files (race condition protection) // Guard against updating removed files (race condition protection)
if (!this.filesRef.current.has(fileId)) { if (!this.filesRef.current.has(fileId)) {
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`); if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);

View File

@ -6,7 +6,7 @@ import { useToolState, type ProcessingProgress } from './useToolState';
import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls'; import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
import { useToolResources } from './useToolResources'; import { useToolResources } from './useToolResources';
import { extractErrorMessage } from '../../../utils/toolErrorHandler'; import { extractErrorMessage } from '../../../utils/toolErrorHandler';
import { StirlingFile, extractFiles, FileId, WorkbenchFile } from '../../../types/fileContext'; import { StirlingFile, extractFiles, FileId, StirlingFileStub } from '../../../types/fileContext';
import { ResponseHandler } from '../../../utils/toolResponseProcessor'; import { ResponseHandler } from '../../../utils/toolResponseProcessor';
// Re-export for backwards compatibility // Re-export for backwards compatibility
@ -138,7 +138,7 @@ export const useToolOperation = <TParams>(
// Track last operation for undo functionality // Track last operation for undo functionality
const lastOperationRef = useRef<{ const lastOperationRef = useRef<{
inputFiles: File[]; inputFiles: File[];
inputWorkbenchFiles: WorkbenchFile[]; inputStirlingFileStubs: StirlingFileStub[];
outputFileIds: FileId[]; outputFileIds: FileId[];
} | null>(null); } | null>(null);
@ -240,15 +240,15 @@ export const useToolOperation = <TParams>(
// Replace input files with processed files (consumeFiles handles pinning) // Replace input files with processed files (consumeFiles handles pinning)
const inputFileIds: FileId[] = []; const inputFileIds: FileId[] = [];
const inputWorkbenchFiles: WorkbenchFile[] = []; const inputStirlingFileStubs: StirlingFileStub[] = [];
// Build parallel arrays of IDs and records for undo tracking // Build parallel arrays of IDs and records for undo tracking
for (const file of validFiles) { for (const file of validFiles) {
const fileId = file.fileId; const fileId = file.fileId;
const record = selectors.getWorkbenchFile(fileId); const record = selectors.getStirlingFileStub(fileId);
if (record) { if (record) {
inputFileIds.push(fileId); inputFileIds.push(fileId);
inputWorkbenchFiles.push(record); inputStirlingFileStubs.push(record);
} else { } else {
console.warn(`No file record found for file: ${file.name}`); console.warn(`No file record found for file: ${file.name}`);
} }
@ -259,7 +259,7 @@ export const useToolOperation = <TParams>(
// Store operation data for undo (only store what we need to avoid memory bloat) // Store operation data for undo (only store what we need to avoid memory bloat)
lastOperationRef.current = { lastOperationRef.current = {
inputFiles: extractFiles(validFiles), // Convert to File objects for undo inputFiles: extractFiles(validFiles), // Convert to File objects for undo
inputWorkbenchFiles: inputWorkbenchFiles.map(record => ({ ...record })), // Deep copy to avoid reference issues inputStirlingFileStubs: inputStirlingFileStubs.map(record => ({ ...record })), // Deep copy to avoid reference issues
outputFileIds outputFileIds
}; };
@ -302,10 +302,10 @@ export const useToolOperation = <TParams>(
return; return;
} }
const { inputFiles, inputWorkbenchFiles, outputFileIds } = lastOperationRef.current; const { inputFiles, inputStirlingFileStubs, outputFileIds } = lastOperationRef.current;
// Validate that we have data to undo // Validate that we have data to undo
if (inputFiles.length === 0 || inputWorkbenchFiles.length === 0) { if (inputFiles.length === 0 || inputStirlingFileStubs.length === 0) {
actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data')); actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data'));
return; return;
} }
@ -317,7 +317,7 @@ export const useToolOperation = <TParams>(
try { try {
// Undo the consume operation // Undo the consume operation
await undoConsumeFiles(inputFiles, inputWorkbenchFiles, outputFileIds); await undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds);
// Clear results and operation tracking // Clear results and operation tracking
resetResults(); resetResults();

View File

@ -44,25 +44,32 @@ export interface ProcessedFileMetadata {
[key: string]: any; [key: string]: any;
} }
export interface WorkbenchFile { /**
id: FileId; * StirlingFileStub - Metadata record for files in the active workbench session
name: string; *
size: number; * Contains UI display data and processing state. Actual File objects stored
type: string; * separately in refs for memory efficiency. Supports multi-tool workflows
lastModified: number; * where files persist across tool operations.
*/
export interface StirlingFileStub {
id: FileId; // UUID primary key for collision-free operations
name: string; // Display name for UI
size: number; // File size for progress indicators
type: string; // MIME type for format validation
lastModified: number; // Original timestamp for deduplication
quickKey?: string; // Fast deduplication key: name|size|lastModified quickKey?: string; // Fast deduplication key: name|size|lastModified
thumbnailUrl?: string; thumbnailUrl?: string; // Generated thumbnail blob URL for visual display
blobUrl?: string; blobUrl?: string; // File access blob URL for downloads/processing
createdAt?: number; createdAt?: number; // When added to workbench for sorting
processedFile?: ProcessedFileMetadata; processedFile?: ProcessedFileMetadata; // PDF page data and processing results
insertAfterPageId?: string; // Page ID after which this file should be inserted insertAfterPageId?: string; // Page ID after which this file should be inserted
isPinned?: boolean; isPinned?: boolean; // Protected from tool consumption (replace/remove)
// Note: File object stored in provider ref, not in state // Note: File object stored in provider ref, not in state
} }
export interface FileContextNormalizedFiles { export interface FileContextNormalizedFiles {
ids: FileId[]; ids: FileId[];
byId: Record<FileId, WorkbenchFile>; byId: Record<FileId, StirlingFileStub>;
} }
// Helper functions - UUID-based primary keys (zero collisions, synchronous) // Helper functions - UUID-based primary keys (zero collisions, synchronous)
@ -143,7 +150,7 @@ export function isFileObject(obj: any): obj is File | StirlingFile {
export function toWorkbenchFile(file: File, id?: FileId): WorkbenchFile { export function toWorkbenchFile(file: File, id?: FileId): StirlingFileStub {
const fileId = id || createFileId(); const fileId = id || createFileId();
return { return {
id: fileId, id: fileId,
@ -156,7 +163,7 @@ export function toWorkbenchFile(file: File, id?: FileId): WorkbenchFile {
}; };
} }
export function revokeFileResources(record: WorkbenchFile): void { export function revokeFileResources(record: StirlingFileStub): void {
// Only revoke blob: URLs to prevent errors on other schemes // Only revoke blob: URLs to prevent errors on other schemes
if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) { if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) {
try { try {
@ -230,7 +237,7 @@ export interface FileContextState {
// Core file management - lightweight file IDs only // Core file management - lightweight file IDs only
files: { files: {
ids: FileId[]; ids: FileId[];
byId: Record<FileId, WorkbenchFile>; byId: Record<FileId, StirlingFileStub>;
}; };
// Pinned files - files that won't be consumed by tools // Pinned files - files that won't be consumed by tools
@ -249,16 +256,16 @@ export interface FileContextState {
// Action types for reducer pattern // Action types for reducer pattern
export type FileContextAction = export type FileContextAction =
// File management actions // File management actions
| { type: 'ADD_FILES'; payload: { workbenchFiles: WorkbenchFile[] } } | { type: 'ADD_FILES'; payload: { stirlingFileStubs: StirlingFileStub[] } }
| { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } } | { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } }
| { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial<WorkbenchFile> } } | { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial<StirlingFileStub> } }
| { type: 'REORDER_FILES'; payload: { orderedFileIds: FileId[] } } | { type: 'REORDER_FILES'; payload: { orderedFileIds: FileId[] } }
// Pinned files actions // Pinned files actions
| { type: 'PIN_FILE'; payload: { fileId: FileId } } | { type: 'PIN_FILE'; payload: { fileId: FileId } }
| { type: 'UNPIN_FILE'; payload: { fileId: FileId } } | { type: 'UNPIN_FILE'; payload: { fileId: FileId } }
| { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputWorkbenchFiles: WorkbenchFile[] } } | { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputStirlingFileStubs: StirlingFileStub[] } }
| { type: 'UNDO_CONSUME_FILES'; payload: { inputWorkbenchFiles: WorkbenchFile[]; outputFileIds: FileId[] } } | { type: 'UNDO_CONSUME_FILES'; payload: { inputStirlingFileStubs: StirlingFileStub[]; outputFileIds: FileId[] } }
// UI actions // UI actions
| { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } } | { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } }
@ -278,7 +285,7 @@ export interface FileContextActions {
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<StirlingFile[]>; addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<StirlingFile[]>;
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<StirlingFile[]>; addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<StirlingFile[]>;
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>; removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
updateWorkbenchFile: (id: FileId, updates: Partial<WorkbenchFile>) => void; updateStirlingFileStub: (id: FileId, updates: Partial<StirlingFileStub>) => void;
reorderFiles: (orderedFileIds: FileId[]) => void; reorderFiles: (orderedFileIds: FileId[]) => void;
clearAllFiles: () => Promise<void>; clearAllFiles: () => Promise<void>;
clearAllData: () => Promise<void>; clearAllData: () => Promise<void>;
@ -289,7 +296,7 @@ export interface FileContextActions {
// File consumption (replace unpinned files with outputs) // File consumption (replace unpinned files with outputs)
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>; consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>;
undoConsumeFiles: (inputFiles: File[], inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[]) => Promise<void>; undoConsumeFiles: (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]) => Promise<void>;
// Selection management // Selection management
setSelectedFiles: (fileIds: FileId[]) => void; setSelectedFiles: (fileIds: FileId[]) => void;
setSelectedPages: (pageNumbers: number[]) => void; setSelectedPages: (pageNumbers: number[]) => void;
@ -314,14 +321,14 @@ export interface FileContextActions {
export interface FileContextSelectors { export interface FileContextSelectors {
getFile: (id: FileId) => StirlingFile | undefined; getFile: (id: FileId) => StirlingFile | undefined;
getFiles: (ids?: FileId[]) => StirlingFile[]; getFiles: (ids?: FileId[]) => StirlingFile[];
getWorkbenchFile: (id: FileId) => WorkbenchFile | undefined; getStirlingFileStub: (id: FileId) => StirlingFileStub | undefined;
getWorkbenchFiles: (ids?: FileId[]) => WorkbenchFile[]; getStirlingFileStubs: (ids?: FileId[]) => StirlingFileStub[];
getAllFileIds: () => FileId[]; getAllFileIds: () => FileId[];
getSelectedFiles: () => StirlingFile[]; getSelectedFiles: () => StirlingFile[];
getSelectedWorkbenchFiles: () => WorkbenchFile[]; getSelectedStirlingFileStubs: () => StirlingFileStub[];
getPinnedFileIds: () => FileId[]; getPinnedFileIds: () => FileId[];
getPinnedFiles: () => StirlingFile[]; getPinnedFiles: () => StirlingFile[];
getPinnedWorkbenchFiles: () => WorkbenchFile[]; getPinnedStirlingFileStubs: () => StirlingFileStub[];
isFilePinned: (file: StirlingFile) => boolean; isFilePinned: (file: StirlingFile) => boolean;
getFilesSignature: () => string; getFilesSignature: () => string;
} }