This commit is contained in:
ValueOn AG 2026-04-21 07:46:18 +02:00
parent b4574b6a2e
commit 8e5a01df6d

View file

@ -138,8 +138,8 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId,
// Rehydrate the chip-bar whenever the parent re-loads a chat (loadedNonce // Rehydrate the chip-bar whenever the parent re-loads a chat (loadedNonce
// bumps on every loadWorkflow call). We trust the loaded IDs initially; // bumps on every loadWorkflow call). We trust the loaded IDs initially;
// a separate effect below drops IDs that don't resolve once the source // a separate one-shot reconciliation below drops IDs that don't resolve
// lists have arrived from the backend. // once the source lists have arrived from the backend.
useEffect(() => { useEffect(() => {
if (loadedNonce === undefined) return; if (loadedNonce === undefined) return;
setAttachedFileIds([]); setAttachedFileIds([]);
@ -149,25 +149,40 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId,
// Drop persisted attachment IDs that no longer resolve to an existing // Drop persisted attachment IDs that no longer resolve to an existing
// source (e.g. the DataSource was deleted while the chat was closed). // 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<number | undefined>(undefined);
const _reconciledFdsForNonce = useRef<number | undefined>(undefined);
useEffect(() => { 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)); const validIds = new Set(dataSources.map(d => d.id));
setAttachedDataSourceIds(prev => { setAttachedDataSourceIds(prev => {
const filtered = prev.filter(id => validIds.has(id)); const filtered = prev.filter(id => validIds.has(id));
return filtered.length === prev.length ? prev : filtered; return filtered.length === prev.length ? prev : filtered;
}); });
}, [dataSources, attachedDataSourceIds.length]); }, [loadedNonce, dataSources]);
useEffect(() => { 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)); const validIds = new Set(featureDataSources.map(d => d.id));
setAttachedFeatureDataSourceIds(prev => { setAttachedFeatureDataSourceIds(prev => {
const filtered = prev.filter(id => validIds.has(id)); const filtered = prev.filter(id => validIds.has(id));
return filtered.length === prev.length ? prev : filtered; return filtered.length === prev.length ? prev : filtered;
}); });
}, [featureDataSources, attachedFeatureDataSourceIds.length]); }, [loadedNonce, featureDataSources]);
// Persist a changed attachment list to the backend so the next chat // Persist a changed attachment list to the backend so the next chat
// reload reflects the current state. We debounce slightly by sending on // reload reflects the current state. We debounce slightly by sending on