Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/docs/integrations/file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ With the File block, you can:
- **Compress and decompress**: Bundle files into a .zip archive or extract an archive into the workspace
- **Manage sharing**: Enable or disable a public share link for a file, with public, password, email, or SSO access modes

In Sim, the File block allows your agents to search, read, and extract text from workspace files, fetch and parse files from URLs, write or append content to files, bundle files into or out of .zip archives, and control public sharing access for a file—all programmatically as steps in a workflow. Folder selection is optional and supports multiple folders; when no folder is selected, search spans the workspace and file pickers are unscoped. Selected folders are expanded when the workflow runs, so newly added files are included automatically. This makes it possible to explore workspace content, move file content into and out of a workflow, package outputs for download or transfer, and expose files to external users through a managed share link.
In Sim, the File block allows your agents to search, read, and extract text from workspace files, fetch and parse files from URLs, write or append content to files, bundle files into or out of .zip archives, and control public sharing access for a file—all programmatically as steps in a workflow. Folder selection is optional and supports multiple folders. Pick them from the workspace, or switch the field to advanced mode and type comma-separated paths, including a value from an earlier block. When no folder is selected, search spans the workspace and file pickers are unscoped. Selected folders are expanded when the workflow runs, so newly added files are included automatically. This makes it possible to explore workspace content, move file content into and out of a workflow, package outputs for download or transfer, and expose files to external users through a managed share link.
{/* MANUAL-CONTENT-END */}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { isFileInFolderScope } from '@/lib/workspace-files/folder-path-selection
import { findSelectedWorkspaceFile } from '@/lib/workspace-files/selection'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useActiveCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value'
import { useResourceFolders } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-resource-folders'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
Expand Down Expand Up @@ -56,7 +57,7 @@ interface FileUploadProps {
* A sibling folder field that narrows what this picker offers, and the switch
* saying whether that scope descends. See `SubBlockConfig.folderScope`.
*/
folderScope?: { fieldId: string; manualFieldId?: string; recursiveFieldId?: string }
folderScope?: { fieldId: string; recursiveFieldId?: string }
/**
* Controlled value. When `onValueChange` is provided the component reads from
* this prop and writes through `onValueChange` instead of the subblock store,
Expand Down Expand Up @@ -353,20 +354,19 @@ export function FileUpload({
* a picker with no folder scope; its own value is never a folder path, so the
* scope reads as absent.
*/
const [folderScopeValue] = useSubBlockValue<unknown>(blockId, folderScope?.fieldId ?? subBlockId)
const folderScopeValue = useActiveCanonicalSubBlockValue<unknown>(
blockId,
folderScope?.fieldId ?? subBlockId
)
/*
* Through `readFolderPaths` rather than a string check so current arrays and
* legacy serialized arrays resolve to the same canonical scopes.
* Through `readFolderPaths` rather than a string check so a picked array, a
* legacy serialized array, and a typed comma-separated list all resolve to
* the same canonical scopes.
*/
const [manualFolderScopeValue] = useSubBlockValue<unknown>(
blockId,
folderScope?.manualFieldId ?? folderScope?.fieldId ?? subBlockId
const folderScopePaths = useMemo(
() => (folderScope ? readFolderPaths(folderScopeValue) : []),
[folderScope, folderScopeValue]
)
const folderScopePaths = useMemo(() => {
if (!folderScope) return []
const selectedPaths = readFolderPaths(folderScopeValue)
return selectedPaths.length > 0 ? selectedPaths : readFolderPaths(manualFolderScopeValue)
}, [folderScope, folderScopeValue, manualFolderScopeValue])

const [folderScopeRecursive] = useSubBlockValue<unknown>(
blockId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@ import { isEqual } from 'es-toolkit'
import { useStoreWithEqualityFn } from 'zustand/traditional'
import {
buildCanonicalIndexForSurface,
type CanonicalIndex,
type CanonicalModeOverrides,
resolveActiveDependencyValue,
resolveDependencyValue,
} from '@/lib/workflows/subblocks/visibility'
import { getBlock } from '@/blocks/registry'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

/**
* Read a sub-block value by either its raw subBlockId or its canonicalParamId.
*
* `useSubBlockValue` only looks up the raw subBlockId. For fields that use
* `canonicalParamId` to unify basic/advanced inputs (e.g. `tableSelector` vs
* `manualTableId` both mapping to `tableId`), this hook resolves to whichever
* member of the canonical group currently holds the value.
*/
export function useCanonicalSubBlockValue<T = unknown>(
type CanonicalResolver = (
key: string,
values: Record<string, unknown>,
canonicalIndex: CanonicalIndex,
overrides?: CanonicalModeOverrides
) => unknown

/** Subscribes to one key of a block's values, read through the given canonical resolver. */
function useResolvedSubBlockValue<T>(
blockId: string,
canonicalOrSubBlockId: string
canonicalOrSubBlockId: string,
resolve: CanonicalResolver
): T | null {
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
const blockState = useWorkflowStore((state) => state.blocks[blockId])
Expand All @@ -38,16 +42,55 @@ export function useCanonicalSubBlockValue<T = unknown>(
(state) => {
if (!activeWorkflowId) return null
const blockValues = state.workflowValues[activeWorkflowId]?.[blockId] || {}
const resolved = resolveDependencyValue(
const resolved = resolve(
canonicalOrSubBlockId,
blockValues,
canonicalIndex,
canonicalModeOverrides
)
return (resolved ?? null) as T | null
},
[activeWorkflowId, blockId, canonicalOrSubBlockId, canonicalIndex, canonicalModeOverrides]
[
activeWorkflowId,
blockId,
canonicalOrSubBlockId,
canonicalIndex,
canonicalModeOverrides,
resolve,
]
),
(a, b) => isEqual(a, b)
)
}

/**
* Read a sub-block value by either its raw subBlockId or its canonicalParamId.
*
* `useSubBlockValue` only looks up the raw subBlockId. For fields that use
* `canonicalParamId` to unify basic/advanced inputs (e.g. `tableSelector` vs
* `manualTableId` both mapping to `tableId`), this hook resolves to whichever
* member of the canonical group currently holds the value.
*/
export function useCanonicalSubBlockValue<T = unknown>(
blockId: string,
canonicalOrSubBlockId: string
): T | null {
return useResolvedSubBlockValue<T>(blockId, canonicalOrSubBlockId, resolveDependencyValue)
}

/**
* Like {@link useCanonicalSubBlockValue}, but strict: a pair answers with its
* ACTIVE member only, honoring the user's basic/advanced toggle, so a dormant
* half's stale value never leaks.
*
* This is the reading for a control that narrows itself by a sibling field,
* such as the file picker's folder scope. The serializer publishes only the
* active half, so a picker that fell back to the other one would offer a set
* the operation then ignores.
*/
export function useActiveCanonicalSubBlockValue<T = unknown>(
blockId: string,
canonicalOrSubBlockId: string
): T | null {
return useResolvedSubBlockValue<T>(blockId, canonicalOrSubBlockId, resolveActiveDependencyValue)
}
2 changes: 1 addition & 1 deletion apps/sim/blocks/blocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ describe.concurrent('Blocks Module', () => {
recursiveFieldId: 'folderIncludeSubfolders',
})
expect(block?.subBlocks.find((subBlock) => subBlock.id === 'folderSelection')?.mode).toBe(
'both'
'basic'
)
expect(block?.tools.config?.tool({ operation: 'file_read' })).toBe('file_read')
expect(block?.tools.config?.tool({ operation: 'file_get_content' })).toBe('file_get_content')
Expand Down
Loading
Loading