From 4142ce7737e0d7c1b8e1d4eda6656f684be6bc93 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 10 Feb 2026 15:36:53 -0300 Subject: [PATCH 1/9] fix(AGENT-676): prevent quick setup from overwriting unsaved canvas When the credential setup modal runs on an open canvas, apply credentials directly to the live ReactFlow nodes and mark the canvas dirty instead of persisting to the backend (which would silently overwrite any unsaved node edits). - Read live canvas credentials via processFlowCredentials - Add skipRefetch flag so credential-updated event doesn't re-fetch - Sync AsyncDropdown internal value when external prop changes Co-Authored-By: Claude Opus 4.6 --- .../ui/src/components/SidekickSetupModal.jsx | 79 +++++++++++++++---- .../ui-component/dropdown/AsyncDropdown.jsx | 5 ++ packages/ui/src/views/agentflowsv2/Canvas.jsx | 2 +- packages/ui/src/views/canvas/index.jsx | 2 +- 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/packages/ui/src/components/SidekickSetupModal.jsx b/packages/ui/src/components/SidekickSetupModal.jsx index d5422efe464..6ff5e583e0f 100644 --- a/packages/ui/src/components/SidekickSetupModal.jsx +++ b/packages/ui/src/components/SidekickSetupModal.jsx @@ -1,5 +1,5 @@ 'use client' -import { useCallback, useState, useEffect } from 'react' +import { useCallback, useState, useEffect, useContext } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import { useDispatch } from 'react-redux' import dynamic from 'next/dynamic' @@ -9,12 +9,15 @@ import PropTypes from 'prop-types' import Button from '@mui/material/Button' // Redux notification imports -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_DIRTY } from '@/store/actions' import useNotifier from '@/utils/useNotifier' // UI components and utilities import { IconX } from '@tabler/icons-react' +import { flowContext } from '@/store/context/ReactFlowContext' +import { FLOWISE_CREDENTIAL_ID } from '@/store/constant' import { useSidekickWithCredentials } from '@/hooks/useSidekickWithCredentials' +import { processFlowCredentials } from '@utils/processFlowCredentials' import { updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' import { getCredentialModalDismissed, setCredentialModalDismissed } from '@/utils/credentialModalPreference' import useConfirm from '@/hooks/useConfirm' @@ -26,6 +29,7 @@ const ConfirmDialog = dynamic(() => import('@/ui-component/dialog/ConfirmDialog' const SidekickSetupModal = ({ sidekickId, onComplete }) => { const { confirm } = useConfirm() const preferenceScope = sidekickId ? `sidekick:${sidekickId}` : null + const { reactFlowInstance } = useContext(flowContext) // Local state to track if user has skipped setup for this instance const [hasSkipped, setHasSkipped] = useState(false) @@ -39,6 +43,9 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { // Fetch sidekick data with credentials const { sidekick, needsSetup, credentialsToShow, updateSidekick } = useSidekickWithCredentials(sidekickId, isQuickSetup) + const canvasNodes = reactFlowInstance?.getNodes() ?? [] + const liveCredentials = canvasNodes.length > 0 ? processFlowCredentials({ nodes: canvasNodes }).credentials : credentialsToShow + // Redux notification setup const dispatch = useDispatch() useNotifier() @@ -106,16 +113,48 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { async (credentialAssignments, options) => { try { if (credentialAssignments && Object.keys(credentialAssignments).length > 0) { - const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) - await updateSidekick({ - flowData: JSON.stringify(updatedFlowData) - }) - // Notify canvas and other listeners to refresh with updated credentials - window.dispatchEvent(new CustomEvent('credentials-updated', { detail: { chatflowId: sidekickId } })) - enqueueSnackbar({ - message: 'Credentials saved successfully!', - options: { variant: 'success' } - }) + if (reactFlowInstance) { + // Canvas path: apply credentials directly to live ReactFlow nodes + reactFlowInstance.setNodes((nodes) => + nodes.map((node) => { + if (credentialAssignments[node.id]) { + return { + ...node, + data: { + ...node.data, + credential: credentialAssignments[node.id], + inputs: { + ...node.data.inputs, + [FLOWISE_CREDENTIAL_ID]: credentialAssignments[node.id] + } + } + } + } + return node + }) + ) + dispatch({ type: SET_DIRTY }) + window.dispatchEvent( + new CustomEvent('credentials-updated', { + detail: { chatflowId: sidekickId, skipRefetch: true } + }) + ) + enqueueSnackbar({ + message: 'Credentials applied — save to persist', + options: { variant: 'success' } + }) + } else { + // Non-canvas path: save to backend via API + const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) + await updateSidekick({ + flowData: JSON.stringify(updatedFlowData) + }) + window.dispatchEvent(new CustomEvent('credentials-updated', { detail: { chatflowId: sidekickId } })) + enqueueSnackbar({ + message: 'Credentials saved successfully!', + options: { variant: 'success' } + }) + } } persistDismissPreference(options) handleURLCleanup() @@ -124,7 +163,17 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { handleModalError('Error assigning credentials. Please try again.') } }, - [sidekick, updateSidekick, enqueueSnackbar, handleURLCleanup, handleModalError, persistDismissPreference] + [ + sidekick, + reactFlowInstance, + updateSidekick, + dispatch, + enqueueSnackbar, + sidekickId, + handleURLCleanup, + handleModalError, + persistDismissPreference + ] ) const handleModalSkip = useCallback( @@ -161,7 +210,7 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { if (isQuickSetup) return false if (hasSkipped) return true if (isSuppressed) return true - return !needsSetup + return reactFlowInstance ? liveCredentials.length === 0 : !needsSetup } if (shouldHideModal()) { @@ -173,7 +222,7 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { (multiple ? [] : '') const addNewOption = [{ label: '- Create New -', name: '-create-' }] let [internalValue, setInternalValue] = useState(value ?? 'choose an option') + + useEffect(() => { + setInternalValue(value ?? 'choose an option') + }, [value]) + const { reactFlowInstance } = useContext(flowContext) const fetchCredentialList = async () => { diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index 350b58c6fd7..11ef11b1ecc 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -679,7 +679,7 @@ const AgentflowCanvas = ({ chatflowid: chatflowId }) => { // Re-fetch canvas when credentials are updated via the setup modal useEffect(() => { const handleCredentialsUpdated = (e) => { - if (e.detail?.chatflowId === chatflowId) { + if (e.detail?.chatflowId === chatflowId && !e.detail?.skipRefetch) { getSpecificChatflowApi.request(chatflowId) } } diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index eb648a65a35..c81bb2896a5 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -604,7 +604,7 @@ const Canvas = ({ chatflowid: chatflowId }) => { // Re-fetch canvas when credentials are updated via the setup modal useEffect(() => { const handleCredentialsUpdated = (e) => { - if (e.detail?.chatflowId === chatflowId) { + if (e.detail?.chatflowId === chatflowId && !e.detail?.skipRefetch) { getSpecificChatflowApi.request(chatflowId) } } From 0f164d539f68dc0d9e4c3a790f660ab8ebed2623 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 10 Feb 2026 15:51:45 -0300 Subject: [PATCH 2/9] fix(AGENT-676): address PR review feedback for race condition and null safety - AsyncDropdown: guard useEffect to skip redundant setInternalValue calls - SidekickSetupModal: add optional chaining on processFlowCredentials result Co-Authored-By: Claude Opus 4.6 --- packages/ui/src/components/SidekickSetupModal.jsx | 3 ++- packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/SidekickSetupModal.jsx b/packages/ui/src/components/SidekickSetupModal.jsx index 6ff5e583e0f..7e147a2d88b 100644 --- a/packages/ui/src/components/SidekickSetupModal.jsx +++ b/packages/ui/src/components/SidekickSetupModal.jsx @@ -44,7 +44,8 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { const { sidekick, needsSetup, credentialsToShow, updateSidekick } = useSidekickWithCredentials(sidekickId, isQuickSetup) const canvasNodes = reactFlowInstance?.getNodes() ?? [] - const liveCredentials = canvasNodes.length > 0 ? processFlowCredentials({ nodes: canvasNodes }).credentials : credentialsToShow + const liveCredentials = + canvasNodes.length > 0 ? processFlowCredentials({ nodes: canvasNodes })?.credentials ?? credentialsToShow : credentialsToShow // Redux notification setup const dispatch = useDispatch() diff --git a/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx b/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx index bf1a06e326d..ff29694509c 100644 --- a/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx +++ b/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx @@ -96,7 +96,8 @@ export const AsyncDropdown = ({ let [internalValue, setInternalValue] = useState(value ?? 'choose an option') useEffect(() => { - setInternalValue(value ?? 'choose an option') + const next = value ?? 'choose an option' + if (next !== internalValue) setInternalValue(next) }, [value]) const { reactFlowInstance } = useContext(flowContext) From 33569430bffa6e32cbea0e1ea608da3a85b4f08f Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 10:51:00 -0300 Subject: [PATCH 3/9] fix(AGENT-676): simplify credential save to single flow, fix stale closure Unify handleModalAssign into a single path that always saves to backend, then applies credentials to live canvas nodes for immediate UI feedback. Skip refetch only when canvas is open to preserve unsaved changes. Remove over-engineered canvas/non-canvas split, SET_DIRTY dispatch, processFlowCredentials import, and canvasNodes/liveCredentials computation. Fix AsyncDropdown stale closure by removing manual equality check in useEffect. Co-Authored-By: Claude Opus 4.6 --- .../ui/src/components/SidekickSetupModal.jsx | 50 +++++++------------ .../ui-component/dropdown/AsyncDropdown.jsx | 3 +- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/packages/ui/src/components/SidekickSetupModal.jsx b/packages/ui/src/components/SidekickSetupModal.jsx index 7e147a2d88b..b518d6d1bbd 100644 --- a/packages/ui/src/components/SidekickSetupModal.jsx +++ b/packages/ui/src/components/SidekickSetupModal.jsx @@ -9,7 +9,7 @@ import PropTypes from 'prop-types' import Button from '@mui/material/Button' // Redux notification imports -import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_DIRTY } from '@/store/actions' +import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' import useNotifier from '@/utils/useNotifier' // UI components and utilities @@ -17,7 +17,6 @@ import { IconX } from '@tabler/icons-react' import { flowContext } from '@/store/context/ReactFlowContext' import { FLOWISE_CREDENTIAL_ID } from '@/store/constant' import { useSidekickWithCredentials } from '@/hooks/useSidekickWithCredentials' -import { processFlowCredentials } from '@utils/processFlowCredentials' import { updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' import { getCredentialModalDismissed, setCredentialModalDismissed } from '@/utils/credentialModalPreference' import useConfirm from '@/hooks/useConfirm' @@ -43,10 +42,6 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { // Fetch sidekick data with credentials const { sidekick, needsSetup, credentialsToShow, updateSidekick } = useSidekickWithCredentials(sidekickId, isQuickSetup) - const canvasNodes = reactFlowInstance?.getNodes() ?? [] - const liveCredentials = - canvasNodes.length > 0 ? processFlowCredentials({ nodes: canvasNodes })?.credentials ?? credentialsToShow : credentialsToShow - // Redux notification setup const dispatch = useDispatch() useNotifier() @@ -114,8 +109,12 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { async (credentialAssignments, options) => { try { if (credentialAssignments && Object.keys(credentialAssignments).length > 0) { + // Save credentials to backend + const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) + await updateSidekick({ flowData: JSON.stringify(updatedFlowData) }) + + // If canvas is open, apply to live nodes for immediate UI feedback if (reactFlowInstance) { - // Canvas path: apply credentials directly to live ReactFlow nodes reactFlowInstance.setNodes((nodes) => nodes.map((node) => { if (credentialAssignments[node.id]) { @@ -134,28 +133,18 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { return node }) ) - dispatch({ type: SET_DIRTY }) - window.dispatchEvent( - new CustomEvent('credentials-updated', { - detail: { chatflowId: sidekickId, skipRefetch: true } - }) - ) - enqueueSnackbar({ - message: 'Credentials applied — save to persist', - options: { variant: 'success' } - }) - } else { - // Non-canvas path: save to backend via API - const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) - await updateSidekick({ - flowData: JSON.stringify(updatedFlowData) - }) - window.dispatchEvent(new CustomEvent('credentials-updated', { detail: { chatflowId: sidekickId } })) - enqueueSnackbar({ - message: 'Credentials saved successfully!', - options: { variant: 'success' } - }) } + + // Skip refetch on canvas to preserve unsaved changes + window.dispatchEvent( + new CustomEvent('credentials-updated', { + detail: { chatflowId: sidekickId, skipRefetch: !!reactFlowInstance } + }) + ) + enqueueSnackbar({ + message: 'Credentials saved successfully!', + options: { variant: 'success' } + }) } persistDismissPreference(options) handleURLCleanup() @@ -168,7 +157,6 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { sidekick, reactFlowInstance, updateSidekick, - dispatch, enqueueSnackbar, sidekickId, handleURLCleanup, @@ -211,7 +199,7 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { if (isQuickSetup) return false if (hasSkipped) return true if (isSuppressed) return true - return reactFlowInstance ? liveCredentials.length === 0 : !needsSetup + return !needsSetup } if (shouldHideModal()) { @@ -223,7 +211,7 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { { - const next = value ?? 'choose an option' - if (next !== internalValue) setInternalValue(next) + setInternalValue(value ?? 'choose an option') }, [value]) const { reactFlowInstance } = useContext(flowContext) From 141540ed0f37562f53497a4e06b987ad25e02b39 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 12:00:30 -0300 Subject: [PATCH 4/9] fix(AGENT-676): use live canvas state for credential modal Read credentials and flow data from reactFlowInstance instead of stale SWR/backend data so unsaved nodes appear in QuickSetup and credential saves preserve canvas layout. Also listen for credentials-updated event in CredentialInputHandler to remount AsyncDropdown after new credentials are created. Co-Authored-By: Claude Opus 4.6 --- .../ui/src/components/SidekickSetupModal.jsx | 41 ++++++++++++++++--- .../views/canvas/CredentialInputHandler.jsx | 8 ++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/SidekickSetupModal.jsx b/packages/ui/src/components/SidekickSetupModal.jsx index b518d6d1bbd..f287de67b4d 100644 --- a/packages/ui/src/components/SidekickSetupModal.jsx +++ b/packages/ui/src/components/SidekickSetupModal.jsx @@ -17,7 +17,7 @@ import { IconX } from '@tabler/icons-react' import { flowContext } from '@/store/context/ReactFlowContext' import { FLOWISE_CREDENTIAL_ID } from '@/store/constant' import { useSidekickWithCredentials } from '@/hooks/useSidekickWithCredentials' -import { updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' +import { extractAllCredentials, updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' import { getCredentialModalDismissed, setCredentialModalDismissed } from '@/utils/credentialModalPreference' import useConfirm from '@/hooks/useConfirm' @@ -109,11 +109,29 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { async (credentialAssignments, options) => { try { if (credentialAssignments && Object.keys(credentialAssignments).length > 0) { - // Save credentials to backend - const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) - await updateSidekick({ flowData: JSON.stringify(updatedFlowData) }) + let flowDataForSave + if (reactFlowInstance) { + const rfObject = reactFlowInstance.toObject() + rfObject.nodes = rfObject.nodes.map((node) => { + if (credentialAssignments[node.id]) { + return { + ...node, + data: { + ...node.data, + credential: credentialAssignments[node.id], + inputs: { ...node.data.inputs, [FLOWISE_CREDENTIAL_ID]: credentialAssignments[node.id] } + } + } + } + return node + }) + flowDataForSave = JSON.stringify(rfObject) + } else { + const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) + flowDataForSave = JSON.stringify(updatedFlowData) + } + await updateSidekick({ flowData: flowDataForSave }) - // If canvas is open, apply to live nodes for immediate UI feedback if (reactFlowInstance) { reactFlowInstance.setNodes((nodes) => nodes.map((node) => { @@ -206,12 +224,23 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { return null } + // When canvas is open, use live nodes for credentials. Otherwise fall back to SWR data. + let effectiveCredentials = credentialsToShow + try { + if (reactFlowInstance) { + const { allCredentials } = extractAllCredentials(reactFlowInstance.toObject()) + if (allCredentials?.length) effectiveCredentials = allCredentials + } + } catch (_e) { + /* fall back to SWR data */ + } + return ( <> { + const handleCredentialsUpdated = () => { + setReloadTimestamp(Date.now().toString()) + } + window.addEventListener('credentials-updated', handleCredentialsUpdated) + return () => window.removeEventListener('credentials-updated', handleCredentialsUpdated) + }, []) + return (
{inputParam && ( From 46a33f51dcdb8e5118841174b99db2bbd90d6f43 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 12:48:29 -0300 Subject: [PATCH 5/9] fix(AGENT-676): add error logging to credential extraction catch block Co-Authored-By: Claude Opus 4.6 --- packages/ui/src/components/SidekickSetupModal.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/SidekickSetupModal.jsx b/packages/ui/src/components/SidekickSetupModal.jsx index f287de67b4d..85e0c1cec2a 100644 --- a/packages/ui/src/components/SidekickSetupModal.jsx +++ b/packages/ui/src/components/SidekickSetupModal.jsx @@ -231,8 +231,8 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { const { allCredentials } = extractAllCredentials(reactFlowInstance.toObject()) if (allCredentials?.length) effectiveCredentials = allCredentials } - } catch (_e) { - /* fall back to SWR data */ + } catch (error) { + console.error('[SidekickSetupModal] Failed to extract credentials from canvas:', error) } return ( From d2a52d7ec4a441912026ca92d4e3dfe04b1086ad Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 14:20:07 -0300 Subject: [PATCH 6/9] refactor(AGENT-676): extract credential helper, fix let to const Extract applyCredentialToNode helper to deduplicate node mapping logic in SidekickSetupModal. Change let to const for useState destructuring in AsyncDropdown. Co-Authored-By: Claude Opus 4.6 --- .../ui/src/components/SidekickSetupModal.jsx | 48 +++++++------------ .../ui-component/dropdown/AsyncDropdown.jsx | 2 +- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/packages/ui/src/components/SidekickSetupModal.jsx b/packages/ui/src/components/SidekickSetupModal.jsx index 85e0c1cec2a..9f4e4cd1e41 100644 --- a/packages/ui/src/components/SidekickSetupModal.jsx +++ b/packages/ui/src/components/SidekickSetupModal.jsx @@ -104,6 +104,21 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { [enqueueSnackbar, dispatch] ) + const applyCredentialToNode = (node, credentialAssignments) => { + if (!credentialAssignments[node.id]) return node + return { + ...node, + data: { + ...node.data, + credential: credentialAssignments[node.id], + inputs: { + ...node.data.inputs, + [FLOWISE_CREDENTIAL_ID]: credentialAssignments[node.id] + } + } + } + } + // Handle credential assignment const handleModalAssign = useCallback( async (credentialAssignments, options) => { @@ -112,19 +127,7 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { let flowDataForSave if (reactFlowInstance) { const rfObject = reactFlowInstance.toObject() - rfObject.nodes = rfObject.nodes.map((node) => { - if (credentialAssignments[node.id]) { - return { - ...node, - data: { - ...node.data, - credential: credentialAssignments[node.id], - inputs: { ...node.data.inputs, [FLOWISE_CREDENTIAL_ID]: credentialAssignments[node.id] } - } - } - } - return node - }) + rfObject.nodes = rfObject.nodes.map((node) => applyCredentialToNode(node, credentialAssignments)) flowDataForSave = JSON.stringify(rfObject) } else { const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) @@ -133,24 +136,7 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { await updateSidekick({ flowData: flowDataForSave }) if (reactFlowInstance) { - reactFlowInstance.setNodes((nodes) => - nodes.map((node) => { - if (credentialAssignments[node.id]) { - return { - ...node, - data: { - ...node.data, - credential: credentialAssignments[node.id], - inputs: { - ...node.data.inputs, - [FLOWISE_CREDENTIAL_ID]: credentialAssignments[node.id] - } - } - } - } - return node - }) - ) + reactFlowInstance.setNodes((nodes) => nodes.map((node) => applyCredentialToNode(node, credentialAssignments))) } // Skip refetch on canvas to preserve unsaved changes diff --git a/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx b/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx index bf1a06e326d..586d3019b97 100644 --- a/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx +++ b/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx @@ -93,7 +93,7 @@ export const AsyncDropdown = ({ } const getDefaultOptionValue = () => (multiple ? [] : '') const addNewOption = [{ label: '- Create New -', name: '-create-' }] - let [internalValue, setInternalValue] = useState(value ?? 'choose an option') + const [internalValue, setInternalValue] = useState(value ?? 'choose an option') useEffect(() => { setInternalValue(value ?? 'choose an option') From dfca65bf6aab954199db4089adf5f05cad64a081 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 11 Feb 2026 15:27:57 -0300 Subject: [PATCH 7/9] refactor(AGENT-676): hoist applyCredentialToNode to module scope, add null guard Move helper outside component to avoid re-creation on each render and add !node.data early return for null safety. Co-Authored-By: Claude Opus 4.6 --- .../ui/src/components/SidekickSetupModal.jsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/SidekickSetupModal.jsx b/packages/ui/src/components/SidekickSetupModal.jsx index 9f4e4cd1e41..be54519e874 100644 --- a/packages/ui/src/components/SidekickSetupModal.jsx +++ b/packages/ui/src/components/SidekickSetupModal.jsx @@ -25,6 +25,21 @@ import useConfirm from '@/hooks/useConfirm' const UnifiedCredentialsModal = dynamic(() => import('@/ui-component/dialog/UnifiedCredentialsModal'), { ssr: false }) const ConfirmDialog = dynamic(() => import('@/ui-component/dialog/ConfirmDialog'), { ssr: false }) +const applyCredentialToNode = (node, credentialAssignments) => { + if (!credentialAssignments[node.id] || !node.data) return node + return { + ...node, + data: { + ...node.data, + credential: credentialAssignments[node.id], + inputs: { + ...node.data.inputs, + [FLOWISE_CREDENTIAL_ID]: credentialAssignments[node.id] + } + } + } +} + const SidekickSetupModal = ({ sidekickId, onComplete }) => { const { confirm } = useConfirm() const preferenceScope = sidekickId ? `sidekick:${sidekickId}` : null @@ -104,21 +119,6 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { [enqueueSnackbar, dispatch] ) - const applyCredentialToNode = (node, credentialAssignments) => { - if (!credentialAssignments[node.id]) return node - return { - ...node, - data: { - ...node.data, - credential: credentialAssignments[node.id], - inputs: { - ...node.data.inputs, - [FLOWISE_CREDENTIAL_ID]: credentialAssignments[node.id] - } - } - } - } - // Handle credential assignment const handleModalAssign = useCallback( async (credentialAssignments, options) => { From 669beb4b23243ae10818961d4f3922374d296ed4 Mon Sep 17 00:00:00 2001 From: Bradley Taylor Date: Thu, 12 Feb 2026 13:09:51 -0800 Subject: [PATCH 8/9] feat(AGENT-683): refactor AnswerAgent MCP to use standard credential pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: AnswerAgent MCP now requires credential configuration instead of automatic API key lookup from the database. - Create AnswerAgentApi credential with apiKey and instanceDomain fields - Replace custom getUserApiKey() database query with getCredentialData/getCredentialParam - Add instance domain support for connecting to other AnswerAgent instances - Fix env variable name to ANSWERAGENT_AI_API_BASE_URL (matching MCP package) - Add fallback chain: credential domain → chatflowDomain → API_HOST → FLOWISE_DOMAIN - Bump version from 1.0 to 2.0 - Link to /sidekick-studio/apikey for API key management Co-Authored-By: Claude Opus 4.6 --- .../credentials/AnswerAgentApi.credential.ts | 37 +++++++ .../tools/MCP/AnswerAgent/AnswerAgentMCP.ts | 104 ++++-------------- 2 files changed, 61 insertions(+), 80 deletions(-) create mode 100644 packages/components/credentials/AnswerAgentApi.credential.ts diff --git a/packages/components/credentials/AnswerAgentApi.credential.ts b/packages/components/credentials/AnswerAgentApi.credential.ts new file mode 100644 index 00000000000..497dac5811d --- /dev/null +++ b/packages/components/credentials/AnswerAgentApi.credential.ts @@ -0,0 +1,37 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class AnswerAgentApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'AnswerAgent API' + this.name = 'answerAgentApi' + this.version = 1.0 + this.description = + 'You can find your API key in API Keys settings' + this.inputs = [ + { + label: 'API Key', + name: 'apiKey', + type: 'password', + placeholder: 'your-api-key' + }, + { + label: 'Instance Domain', + name: 'instanceDomain', + type: 'string', + optional: true, + default: process.env.API_HOST || process.env.FLOWISE_DOMAIN || '', + placeholder: 'https://your-instance.answerai.com', + description: + 'The domain of the AnswerAgent instance to connect to. Change this to connect to a different AnswerAgent instance.' + } + ] + } +} + +module.exports = { credClass: AnswerAgentApi } diff --git a/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts b/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts index e81e602a0ec..8a709360ed6 100644 --- a/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts +++ b/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts @@ -7,8 +7,8 @@ * - Implements INode interface * - Sets tags = ['AAI'] for UI Answer tab integration * - Sets category = 'Tools (MCP)' - * - Automatically uses user's API key from database (no manual credential setup required) - * - Uses API_HOST environment variable for base URL + * - Uses standard Flowise credential pattern (answerAgentApi) + * - Supports connecting to any AnswerAgent instance via credential domain config * - Exposes available actions via mcpActions input * - Registers the node as module.exports = { nodeClass: AnswerAgent_MCP } * @@ -17,7 +17,7 @@ */ import { Tool } from '@langchain/core/tools' import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' -import { getNodeModulesPackagePath } from '../../../../src/utils' +import { getCredentialData, getCredentialParam, getNodeModulesPackagePath } from '../../../../src/utils' import { MCPToolkit } from '../core' class AnswerAgent_MCP implements INode { @@ -30,19 +30,26 @@ class AnswerAgent_MCP implements INode { category: string baseClasses: string[] documentation: string + credential: INodeParams inputs: INodeParams[] tags: string[] constructor() { this.label = 'AnswerAgent MCP' this.name = 'answerAgentMCP' - this.version = 1.0 + this.version = 2.0 this.type = 'AnswerAgent MCP Tool' this.icon = 'answerai-square-black.png' this.category = 'Tools (MCP)' this.tags = ['AAI'] - this.description = 'MCP server that integrates with AnswerAgent API • Zero configuration required' + this.description = 'MCP server that integrates with AnswerAgent API' this.documentation = 'https://www.npmjs.com/package/@answerai/answeragent-mcp' + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['answerAgentApi'] + } this.inputs = [ { label: 'Available Actions', @@ -100,17 +107,20 @@ class AnswerAgent_MCP implements INode { } async getTools(nodeData: INodeData, options: ICommonObject): Promise { - // Get API host from environment variable - const apiHost = options.user?.chatflowDomain - if (!apiHost) { - console.error('AnswerAgent MCP: API_HOST environment variable is not set') + // Get API key and optional instance domain from credential + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const instanceDomain = getCredentialParam('instanceDomain', credentialData, nodeData) + + if (!apiKey) { + console.error('AnswerAgent MCP: Missing AnswerAgent API Key') return [] } - // Get user's API key from database - const apiKey = await this.getUserApiKey(nodeData, options) - if (!apiKey) { - console.error('AnswerAgent MCP: Unable to retrieve user API key from database') + // Use instance domain from credential, fall back to current instance domain or env var + const apiHost = instanceDomain || options.user?.chatflowDomain || process.env.API_HOST || process.env.FLOWISE_DOMAIN + if (!apiHost) { + console.error('AnswerAgent MCP: No instance domain configured and API_HOST is not set') return [] } @@ -121,7 +131,7 @@ class AnswerAgent_MCP implements INode { command: process.execPath, args: [packagePath], env: { - ANSWERAGENT_AI_API_HOST: apiHost, + ANSWERAGENT_AI_API_BASE_URL: apiHost, ANSWERAGENT_AI_API_TOKEN: apiKey } } @@ -134,72 +144,6 @@ class AnswerAgent_MCP implements INode { return tools } - - /** - * Retrieve the user's API key from the database - * This automatically uses the user's default API key, eliminating the need for manual credential setup - */ - private async getUserApiKey(nodeData: INodeData, options: ICommonObject): Promise { - try { - // Get database connection from options - const appDataSource = options.appDataSource - if (!appDataSource) { - console.error('No database connection available') - return null - } - - // Get user ID - handle both contexts: - // 1. Load method context: options.userId - // 2. Workflow execution context: options.user.id - const userId = options.userId || options.user?.id - if (!userId) { - console.error('No user ID available in options') - return null - } - - // Get organization ID - handle both contexts: - // 1. Load method context: options.organizationId - // 2. Workflow execution context: options.user.organizationId - const organizationId = options.organizationId || options.user?.organizationId - if (!organizationId) { - console.error('No organization ID available') - return null - } - - // Fallback to ApiKey table - const databaseEntities = options.databaseEntities - if (!databaseEntities || !databaseEntities['ApiKey']) { - console.error('ApiKey entity not available in databaseEntities') - return null - } - - // Query the database for the user's API keys using the entity from databaseEntities - - const apiKeys = await appDataSource.getRepository(databaseEntities['ApiKey']).find({ - where: { - userId: userId, - organizationId: organizationId, - isActive: true - }, - order: { - updatedDate: 'DESC' // Get the most recently updated key - } - }) - - if (apiKeys.length === 0) { - console.error('No active API keys found for user') - return null - } - - // Return the first (most recent) API key - const selectedApiKey = apiKeys[0].apiKey - - return selectedApiKey - } catch (error) { - console.error('Error retrieving user API key:', error) - return null - } - } } module.exports = { nodeClass: AnswerAgent_MCP } From f3468e2e06510fe674f38e900fdb69c6c3881572 Mon Sep 17 00:00:00 2001 From: Diego Costa Date: Fri, 13 Feb 2026 12:08:35 -0300 Subject: [PATCH 9/9] fix(AGENT-654): change marketplace AppBar position from fixed to relative Back button in marketplace template detail view was hidden behind global navigation due to AppBar using position='fixed'. Changed to position='relative' to match the working v2 implementation. Co-Authored-By: Claude Opus 4.6 --- packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx index b1b9c2b1121..50b285142c0 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx @@ -65,7 +65,7 @@ const MarketplaceCanvas = () => {