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