diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx
index cf50fe16738..1eb22d6b5a8 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx
@@ -66,6 +66,8 @@ interface DropdownProps {
dependsOn?: SubBlockConfig['dependsOn']
/** Enable search input in dropdown */
searchable?: boolean
+ /** Render option labels verbatim instead of lowercasing them */
+ preserveLabelCase?: boolean
}
/**
@@ -92,6 +94,7 @@ export const Dropdown = memo(function Dropdown({
fetchOptionById,
dependsOn,
searchable = false,
+ preserveLabelCase = false,
}: DropdownProps) {
const activeSearchTarget = useActiveSearchTarget()
const { isToolAllowed } = usePermissionConfig()
@@ -208,18 +211,19 @@ export const Dropdown = memo(function Dropdown({
}, [subBlockId, blockConfig, allOptions, isToolAllowed])
const comboboxOptions = useMemo((): ComboboxOption[] => {
+ const toLabel = (raw: string) => (preserveLabelCase ? raw : raw.toLowerCase())
return allOptions.map((opt) => {
if (typeof opt === 'string') {
- return { label: opt.toLowerCase(), value: opt, hidden: deniedOperationIds.has(opt) }
+ return { label: toLabel(opt), value: opt, hidden: deniedOperationIds.has(opt) }
}
return {
- label: opt.label.toLowerCase(),
+ label: toLabel(opt.label),
value: opt.id,
icon: 'icon' in opt ? opt.icon : undefined,
hidden: opt.hidden || deniedOperationIds.has(opt.id),
}
})
- }, [allOptions, deniedOperationIds])
+ }, [allOptions, deniedOperationIds, preserveLabelCase])
const optionMap = useMemo(() => {
return new Map(comboboxOptions.map((opt) => [opt.value, opt.label]))
@@ -370,7 +374,8 @@ export const Dropdown = memo(function Dropdown({
return (
{visibleValues.map((selectedValue: string, index) => {
- const label = (optionMap.get(selectedValue) || selectedValue).toLowerCase()
+ const rawLabel = optionMap.get(selectedValue) || selectedValue
+ const label = preserveLabelCase ? rawLabel : rawLabel.toLowerCase()
const workflowSearchHighlight = getWorkflowSearchLabelHighlight({
activeSearchTarget,
blockId,
@@ -379,7 +384,7 @@ export const Dropdown = memo(function Dropdown({
label,
})
return (
-
+
{formatDisplayText(label, { workflowSearchHighlight })}
@@ -387,13 +392,21 @@ export const Dropdown = memo(function Dropdown({
)
})}
{overflowCount > 0 && (
-
+
+{overflowCount}
)}
)
- }, [activeSearchTarget, blockId, multiSelect, multiValues, optionMap, subBlockId])
+ }, [
+ activeSearchTarget,
+ blockId,
+ multiSelect,
+ multiValues,
+ optionMap,
+ preserveLabelCase,
+ subBlockId,
+ ])
const singleSelectOverlay = useMemo(() => {
if (multiSelect || !singleValue) return undefined
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx
index a860fcfcc33..8832dd0ef54 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx
@@ -689,6 +689,7 @@ function SubBlockComponent({
fetchOptionById={config.fetchOptionById}
dependsOn={config.dependsOn}
searchable={config.searchable}
+ preserveLabelCase={config.preserveLabelCase}
/>
)
diff --git a/apps/sim/blocks/blocks/function.ts b/apps/sim/blocks/blocks/function.ts
index 6229b718a75..ea8acae1fcb 100644
--- a/apps/sim/blocks/blocks/function.ts
+++ b/apps/sim/blocks/blocks/function.ts
@@ -138,6 +138,9 @@ try {
paramVisibility: 'user-only',
multiSelect: true,
searchable: true,
+ // Secret names are case-sensitive: the code references the exact name via
+ // `{{NAME}}`, so the picker must not lowercase what it shows.
+ preserveLabelCase: true,
options: [],
condition: { field: 'secretScope', value: 'selected' },
placeholder: 'Select secrets this tool can read',
diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts
index 2b29a9a1ab4..716625d7102 100644
--- a/apps/sim/blocks/types.ts
+++ b/apps/sim/blocks/types.ts
@@ -437,6 +437,15 @@ export interface SubBlockConfig {
rows?: number
// Multi-select functionality
multiSelect?: boolean
+ /**
+ * Dropdown-specific: render option labels verbatim instead of lowercasing them.
+ *
+ * The editor lowercases dropdown labels as a typographic convention, which
+ * suits authored operation names ("Send Message"). It corrupts labels that are
+ * case-sensitive identifiers the user must reproduce elsewhere — a workspace
+ * secret shown as `stripe_key` cannot be referenced as `{{stripe_key}}`.
+ */
+ preserveLabelCase?: boolean
// Combobox specific: Enable search input in dropdown
searchable?: boolean
/** Dropdown-specific: include static options as Cmd K search entries that preset this subblock. */
diff --git a/apps/sim/lib/workflows/subblocks/options.ts b/apps/sim/lib/workflows/subblocks/options.ts
index 9c6dc78216a..8f5baab9b33 100644
--- a/apps/sim/lib/workflows/subblocks/options.ts
+++ b/apps/sim/lib/workflows/subblocks/options.ts
@@ -1,10 +1,6 @@
-import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/environment/api'
+import { fetchWorkspaceEnvironment } from '@/lib/environment/api'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
-import {
- environmentKeys,
- PERSONAL_ENVIRONMENT_STALE_TIME,
- WORKSPACE_ENVIRONMENT_STALE_TIME,
-} from '@/hooks/queries/environment'
+import { environmentKeys, WORKSPACE_ENVIRONMENT_STALE_TIME } from '@/hooks/queries/environment'
import { getSandboxListQueryOptions, type SandboxListResponse } from '@/hooks/queries/sandboxes'
import { getWorkflowListQueryOptions } from '@/hooks/queries/utils/workflow-list-query'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -43,30 +39,33 @@ export async function fetchWorkspaceWorkflowOptions(options?: {
* Loads the active workspace's secret NAMES for the Function block's secret-scope
* picker. Names only — values stay server-side and are injected at execution, the
* same discipline the copilot's workspace context uses.
+ *
+ * Both halves come from the single workspace-environment response, which is the
+ * client-side mirror of `getEffectiveDecryptedEnv` — the resolver the executor
+ * injects from. Its `personal` slice is NOT the caller's raw personal variables:
+ * under credential filtering it also carries personal secrets other members have
+ * shared into the workspace, so reading `/api/environment` instead would hide
+ * names the code can genuinely resolve. This is the same pair the `{{VAR}}`
+ * autocomplete lists, so the picker and the editor never disagree.
+ *
+ * Values are masked to `''` for non-admin viewers; only the keys are used here.
*/
export async function fetchWorkspaceSecretNameOptions(): Promise {
const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId
if (!workspaceId) return []
- const [workspace, personal] = await Promise.all([
- getQueryClient().fetchQuery({
- queryKey: environmentKeys.workspace(workspaceId),
- queryFn: ({ signal }: { signal?: AbortSignal }) =>
- fetchWorkspaceEnvironment(workspaceId, signal),
- staleTime: WORKSPACE_ENVIRONMENT_STALE_TIME,
- }),
- getQueryClient().fetchQuery({
- queryKey: environmentKeys.personal(),
- queryFn: ({ signal }: { signal?: AbortSignal }) => fetchPersonalEnvironment(signal),
- staleTime: PERSONAL_ENVIRONMENT_STALE_TIME,
- }),
- ])
+ const environment = await getQueryClient().fetchQuery({
+ queryKey: environmentKeys.workspace(workspaceId),
+ queryFn: ({ signal }: { signal?: AbortSignal }) =>
+ fetchWorkspaceEnvironment(workspaceId, signal),
+ staleTime: WORKSPACE_ENVIRONMENT_STALE_TIME,
+ })
- // Personal variables shadow workspace ones at execution, so both are offered
- // under one de-duplicated list of names.
+ // Workspace entries shadow personal ones at execution (`getEffectiveDecryptedEnv`
+ // spreads workspace last), but a name-only list just de-duplicates the union.
const names = new Set([
- ...Object.keys(workspace?.workspace?.variables ?? {}),
- ...Object.keys(personal ?? {}),
+ ...Object.keys(environment?.workspace ?? {}),
+ ...Object.keys(environment?.personal ?? {}),
])
return [...names].sort().map((name) => ({ id: name, label: name }))
}
diff --git a/packages/emcn/src/components/chip-tag/chip-tag.tsx b/packages/emcn/src/components/chip-tag/chip-tag.tsx
index 32fe4cf022f..e69ae59c429 100644
--- a/packages/emcn/src/components/chip-tag/chip-tag.tsx
+++ b/packages/emcn/src/components/chip-tag/chip-tag.tsx
@@ -12,7 +12,11 @@ import { cn } from '../../lib/cn'
* Variants, theme-aware via workspace tokens:
* - `mono` — borderless, sharing the {@link ChipSwitch} trough surface
* (`--surface-5` light / `--surface-4` dark) with strong `--text-primary` text
- * for emphasis (e.g. a discount next to a primary CTA).
+ * for emphasis (e.g. a discount next to a primary CTA). Because its fill *is*
+ * the trough/field surface, it disappears on one — use `field` there.
+ * - `field` — `mono` with the fill stepped one level away from the form-field
+ * surface (`--surface-6` light / `--surface-3` dark), so the pill stays legible
+ * when it sits *on* a `--surface-5` input, combobox trigger, or tag container.
* - `gray` — a light surface over a slightly darker inset ring with muted
* `--text-secondary` text for low-emphasis status labels.
* - `solid` — a filled inverse tag: a dark neutral surface (`--text-secondary`)
@@ -31,6 +35,8 @@ const chipTagVariants = cva(
variants: {
variant: {
mono: 'h-5 gap-[3px] px-1 bg-[var(--surface-5)] text-[var(--text-primary)] dark:bg-[var(--surface-4)]',
+ field:
+ 'h-5 gap-[3px] px-1 bg-[var(--surface-6)] text-[var(--text-primary)] dark:bg-[var(--surface-3)]',
gray: 'h-5 gap-[3px] px-1 bg-[var(--surface-5)] text-[var(--text-secondary)] shadow-[inset_0_0_0_1px_var(--border-1)]',
solid: 'h-5 gap-[3px] px-1 bg-[var(--text-secondary)] text-[var(--text-inverse)]',
invite: