From 56e8035b8a43124be6ef42f31d4ae52bc1f464ce Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Sun, 12 Jul 2026 13:38:24 +0700 Subject: [PATCH] feat(flows): surface partial container listings in the Pull dialog The container file endpoint now returns HTTP 200 with the readable entries plus a Failures list when some entries can't be stat'd, instead of failing the whole request. Plumb those failures through useFlowContainerFiles and show a non-blocking warning banner above the readable files ("N entries could not be read"), reserving the full error empty-state for a true listing failure. When nothing is readable, the empty-state says so rather than "Directory is empty". Co-Authored-By: Claude Opus 4.8 --- .../flows/files/flow-files-pull-dialog.tsx | 34 ++++++++++++++++++- .../features/flows/files/flow-files-utils.ts | 7 ++++ .../flows/files/use-flow-container-files.ts | 14 +++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/flows/files/flow-files-pull-dialog.tsx b/frontend/src/features/flows/files/flow-files-pull-dialog.tsx index 5e38fe7a..4332a6bc 100644 --- a/frontend/src/features/flows/files/flow-files-pull-dialog.tsx +++ b/frontend/src/features/flows/files/flow-files-pull-dialog.tsx @@ -1,4 +1,4 @@ -import { ArrowDownToLine, ArrowUp, FolderOpen, Loader2, RefreshCw } from 'lucide-react'; +import { ArrowDownToLine, ArrowUp, FolderOpen, Loader2, RefreshCw, TriangleAlert } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; import { @@ -8,6 +8,7 @@ import { type FileNode, } from '@/components/shared/file-manager'; import { OverwriteButtons, OverwriteDialog, useOverwrite } from '@/components/shared/overwrite'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Autocomplete, AutocompleteContent, @@ -133,6 +134,7 @@ function FlowFilesPullDialogForm({ cachedFiles, flowId, onClose, onSuccess }: Fl const { error: listingError, + failures: listingFailures, files, isLoading: isListingLoading, refetch: refetchListing, @@ -331,6 +333,18 @@ function FlowFilesPullDialogForm({ cachedFiles, flowId, onClose, onSuccess }: Fl {listingError.message} + ) : listingFailures.length > 0 ? ( + + + + + + Nothing readable here + + None of the entries in {currentPath} could be read. + + + ) : ( @@ -424,6 +438,24 @@ function FlowFilesPullDialogForm({ cachedFiles, flowId, onClose, onSuccess }: Fl + {listingFailures.length > 0 && flatFiles.length > 0 && ( + + + + {listingFailures.length} {listingFailures.length === 1 ? 'entry' : 'entries'} could not + be read + + + The readable entries are shown below. Skipped:{' '} + {listingFailures + .slice(0, 5) + .map((failure) => failure.name) + .join(', ')} + {listingFailures.length > 5 ? `, and ${listingFailures.length - 5} more` : ''}. + + + )} + ([]); + const [failures, setFailures] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -65,6 +73,7 @@ export function useFlowContainerFiles({ flowId, paths }: UseFlowContainerFilesPa if (!flowId || paths.length === 0) { currentTokenRef.current += 1; setFiles([]); + setFailures([]); setIsLoading(false); setError(null); @@ -89,6 +98,7 @@ export function useFlowContainerFiles({ flowId, paths }: UseFlowContainerFilesPa } setFiles(data.files.map(containerFileToFileNode)); + setFailures(data.failures ?? []); } catch (caught) { if (token !== currentTokenRef.current) { return; @@ -96,6 +106,7 @@ export function useFlowContainerFiles({ flowId, paths }: UseFlowContainerFilesPa setError(new Error(getApiErrorMessage(caught, 'Failed to load container files'))); setFiles([]); + setFailures([]); } finally { if (token === currentTokenRef.current) { setIsLoading(false); @@ -116,6 +127,7 @@ export function useFlowContainerFiles({ flowId, paths }: UseFlowContainerFilesPa return { error, + failures, files, isLoading, refetch: fetchListing,