d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
541 lines
19 KiB
TypeScript
541 lines
19 KiB
TypeScript
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
|
import { isHosted } from '@/lib/core/config/env-flags'
|
|
import type { SubBlockConfig } from '@/blocks/types'
|
|
|
|
export type CanonicalMode = 'basic' | 'advanced'
|
|
|
|
export interface CanonicalGroup {
|
|
canonicalId: string
|
|
basicId?: string
|
|
advancedIds: string[]
|
|
}
|
|
|
|
export interface CanonicalIndex {
|
|
groupsById: Record<string, CanonicalGroup>
|
|
canonicalIdBySubBlockId: Record<string, string>
|
|
}
|
|
|
|
export interface SubBlockCondition {
|
|
field: string
|
|
value: string | number | boolean | Array<string | number | boolean> | undefined
|
|
not?: boolean
|
|
and?: SubBlockCondition
|
|
}
|
|
|
|
export interface CanonicalModeOverrides {
|
|
[canonicalId: string]: CanonicalMode | undefined
|
|
}
|
|
|
|
export interface CanonicalValueSelection {
|
|
basicValue: unknown
|
|
advancedValue: unknown
|
|
advancedSourceId?: string
|
|
}
|
|
|
|
interface TriggerVisibilityBlockConfig {
|
|
category?: string
|
|
triggers?: {
|
|
enabled?: boolean
|
|
}
|
|
}
|
|
|
|
export function parseDependsOn(dependsOn: SubBlockConfig['dependsOn']): {
|
|
allFields: string[]
|
|
anyFields: string[]
|
|
allDependsOnFields: string[]
|
|
} {
|
|
if (!dependsOn) {
|
|
return { allFields: [], anyFields: [], allDependsOnFields: [] }
|
|
}
|
|
|
|
if (Array.isArray(dependsOn)) {
|
|
return { allFields: dependsOn, anyFields: [], allDependsOnFields: dependsOn }
|
|
}
|
|
|
|
const allFields = dependsOn.all || []
|
|
const anyFields = dependsOn.any || []
|
|
return {
|
|
allFields,
|
|
anyFields,
|
|
allDependsOnFields: [...allFields, ...anyFields],
|
|
}
|
|
}
|
|
|
|
export function 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, unknown>
|
|
if ('value' in record) return normalizeDependencyValue(record.value)
|
|
if ('id' in record) return record.id
|
|
|
|
return record
|
|
}
|
|
|
|
return rawValue
|
|
}
|
|
|
|
/**
|
|
* Build a flat map of subblock values keyed by subblock id.
|
|
*/
|
|
export function buildSubBlockValues(
|
|
subBlocks: Record<string, { value?: unknown } | null | undefined>
|
|
): Record<string, unknown> {
|
|
return Object.entries(subBlocks).reduce<Record<string, unknown>>((acc, [key, subBlock]) => {
|
|
acc[key] = subBlock?.value
|
|
return acc
|
|
}, {})
|
|
}
|
|
|
|
/**
|
|
* Build canonical group indices for a block's subblocks.
|
|
*/
|
|
export function buildCanonicalIndex(subBlocks: SubBlockConfig[]): CanonicalIndex {
|
|
const groupsById: Record<string, CanonicalGroup> = {}
|
|
const canonicalIdBySubBlockId: Record<string, string> = {}
|
|
|
|
subBlocks.forEach((subBlock) => {
|
|
if (!subBlock.canonicalParamId) return
|
|
const canonicalId = subBlock.canonicalParamId
|
|
if (!groupsById[canonicalId]) {
|
|
groupsById[canonicalId] = { canonicalId, advancedIds: [] }
|
|
}
|
|
const group = groupsById[canonicalId]
|
|
if (subBlock.mode === 'advanced' || subBlock.mode === 'trigger-advanced') {
|
|
// Deduplicate: trigger spreads may repeat the same advanced ID as the regular block
|
|
if (!group.advancedIds.includes(subBlock.id)) {
|
|
group.advancedIds.push(subBlock.id)
|
|
}
|
|
} else {
|
|
// A trigger-mode subblock must not overwrite a basicId already claimed by a non-trigger subblock.
|
|
// Blocks spread their trigger's subBlocks after their own, so the regular subblock always wins.
|
|
if (!group.basicId || subBlock.mode !== 'trigger') {
|
|
group.basicId = subBlock.id
|
|
}
|
|
}
|
|
canonicalIdBySubBlockId[subBlock.id] = canonicalId
|
|
})
|
|
|
|
return { groupsById, canonicalIdBySubBlockId }
|
|
}
|
|
|
|
/**
|
|
* Resolve if a canonical group is a swap pair (basic + advanced).
|
|
*/
|
|
export function isCanonicalPair(group?: CanonicalGroup): boolean {
|
|
return Boolean(group?.basicId && group?.advancedIds?.length)
|
|
}
|
|
|
|
/**
|
|
* Builds default canonical mode overrides for a block's subblocks.
|
|
* All canonical pairs default to `'basic'`.
|
|
*/
|
|
export function buildDefaultCanonicalModes(
|
|
subBlocks: SubBlockConfig[]
|
|
): Record<string, 'basic' | 'advanced'> {
|
|
const index = buildCanonicalIndex(subBlocks)
|
|
const modes: Record<string, 'basic' | 'advanced'> = {}
|
|
for (const group of Object.values(index.groupsById)) {
|
|
if (isCanonicalPair(group)) {
|
|
modes[group.canonicalId] = 'basic'
|
|
}
|
|
}
|
|
return modes
|
|
}
|
|
|
|
/**
|
|
* Determine the active mode for a canonical group.
|
|
*/
|
|
export function resolveCanonicalMode(
|
|
group: CanonicalGroup,
|
|
values: Record<string, unknown>,
|
|
overrides?: CanonicalModeOverrides
|
|
): CanonicalMode {
|
|
const override = overrides?.[group.canonicalId]
|
|
if (override === 'advanced' && group.advancedIds.length > 0) return 'advanced'
|
|
if (override === 'basic' && group.basicId) return 'basic'
|
|
|
|
const { basicValue, advancedValue } = getCanonicalValues(group, values)
|
|
const hasBasic = isNonEmptyValue(basicValue)
|
|
const hasAdvanced = isNonEmptyValue(advancedValue)
|
|
|
|
if (!group.basicId) return 'advanced'
|
|
if (!hasBasic && hasAdvanced) return 'advanced'
|
|
return 'basic'
|
|
}
|
|
|
|
/**
|
|
* Evaluate a subblock condition against a map of raw values.
|
|
*/
|
|
export function evaluateSubBlockCondition(
|
|
condition:
|
|
| SubBlockCondition
|
|
| ((values?: Record<string, unknown>) => SubBlockCondition)
|
|
| undefined,
|
|
values: Record<string, unknown>
|
|
): boolean {
|
|
if (!condition) return true
|
|
const actual = typeof condition === 'function' ? condition(values) : condition
|
|
const fieldValue = values[actual.field]
|
|
const valueMatch = Array.isArray(actual.value)
|
|
? fieldValue != null &&
|
|
(actual.not
|
|
? !actual.value.includes(fieldValue as any)
|
|
: actual.value.includes(fieldValue as any))
|
|
: actual.not
|
|
? fieldValue !== actual.value
|
|
: fieldValue === actual.value
|
|
const andMatch = !actual.and
|
|
? true
|
|
: (() => {
|
|
const andFieldValue = values[actual.and!.field]
|
|
const andValueMatch = Array.isArray(actual.and!.value)
|
|
? andFieldValue != null &&
|
|
(actual.and!.not
|
|
? !actual.and!.value.includes(andFieldValue as any)
|
|
: actual.and!.value.includes(andFieldValue as any))
|
|
: actual.and!.not
|
|
? andFieldValue !== actual.and!.value
|
|
: andFieldValue === actual.and!.value
|
|
return andValueMatch
|
|
})()
|
|
|
|
return valueMatch && andMatch
|
|
}
|
|
|
|
/**
|
|
* Check if a value is considered set for advanced visibility/selection.
|
|
*/
|
|
export function isNonEmptyValue(value: unknown): boolean {
|
|
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 true
|
|
}
|
|
|
|
/**
|
|
* Resolve basic and advanced values for a canonical group.
|
|
*/
|
|
export function getCanonicalValues(
|
|
group: CanonicalGroup,
|
|
values: Record<string, unknown>
|
|
): CanonicalValueSelection {
|
|
const basicValue = group.basicId ? values[group.basicId] : undefined
|
|
let advancedValue: unknown
|
|
let advancedSourceId: string | undefined
|
|
|
|
group.advancedIds.forEach((advancedId) => {
|
|
if (advancedValue !== undefined) return
|
|
const candidate = values[advancedId]
|
|
if (isNonEmptyValue(candidate)) {
|
|
advancedValue = candidate
|
|
advancedSourceId = advancedId
|
|
}
|
|
})
|
|
|
|
return { basicValue, advancedValue, advancedSourceId }
|
|
}
|
|
|
|
/**
|
|
* Resolve the ACTIVE canonical member's value for a group: the basic value in basic mode, the
|
|
* advanced value in advanced mode (per {@link resolveCanonicalMode} - honoring an explicit
|
|
* override, then the value heuristic). Strict: returns ONLY the active member's value with no
|
|
* cross-mode fallback, so a dormant mode's stale value can never leak. The single source of truth
|
|
* for "what value is live for this canonical pair" - use it instead of basic-first `||` /
|
|
* `?? 'basic'` reads or last-write-wins scans.
|
|
*/
|
|
export function resolveActiveCanonicalValue(
|
|
group: CanonicalGroup,
|
|
values: Record<string, unknown>,
|
|
overrides?: CanonicalModeOverrides
|
|
): unknown {
|
|
const mode = resolveCanonicalMode(group, values, overrides)
|
|
const { basicValue, advancedValue } = getCanonicalValues(group, values)
|
|
return mode === 'advanced' ? advancedValue : basicValue
|
|
}
|
|
|
|
/** Extract override entries matching a `${prefix}` key into a bare-`canonicalId`-keyed object. */
|
|
function extractPrefixedModes(
|
|
overrides: CanonicalModeOverrides,
|
|
prefix: string
|
|
): CanonicalModeOverrides | undefined {
|
|
let scoped: CanonicalModeOverrides | undefined
|
|
for (const [key, value] of Object.entries(overrides)) {
|
|
if (key.startsWith(prefix) && value) {
|
|
scoped = scoped ?? {}
|
|
scoped[key.slice(prefix.length)] = value
|
|
}
|
|
}
|
|
return scoped
|
|
}
|
|
|
|
/**
|
|
* Strip the `${toolIndex}:` prefix from canonical-mode override keys, returning the overrides for a
|
|
* nested tool keyed by bare `canonicalId`. An agent block stores its nested tools' modes scoped as
|
|
* `${toolIndex}:${canonicalId}` — keyed by the tool's position in the `tool-input` array, not its
|
|
* `type` — so that two tool entries of the SAME type (e.g. two Table tools on one Agent block) get
|
|
* independent canonical modes instead of colliding on a shared `${toolType}:${canonicalId}` key.
|
|
*
|
|
* Falls back to the legacy `${legacyToolType}:` prefix (the pre-instance-scoping format) when no
|
|
* index-scoped key matches, so an override saved before this scoping change isn't silently dropped -
|
|
* it keeps applying (type-shared, the old behavior) until the user re-toggles it explicitly, at which
|
|
* point it's rewritten under the new index-scoped key.
|
|
*
|
|
* Returns `undefined` when there are no overrides, no `toolIndex`, and no legacy match.
|
|
*/
|
|
export function scopeCanonicalModesForTool(
|
|
overrides: CanonicalModeOverrides | undefined,
|
|
toolIndex: number | undefined,
|
|
legacyToolType?: string
|
|
): CanonicalModeOverrides | undefined {
|
|
if (!overrides) return undefined
|
|
const scoped =
|
|
toolIndex !== undefined ? extractPrefixedModes(overrides, `${toolIndex}:`) : undefined
|
|
if (scoped) return scoped
|
|
return legacyToolType ? extractPrefixedModes(overrides, `${legacyToolType}:`) : undefined
|
|
}
|
|
|
|
const INDEX_SCOPED_KEY = /^(\d+):(.+)$/
|
|
|
|
/**
|
|
* Canonical-mode overrides are keyed by a tool's position in its `tool-input` array
|
|
* (`${toolIndex}:${canonicalId}`), so anything that reorders or removes tools - the editor
|
|
* (drag-reorder, remove, delete), fork/promote copy (dropping an unresolved custom-tool/MCP
|
|
* entry) - must carry each surviving tool's overrides to its new position and DROP the
|
|
* vacated index. Otherwise a saved basic/advanced choice can attach to whichever DIFFERENT
|
|
* tool later lands on that old index (e.g. a newly-added tool, always appended at the end,
|
|
* can refill a slot a removal just freed).
|
|
*
|
|
* Returns the full replacement `canonicalModes` object (for an atomic whole-map write - a
|
|
* per-key merge can't drop a key, and sequential per-key writes can clobber each other when
|
|
* two tools swap positions), or `undefined` when nothing needs to change. A legacy,
|
|
* non-index-scoped key (the `${toolType}:` fallback format) isn't tied to any array position,
|
|
* so it's carried over unchanged.
|
|
*/
|
|
export function reindexCanonicalModesByPosition(
|
|
newIndexByOldIndex: ReadonlyMap<number, number>,
|
|
overrides: CanonicalModeOverrides | undefined
|
|
): Record<string, 'basic' | 'advanced'> | undefined {
|
|
if (!overrides) return undefined
|
|
|
|
let changed = false
|
|
const result: Record<string, 'basic' | 'advanced'> = {}
|
|
for (const [key, mode] of Object.entries(overrides)) {
|
|
if (!mode) continue
|
|
const match = INDEX_SCOPED_KEY.exec(key)
|
|
if (!match) {
|
|
result[key] = mode
|
|
continue
|
|
}
|
|
const newIndex = newIndexByOldIndex.get(Number(match[1]))
|
|
if (newIndex === undefined) {
|
|
changed = true // Tool removed (or an already-stale index) - drop the key.
|
|
continue
|
|
}
|
|
if (newIndex !== Number(match[1])) changed = true
|
|
result[`${newIndex}:${match[2]}`] = mode
|
|
}
|
|
return changed ? result : undefined
|
|
}
|
|
|
|
/**
|
|
* {@link reindexCanonicalModesByPosition}, diffing `oldTools` against `newTools` by OBJECT
|
|
* IDENTITY to derive the old-index -> new-index map. Callers must not clone the tool objects
|
|
* they keep (only filter/splice/reorder the array itself) - a kept-but-cloned tool (e.g. a
|
|
* `{ ...tool, someField: x }` spread) won't match its old reference and will be treated as
|
|
* removed. Use {@link reindexCanonicalModesByPosition} directly when a caller's remap can
|
|
* clone kept entries (fork/promote does, to rewrite a remapped id) and already knows each
|
|
* surviving old index's new position by other means (e.g. tracking it during the same pass
|
|
* that builds the new array, rather than post-hoc identity comparison).
|
|
*/
|
|
export function reindexToolCanonicalModes<T>(
|
|
oldTools: readonly T[],
|
|
newTools: readonly T[],
|
|
overrides: CanonicalModeOverrides | undefined
|
|
): Record<string, 'basic' | 'advanced'> | undefined {
|
|
const newIndexByRef = new Map(newTools.map((tool, index) => [tool, index]))
|
|
const newIndexByOldIndex = new Map<number, number>()
|
|
oldTools.forEach((tool, oldIndex) => {
|
|
const newIndex = newIndexByRef.get(tool)
|
|
if (newIndex !== undefined) newIndexByOldIndex.set(oldIndex, newIndex)
|
|
})
|
|
return reindexCanonicalModesByPosition(newIndexByOldIndex, overrides)
|
|
}
|
|
|
|
/**
|
|
* Check if a block has any standalone advanced-only fields (not part of canonical pairs).
|
|
* These require the block-level advanced mode toggle to be visible.
|
|
*/
|
|
export function hasStandaloneAdvancedFields(
|
|
subBlocks: SubBlockConfig[],
|
|
canonicalIndex: CanonicalIndex
|
|
): boolean {
|
|
for (const subBlock of subBlocks) {
|
|
if (!isStandaloneAdvancedMode(subBlock.mode)) continue
|
|
if (!canonicalIndex.canonicalIdBySubBlockId[subBlock.id]) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* True for the modes that make a field advanced-only when it is not part of a
|
|
* canonical basic/advanced pair: a standalone `advanced` field, or a standalone
|
|
* `trigger-advanced` field (an advanced option on a trigger block). Both are
|
|
* hidden until the block-level advanced toggle is on.
|
|
*/
|
|
export function isStandaloneAdvancedMode(mode: SubBlockConfig['mode']): boolean {
|
|
return mode === 'advanced' || mode === 'trigger-advanced'
|
|
}
|
|
|
|
/**
|
|
* Check if any advanced-only or canonical advanced values are present.
|
|
*/
|
|
export function hasAdvancedValues(
|
|
subBlocks: SubBlockConfig[],
|
|
values: Record<string, unknown>,
|
|
canonicalIndex: CanonicalIndex
|
|
): boolean {
|
|
const checkedCanonical = new Set<string>()
|
|
|
|
for (const subBlock of subBlocks) {
|
|
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id]
|
|
if (canonicalId) {
|
|
const group = canonicalIndex.groupsById[canonicalId]
|
|
if (group && isCanonicalPair(group) && !checkedCanonical.has(canonicalId)) {
|
|
checkedCanonical.add(canonicalId)
|
|
const { advancedValue } = getCanonicalValues(group, values)
|
|
if (isNonEmptyValue(advancedValue)) return true
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (isStandaloneAdvancedMode(subBlock.mode) && isNonEmptyValue(values[subBlock.id])) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Determine whether a subblock is visible based on mode and canonical swaps.
|
|
*/
|
|
export function isSubBlockVisibleForMode(
|
|
subBlock: SubBlockConfig,
|
|
displayAdvancedOptions: boolean,
|
|
canonicalIndex: CanonicalIndex,
|
|
values: Record<string, unknown>,
|
|
overrides?: CanonicalModeOverrides
|
|
): boolean {
|
|
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id]
|
|
const group = canonicalId ? canonicalIndex.groupsById[canonicalId] : undefined
|
|
|
|
if (group && isCanonicalPair(group)) {
|
|
const mode = resolveCanonicalMode(group, values, overrides)
|
|
if (mode === 'advanced') return group.advancedIds.includes(subBlock.id)
|
|
return group.basicId === subBlock.id
|
|
}
|
|
|
|
if (subBlock.mode === 'basic' && displayAdvancedOptions) return false
|
|
// Standalone advanced-only fields (`advanced` or a trigger's `trigger-advanced`)
|
|
// hide until the block-level advanced toggle is on.
|
|
if (isStandaloneAdvancedMode(subBlock.mode) && !displayAdvancedOptions) return false
|
|
return true
|
|
}
|
|
|
|
export function isTriggerModeSubBlock(subBlock: Pick<SubBlockConfig, 'mode'>): boolean {
|
|
return subBlock.mode === 'trigger' || subBlock.mode === 'trigger-advanced'
|
|
}
|
|
|
|
export function isTriggerConfigSubBlock(subBlock: Pick<SubBlockConfig, 'type'>): boolean {
|
|
return String(subBlock.type) === 'trigger-config'
|
|
}
|
|
|
|
export function shouldUseSubBlockForTriggerModeCanonicalIndex(
|
|
subBlock: Pick<SubBlockConfig, 'mode' | 'type'>
|
|
): boolean {
|
|
return isTriggerModeSubBlock(subBlock) || isTriggerConfigSubBlock(subBlock)
|
|
}
|
|
|
|
export function isPureTriggerBlockConfig(blockConfig?: TriggerVisibilityBlockConfig): boolean {
|
|
return Boolean(blockConfig?.triggers?.enabled && blockConfig.category === 'triggers')
|
|
}
|
|
|
|
export function isSubBlockVisibleForTriggerMode(
|
|
subBlock: Pick<SubBlockConfig, 'mode' | 'type'>,
|
|
displayTriggerMode: boolean,
|
|
blockConfig?: TriggerVisibilityBlockConfig
|
|
): boolean {
|
|
if (isTriggerConfigSubBlock(subBlock)) {
|
|
return displayTriggerMode || isPureTriggerBlockConfig(blockConfig)
|
|
}
|
|
|
|
if (isTriggerModeSubBlock(subBlock)) return displayTriggerMode
|
|
if (displayTriggerMode) return false
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Resolve the dependency value for a dependsOn key, honoring canonical swaps.
|
|
*/
|
|
export function resolveDependencyValue(
|
|
dependencyKey: string,
|
|
values: Record<string, unknown>,
|
|
canonicalIndex: CanonicalIndex,
|
|
overrides?: CanonicalModeOverrides
|
|
): unknown {
|
|
const canonicalId =
|
|
canonicalIndex.groupsById[dependencyKey]?.canonicalId ||
|
|
canonicalIndex.canonicalIdBySubBlockId[dependencyKey]
|
|
|
|
if (!canonicalId) {
|
|
return values[dependencyKey]
|
|
}
|
|
|
|
const group = canonicalIndex.groupsById[canonicalId]
|
|
if (!group) return values[dependencyKey]
|
|
|
|
const { basicValue, advancedValue } = getCanonicalValues(group, values)
|
|
const mode = resolveCanonicalMode(group, values, overrides)
|
|
const canonicalResult =
|
|
mode === 'advanced' ? (advancedValue ?? basicValue) : (basicValue ?? advancedValue)
|
|
|
|
if (canonicalResult != null) return canonicalResult
|
|
|
|
for (const [memberId, memberCanonicalId] of Object.entries(
|
|
canonicalIndex.canonicalIdBySubBlockId
|
|
)) {
|
|
if (memberCanonicalId === canonicalId && isNonEmptyValue(values[memberId])) {
|
|
return values[memberId]
|
|
}
|
|
}
|
|
|
|
return values[dependencyKey]
|
|
}
|
|
|
|
/**
|
|
* Check if a subblock is gated by a feature flag.
|
|
*/
|
|
export function isSubBlockFeatureEnabled(subBlock: SubBlockConfig): boolean {
|
|
if (!subBlock.showWhenEnvSet) return true
|
|
return isTruthy(getEnv(subBlock.showWhenEnvSet))
|
|
}
|
|
|
|
/**
|
|
* Check if a subblock should be hidden based on environment conditions.
|
|
* Covers two cases:
|
|
* - `hideWhenHosted`: hidden when running on hosted Sim (tool API key fields)
|
|
* - `hideWhenEnvSet`: hidden when a specific NEXT_PUBLIC_ env var is truthy
|
|
* (credential fields hidden when the deployment provides them server-side)
|
|
*/
|
|
export function isSubBlockHidden(subBlock: SubBlockConfig): boolean {
|
|
if (subBlock.hideWhenHosted && isHosted) return true
|
|
if (subBlock.hideWhenEnvSet && isTruthy(getEnv(subBlock.hideWhenEnvSet))) return true
|
|
return false
|
|
}
|