[0]> = {}) {
const onChange = vi.fn()
act(() => {
- root.render()
+ root.render(
+
+ )
})
return { onChange }
}
@@ -46,6 +85,33 @@ describe('RetrySettings', () => {
expect(field('block-retry-max-tries')!.value).toBe('5')
})
+ it('commits the normalized value when the field loses focus', () => {
+ const { onChange } = renderSettings()
+ const maxTries = field('block-retry-max-tries')!
+ const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!
+
+ act(() => {
+ setValue.call(maxTries, '2.7')
+ maxTries.dispatchEvent(new Event('input', { bubbles: true }))
+ })
+ expect(field('block-retry-max-tries')!.value).toBe('2.7')
+ expect(onChange).not.toHaveBeenCalled()
+
+ act(() => {
+ maxTries.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
+ })
+
+ expect(onChange).toHaveBeenCalledWith({ ...policy, maxTries: 2 })
+ expect(field('block-retry-max-tries')!.value).toBe('5')
+ })
+
+ it('turns off the reference pickers on the numeric fields', () => {
+ renderSettings()
+
+ expect(field('block-retry-max-tries')!.dataset.allowReferences).toBe('false')
+ expect(field('block-retry-wait')!.dataset.allowReferences).toBe('false')
+ })
+
it('renders only the switch while retry is off', () => {
renderSettings({ retry: { ...policy, enabled: false } })
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/retry-settings/retry-settings.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/retry-settings/retry-settings.tsx
index 87ac1f8271a..6f0cbefffa5 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/retry-settings/retry-settings.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/retry-settings/retry-settings.tsx
@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
-import { ChipInput, FieldDivider, Label, Switch } from '@sim/emcn'
+import { FieldDivider, Label, Switch } from '@sim/emcn'
import {
BLOCK_RETRY_DEFAULT_TRIES,
BLOCK_RETRY_DEFAULT_WAIT_MS,
@@ -9,37 +9,55 @@ import {
normalizeBlockRetryTries,
normalizeBlockRetryWaitMs,
} from '@sim/workflow-types/workflow'
+import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input'
+import type { SubBlockConfig } from '@/blocks/types'
interface RetrySettingsProps {
+ blockId: string
retry: BlockRetryConfig | undefined
disabled: boolean
onChange: (retry: BlockRetryConfig) => void
}
interface RetryNumberFieldProps {
- id: string
- title: string
+ blockId: string
+ config: SubBlockConfig
value: number
disabled: boolean
normalize: (value: unknown) => number
onCommit: (value: number) => void
}
+const MAX_TRIES_CONFIG = {
+ id: 'block-retry-max-tries',
+ title: 'Max tries',
+ type: 'short-input',
+ connectionDroppable: false,
+} as const satisfies SubBlockConfig
+
+const WAIT_CONFIG = {
+ id: 'block-retry-wait',
+ title: 'Wait between tries (ms)',
+ type: 'short-input',
+ connectionDroppable: false,
+} as const satisfies SubBlockConfig
+
/**
* A bounded number field that commits on blur.
*
- * Typed as text with a numeric input mode rather than `type='number'`: the
- * native spinner is all that buys, and it does not fit the field chrome the rest
- * of the panel uses. Bounds are applied on commit through the same normalizer
- * execution uses, so the field cannot clamp differently from the executor.
+ * Renders the same `ShortInput` every other sub-block text field uses, so it
+ * carries the panel's field chrome. Retry values are plain numbers that never
+ * resolve references, so the reference pickers are turned off. Bounds are
+ * applied on commit through the same normalizer execution uses, so the field
+ * cannot clamp differently from the executor.
*
* The draft exists only while the field is being edited; clearing it on commit
* lets an external change — a collaborator's edit, or an undo — flow straight
* through on the next render with no resync.
*/
function RetryNumberField({
- id,
- title,
+ blockId,
+ config,
value,
disabled,
normalize,
@@ -57,15 +75,18 @@ function RetryNumberField({
return (
-
-
+
+
+ setDraft(event.target.value)}
+ onChange={setDraft}
onBlur={commit}
disabled={disabled}
+ allowReferences={false}
/>
)
@@ -79,7 +100,7 @@ function RetryNumberField({
* with `enabled: false` when it is switched off, so turning it back on restores
* what was configured rather than snapping to the defaults.
*/
-export function RetrySettings({ retry, disabled, onChange }: RetrySettingsProps) {
+export function RetrySettings({ blockId, retry, disabled, onChange }: RetrySettingsProps) {
const enabled = retry?.enabled === true
const maxTries = retry?.maxTries ?? BLOCK_RETRY_DEFAULT_TRIES
const waitBetweenTriesMs = retry?.waitBetweenTriesMs ?? BLOCK_RETRY_DEFAULT_WAIT_MS
@@ -106,8 +127,8 @@ export function RetrySettings({ retry, disabled, onChange }: RetrySettingsProps)
<>
+ /** Whether the env-var and tag reference pickers may open. Defaults to `true`. */
+ allowReferences?: boolean
+ /** Called when the input loses focus. */
+ onBlur?: () => void
}
/**
@@ -81,6 +85,8 @@ export const ShortInput = memo(function ShortInput({
wandControlRef,
hideInternalWand = false,
workflowSearchValuePath = [],
+ allowReferences = true,
+ onBlur,
}: ShortInputProps) {
const activeSearchTarget = useActiveSearchTarget()
const [localContent, setLocalContent] = useState('')
@@ -284,7 +290,8 @@ export const ShortInput = memo(function ShortInput({
const handleBlur = useCallback(() => {
setIsFocused(false)
- }, [])
+ onBlur?.()
+ }, [onBlur])
// Expose wand control handlers to parent via ref
useImperativeHandle(
@@ -325,6 +332,7 @@ export const ShortInput = memo(function ShortInput({
disabled={disabled}
isStreaming={wandHook.isStreaming}
previewValue={previewValue}
+ allowReferences={allowReferences}
shouldForceEnvDropdown={shouldForceEnvDropdown}
shouldForceTagDropdown={shouldForceTagDropdown}
>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller/sub-block-input-controller.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller/sub-block-input-controller.tsx
index 18627eeb96d..995a6901787 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller/sub-block-input-controller.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller/sub-block-input-controller.tsx
@@ -33,6 +33,8 @@ export interface SubBlockInputControllerProps {
onStreamingEnd?: () => void
/** Optional preview value for read-only preview. */
previewValue?: string | null
+ /** Whether the env-var and tag reference pickers may open. Defaults to `true`. */
+ allowReferences?: boolean
/**
* Optional callback to force/show the env var dropdown (e.g., API key fields).
* Return { show: true, searchTerm?: string } to override defaults.
@@ -82,6 +84,7 @@ export function SubBlockInputController(props: SubBlockInputControllerProps): Re
isStreaming,
onStreamingEnd,
previewValue,
+ allowReferences,
shouldForceEnvDropdown,
shouldForceTagDropdown,
children,
@@ -98,6 +101,7 @@ export function SubBlockInputController(props: SubBlockInputControllerProps): Re
isStreaming,
onStreamingEnd,
previewValue,
+ allowReferences,
shouldForceEnvDropdown,
shouldForceTagDropdown,
})
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control.tsx
index d9de3abfb8b..e79ab62abcd 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/usage-control.tsx
@@ -1,8 +1,11 @@
-import { Button, ChipCombobox, cn, Label, Tooltip } from '@sim/emcn'
+import { Combobox, cn, Label, Tooltip } from '@sim/emcn'
import { ArrowLeftRight } from '@sim/emcn/icons'
import type { CanonicalMode } from '@/lib/workflows/subblocks/visibility'
import type { StoredTool } from '@/lib/workflows/tool-input/types'
import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input'
+import type { SubBlockConfig } from '@/blocks/types'
+
+type UsageControlValue = NonNullable
interface ToolUsageControlProps {
blockId: string
@@ -12,33 +15,34 @@ interface ToolUsageControlProps {
mode: CanonicalMode
supportsForce: boolean
disabled: boolean
- onFixedChange: (value: NonNullable) => void
+ onFixedChange: (value: UsageControlValue) => void
onExpressionChange: (value: string) => void
onModeToggle: () => void
}
const MODE_OPTIONS = [
- {
- value: 'auto',
- label: 'Auto',
- suffixElement: (model decides),
- },
- {
- value: 'force',
- label: 'Force',
- suffixElement: (always use),
- },
- {
- value: 'none',
- label: 'None',
- suffixElement: (disable tool),
- },
-] as const
+ { value: 'auto', label: 'Auto', hint: '(model decides)' },
+ { value: 'force', label: 'Force', hint: '(always use)' },
+ { value: 'none', label: 'None', hint: '(disable tool)' },
+] as const satisfies ReadonlyArray<{ value: UsageControlValue; label: string; hint: string }>
+
+const EXPRESSION_CONFIG = {
+ id: 'usageControlExpression',
+ title: 'Permission Mode',
+ type: 'short-input',
+} as const satisfies SubBlockConfig
+
+function isUsageControlValue(value: string): value is UsageControlValue {
+ return MODE_OPTIONS.some((option) => option.value === value)
+}
/**
* Permission Mode control for one agent tool. Selector mode picks a fixed `usageControl`, and
* Variable mode edits a `usageControlExpression` that must resolve to auto, force, or none.
* Both values are kept so toggling modes does not discard the inactive one.
+ *
+ * Renders the same label row, `Combobox`, and `ShortInput` as every other sub-block field, so
+ * the control matches the tool params beneath it.
*/
export function ToolUsageControl({
blockId,
@@ -57,40 +61,38 @@ export function ToolUsageControl({
return (
-
-
-
-
-
- {toggleLabel}
-
+
+
+
+
+
+
+
+ {toggleLabel}
+
+
+
{mode === 'advanced' ? (
) : (
-
({
- ...option,
- disabled: option.value === 'force' && !supportsForce,
- suffixElement:
- option.value === 'force' && !supportsForce ? (
- (not supported by model)
- ) : (
- option.suffixElement
+ {
+ const unsupported = option.value === 'force' && !supportsForce
+ return {
+ value: option.value,
+ label: option.label,
+ disabled: unsupported,
+ suffixElement: (
+
+ {unsupported ? '(not supported by model)' : option.hint}
+
),
- onSelect: () => onFixedChange(option.value),
- }))}
+ }
+ })}
value={tool.usageControl ?? 'auto'}
+ onChange={(value) => {
+ if (isUsageControlValue(value)) onFixedChange(value)
+ }}
+ editable={false}
disabled={disabled}
aria-label='Permission Mode'
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input.ts
index b91ba626b44..8723ae4922c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input.ts
@@ -38,6 +38,11 @@ export interface UseSubBlockInputOptions {
onStreamingEnd?: () => void
/** Optional preview value for read-only preview displays. */
previewValue?: string | null
+ /**
+ * Whether the env-var and tag reference pickers may open. Defaults to `true`; pass `false` for
+ * fields whose value can never hold a reference.
+ */
+ allowReferences?: boolean
/**
* Optional callback to force/show the env var dropdown (e.g., API key fields).
* Return { show: true, searchTerm?: string } to override defaults.
@@ -160,6 +165,7 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI
isStreaming = false,
onStreamingEnd,
previewValue,
+ allowReferences = true,
shouldForceEnvDropdown,
shouldForceTagDropdown,
} = options
@@ -558,8 +564,8 @@ export function useSubBlockInput(options: UseSubBlockInputOptions): UseSubBlockI
valueString,
isDisabled,
cursorPosition,
- showEnvVars,
- showTags,
+ showEnvVars: allowReferences && showEnvVars,
+ showTags: allowReferences && showTags,
searchTerm,
activeSourceBlockId,
handlers: {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
index dc345c60fa2..f24e926e2cd 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
@@ -801,6 +801,7 @@ export function Editor() {
{showRetrySettings && (