fix(agent): tool credential dropdown (#1884)

* Checkpoint

* Fix dropdown
This commit is contained in:
Siddharth Ganesan
2025-11-10 21:40:00 -08:00
committed by GitHub
parent 82731850b2
commit 37fd21e0aa
6 changed files with 110 additions and 32 deletions

View File

@@ -51,6 +51,7 @@ export function ChannelSelectorInput({
const { finalDisabled, dependsOn, dependencyValues } = useDependsOnGate(blockId, subBlock, {
disabled,
isPreview,
previewContextValues,
})
// Choose credential strictly based on auth method - use effective values

View File

@@ -42,7 +42,11 @@ export function FileSelectorInput({
const params = useParams()
const workflowIdFromUrl = (params?.workflowId as string) || activeWorkflowId || ''
// Central dependsOn gating for this selector instance
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
const { finalDisabled, dependsOn } = useDependsOnGate(blockId, subBlock, {
disabled,
isPreview,
previewContextValues,
})
// Helper to coerce various preview value shapes into a string ID
const coerceToIdString = (val: unknown): string => {
@@ -63,12 +67,20 @@ export function FileSelectorInput({
// Use the proper hook to get the current value and setter
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
const [domainValue] = useSubBlockValue(blockId, 'domain')
const [projectIdValue] = useSubBlockValue(blockId, 'projectId')
const [planIdValue] = useSubBlockValue(blockId, 'planId')
const [teamIdValue] = useSubBlockValue(blockId, 'teamId')
const [operationValue] = useSubBlockValue(blockId, 'operation')
const [connectedCredentialFromStore] = useSubBlockValue(blockId, 'credential')
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
const [projectIdValueFromStore] = useSubBlockValue(blockId, 'projectId')
const [planIdValueFromStore] = useSubBlockValue(blockId, 'planId')
const [teamIdValueFromStore] = useSubBlockValue(blockId, 'teamId')
const [operationValueFromStore] = useSubBlockValue(blockId, 'operation')
// Use previewContextValues if provided (for tools inside agent blocks), otherwise use store values
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
const domainValue = previewContextValues?.domain ?? domainValueFromStore
const projectIdValue = previewContextValues?.projectId ?? projectIdValueFromStore
const planIdValue = previewContextValues?.planId ?? planIdValueFromStore
const teamIdValue = previewContextValues?.teamId ?? teamIdValueFromStore
const operationValue = previewContextValues?.operation ?? operationValueFromStore
// Determine if the persisted credential belongs to the current viewer
// Use service providerId where available (e.g., onedrive/sharepoint) instead of base provider ("microsoft")
@@ -76,7 +88,7 @@ export function FileSelectorInput({
? getProviderIdFromServiceId(subBlock.serviceId)
: (subBlock.provider as string) || ''
const { isForeignCredential } = useForeignCredential(
foreignCheckProvider,
subBlock.provider || subBlock.serviceId || 'outlook',
(connectedCredential as string) || ''
)
@@ -109,6 +121,20 @@ export function FileSelectorInput({
// Use preview value when in preview mode, otherwise use store value
const value = isPreview ? previewValue : storeValue
const credentialDependencySatisfied = (() => {
if (!dependsOn.includes('credential')) return true
const normalizedCredential = coerceToIdString(connectedCredential)
if (!normalizedCredential || normalizedCredential.trim().length === 0) {
return false
}
if (isForeignCredential) {
return false
}
return true
})()
const shouldForceDisable = !credentialDependencySatisfied
// For Google Drive
const clientId = getEnv('NEXT_PUBLIC_GOOGLE_CLIENT_ID') || ''
const apiKey = getEnv('NEXT_PUBLIC_GOOGLE_API_KEY') || ''
@@ -132,7 +158,7 @@ export function FileSelectorInput({
collaborativeSetSubblockValue(blockId, subBlock.id, val)
}}
label={subBlock.placeholder || 'Select Google Calendar'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
credentialId={credential}
workflowId={workflowIdFromUrl}
@@ -166,7 +192,7 @@ export function FileSelectorInput({
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select Confluence page'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
credentialId={credential}
workflowId={workflowIdFromUrl}
@@ -200,7 +226,7 @@ export function FileSelectorInput({
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select Jira issue'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
credentialId={credential}
projectId={(projectIdValue as string) || ''}
@@ -230,7 +256,7 @@ export function FileSelectorInput({
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select Microsoft Excel file'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
workflowId={activeWorkflowId || ''}
credentialId={credential}
@@ -260,7 +286,7 @@ export function FileSelectorInput({
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select Microsoft Word document'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
/>
</div>
@@ -288,7 +314,7 @@ export function FileSelectorInput({
serviceId={subBlock.serviceId}
mimeType={subBlock.mimeType}
label={subBlock.placeholder || 'Select OneDrive folder'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
workflowId={activeWorkflowId || ''}
credentialId={credential}
@@ -318,7 +344,7 @@ export function FileSelectorInput({
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select SharePoint site'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
workflowId={activeWorkflowId || ''}
credentialId={credential}
@@ -354,7 +380,7 @@ export function FileSelectorInput({
requiredScopes={subBlock.requiredScopes || []}
serviceId='microsoft-planner'
label={subBlock.placeholder || 'Select task'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
planId={planId}
workflowId={activeWorkflowId || ''}
@@ -412,7 +438,7 @@ export function FileSelectorInput({
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select Teams message location'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
credential={credential}
selectionType={selectionType}
@@ -455,7 +481,7 @@ export function FileSelectorInput({
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || `Select ${itemType}`}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
showPreview={true}
credentialId={credential}
itemType={itemType}
@@ -496,7 +522,7 @@ export function FileSelectorInput({
provider={provider}
requiredScopes={subBlock.requiredScopes || []}
label={subBlock.placeholder || 'Select file'}
disabled={finalDisabled}
disabled={finalDisabled || shouldForceDisable}
serviceId={subBlock.serviceId}
mimeTypeFilter={subBlock.mimeType}
showPreview={true}

View File

@@ -28,6 +28,7 @@ interface ProjectSelectorInputProps {
onProjectSelect?: (projectId: string) => void
isPreview?: boolean
previewValue?: any | null
previewContextValues?: Record<string, any>
}
export function ProjectSelectorInput({
@@ -37,30 +38,40 @@ export function ProjectSelectorInput({
onProjectSelect,
isPreview = false,
previewValue,
previewContextValues,
}: ProjectSelectorInputProps) {
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
const [selectedProjectId, setSelectedProjectId] = useState<string>('')
const [_projectInfo, setProjectInfo] = useState<any | null>(null)
// Use the proper hook to get the current value and setter
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
const [connectedCredentialFromStore] = useSubBlockValue(blockId, 'credential')
const [linearTeamIdFromStore] = useSubBlockValue(blockId, 'teamId')
const [jiraDomainFromStore] = useSubBlockValue(blockId, 'domain')
// Use previewContextValues if provided (for tools inside agent blocks), otherwise use store values
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
const linearCredential = previewContextValues?.credential ?? connectedCredentialFromStore
const linearTeamId = previewContextValues?.teamId ?? linearTeamIdFromStore
const jiraDomain = previewContextValues?.domain ?? jiraDomainFromStore
const { isForeignCredential } = useForeignCredential(
subBlock.provider || subBlock.serviceId || 'jira',
(connectedCredential as string) || ''
)
// Reactive dependencies from store for Linear
const [linearCredential] = useSubBlockValue(blockId, 'credential')
const [linearTeamId] = useSubBlockValue(blockId, 'teamId')
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) as string | null
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
disabled,
isPreview,
previewContextValues,
})
// Get provider-specific values
const provider = subBlock.provider || 'jira'
const isLinear = provider === 'linear'
// Jira/Discord upstream fields
const [jiraDomain] = useSubBlockValue(blockId, 'domain')
const [jiraCredential] = useSubBlockValue(blockId, 'credential')
// Jira/Discord upstream fields - use values from previewContextValues or store
const jiraCredential = connectedCredential
const domain = (jiraDomain as string) || ''
// Verify Jira credential belongs to current user; if not, treat as absent

View File

@@ -168,6 +168,7 @@ function FileSelectorSyncWrapper({
mimeType: uiComponent.mimeType,
requiredScopes: uiComponent.requiredScopes || [],
placeholder: uiComponent.placeholder,
dependsOn: uiComponent.dependsOn,
}}
disabled={disabled}
previewContextValues={previewContextValues}
@@ -433,6 +434,7 @@ function ChannelSelectorSyncWrapper({
title: paramId,
provider: uiComponent.provider || 'slack',
placeholder: uiComponent.placeholder,
dependsOn: uiComponent.dependsOn,
}}
onChannelSelect={onChange}
disabled={disabled}
@@ -1174,9 +1176,11 @@ export function ToolInput({
serviceId: uiComponent.serviceId,
placeholder: uiComponent.placeholder,
requiredScopes: uiComponent.requiredScopes,
dependsOn: uiComponent.dependsOn,
}}
onProjectSelect={onChange}
disabled={disabled}
previewContextValues={currentToolParams as any}
/>
)

View File

@@ -13,29 +13,62 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
export function useDependsOnGate(
blockId: string,
subBlock: SubBlockConfig,
opts?: { disabled?: boolean; isPreview?: boolean }
opts?: { disabled?: boolean; isPreview?: boolean; previewContextValues?: Record<string, any> }
) {
const disabledProp = opts?.disabled ?? false
const isPreview = opts?.isPreview ?? false
const previewContextValues = opts?.previewContextValues
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
// Use only explicit dependsOn from block config. No inference.
const dependsOn: string[] = (subBlock.dependsOn as string[] | undefined) || []
const normalizeDependencyValue = (rawValue: unknown): unknown => {
if (rawValue === null || rawValue === undefined) return null
if (typeof rawValue === 'object') {
if (Array.isArray(rawValue)) {
if (rawValue.length === 0) return null
return rawValue.map((item) => normalizeDependencyValue(item))
}
const record = rawValue as Record<string, any>
if ('value' in record) {
return normalizeDependencyValue(record.value)
}
if ('id' in record) {
return record.id
}
return record
}
return rawValue
}
const dependencyValues = useSubBlockStore((state) => {
if (dependsOn.length === 0) return [] as any[]
// If previewContextValues are provided (e.g., tool parameters), use those first
if (previewContextValues) {
return dependsOn.map((depKey) => normalizeDependencyValue(previewContextValues[depKey]))
}
if (!activeWorkflowId) return dependsOn.map(() => null)
const workflowValues = state.workflowValues[activeWorkflowId] || {}
const blockValues = (workflowValues as any)[blockId] || {}
return dependsOn.map((depKey) => (blockValues as any)[depKey] ?? null)
return dependsOn.map((depKey) => normalizeDependencyValue((blockValues as any)[depKey]))
}) as any[]
const depsSatisfied = useMemo(() => {
if (dependsOn.length === 0) return true
return dependencyValues.every((v) =>
typeof v === 'string' ? v.trim().length > 0 : v !== null && v !== undefined && v !== ''
)
return dependencyValues.every((value) => {
if (value === null || value === undefined) return false
if (typeof value === 'string') return value.trim().length > 0
if (Array.isArray(value)) return value.length > 0
return value !== ''
})
}, [dependencyValues, dependsOn])
// Block everything except the credential field itself until dependencies are set

View File

@@ -36,6 +36,7 @@ export interface UIComponentConfig {
acceptedTypes?: string[]
multiple?: boolean
maxSize?: number
dependsOn?: string[]
}
export interface SubBlockConfig {
@@ -61,6 +62,7 @@ export interface SubBlockConfig {
acceptedTypes?: string[]
multiple?: boolean
maxSize?: number
dependsOn?: string[]
}
export interface BlockConfig {
@@ -236,6 +238,7 @@ export function getToolParametersConfig(
acceptedTypes: subBlock.acceptedTypes,
multiple: subBlock.multiple,
maxSize: subBlock.maxSize,
dependsOn: subBlock.dependsOn,
}
}
}