From 8e5a01df6d6182681b28bb4b9fe14fe4a4832fe5 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 21 Apr 2026 07:46:18 +0200 Subject: [PATCH] fixes --- src/pages/views/workspace/WorkspaceInput.tsx | 31 +++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/pages/views/workspace/WorkspaceInput.tsx b/src/pages/views/workspace/WorkspaceInput.tsx index 6444c9a..5d59d26 100644 --- a/src/pages/views/workspace/WorkspaceInput.tsx +++ b/src/pages/views/workspace/WorkspaceInput.tsx @@ -138,8 +138,8 @@ export const WorkspaceInput: React.FC = ({ instanceId, // Rehydrate the chip-bar whenever the parent re-loads a chat (loadedNonce // bumps on every loadWorkflow call). We trust the loaded IDs initially; - // a separate effect below drops IDs that don't resolve once the source - // lists have arrived from the backend. + // a separate one-shot reconciliation below drops IDs that don't resolve + // once the source lists have arrived from the backend. useEffect(() => { if (loadedNonce === undefined) return; setAttachedFileIds([]); @@ -149,25 +149,40 @@ export const WorkspaceInput: React.FC = ({ instanceId, // Drop persisted attachment IDs that no longer resolve to an existing // source (e.g. the DataSource was deleted while the chat was closed). - // We only run this once the lists are populated to avoid wiping chips - // before the lists have loaded. + // + // CRITICAL: this MUST run only once per chat-load (per `loadedNonce`), + // and only after the source lists have actually arrived. A continuous + // filter would race with `_handleDataSourceDrop` / + // `_handleSendToChat_FeatureSource` in the parent: the drop sets the + // chip via `pendingAttachDsId` *before* `refreshDataSources()` has + // returned, so a continuous filter would briefly evict the freshly + // dropped ID and the chip would visibly flash in and out. + const _reconciledDsForNonce = useRef(undefined); + const _reconciledFdsForNonce = useRef(undefined); + useEffect(() => { - if (dataSources.length === 0 && attachedDataSourceIds.length === 0) return; + if (loadedNonce === undefined) return; + if (_reconciledDsForNonce.current === loadedNonce) return; + if (dataSources.length === 0) return; // wait for the list to arrive + _reconciledDsForNonce.current = loadedNonce; const validIds = new Set(dataSources.map(d => d.id)); setAttachedDataSourceIds(prev => { const filtered = prev.filter(id => validIds.has(id)); return filtered.length === prev.length ? prev : filtered; }); - }, [dataSources, attachedDataSourceIds.length]); + }, [loadedNonce, dataSources]); useEffect(() => { - if (featureDataSources.length === 0 && attachedFeatureDataSourceIds.length === 0) return; + if (loadedNonce === undefined) return; + if (_reconciledFdsForNonce.current === loadedNonce) return; + if (featureDataSources.length === 0) return; + _reconciledFdsForNonce.current = loadedNonce; const validIds = new Set(featureDataSources.map(d => d.id)); setAttachedFeatureDataSourceIds(prev => { const filtered = prev.filter(id => validIds.has(id)); return filtered.length === prev.length ? prev : filtered; }); - }, [featureDataSources, attachedFeatureDataSourceIds.length]); + }, [loadedNonce, featureDataSources]); // Persist a changed attachment list to the backend so the next chat // reload reflects the current state. We debounce slightly by sending on