Skip to content

Commit 5b0532d

Browse files
authored
refactor(tool-input): replace bidirectional effects with zustand subscription (#3215)
* refactor(tool-input): replace bidirectional effects with zustand subscription * added wand for custom cron, fixed slack inconsistency * fix slack
1 parent 3ef6b05 commit 5b0532d

File tree

12 files changed

+193
-127
lines changed

12 files changed

+193
-127
lines changed

apps/docs/content/docs/en/tools/google_books.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
77

88
<BlockInfoCard
99
type="google_books"
10-
color="#FFFFFF"
10+
color="#E0E0E0"
1111
/>
1212

1313
## Usage Instructions

apps/docs/content/docs/en/tools/s3.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Retrieve an object from an AWS S3 bucket
7171
| --------- | ---- | -------- | ----------- |
7272
| `accessKeyId` | string | Yes | Your AWS Access Key ID |
7373
| `secretAccessKey` | string | Yes | Your AWS Secret Access Key |
74+
| `region` | string | No | Optional region override when URL does not include region \(e.g., us-east-1, eu-west-1\) |
7475
| `s3Uri` | string | Yes | S3 Object URL \(e.g., https://bucket.s3.region.amazonaws.com/path/to/file\) |
7576

7677
#### Output

apps/docs/content/docs/en/tools/slack.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
7979
| `channel` | string | No | Slack channel ID \(e.g., C1234567890\) |
8080
| `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) |
8181
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
82-
| `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) |
82+
| `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) |
8383
| `files` | file[] | No | Files to attach to the message |
8484

8585
#### Output

apps/sim/app/api/wand/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
238238
finalSystemPrompt += currentTimeContext
239239
}
240240

241+
if (generationType === 'cron-expression') {
242+
finalSystemPrompt +=
243+
'\n\nIMPORTANT: Return ONLY the raw cron expression (e.g., "0 9 * * 1-5"). Do NOT wrap it in markdown code blocks, backticks, or quotes. Do NOT include any explanation or text before or after the expression.'
244+
}
245+
241246
if (generationType === 'json-object') {
242247
finalSystemPrompt +=
243248
'\n\nIMPORTANT: Return ONLY the raw JSON object. Do NOT wrap it in markdown code blocks (no ```json or ```). Do NOT include any explanation or text before or after the JSON. The response must start with { and end with }.'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client'
22

33
import { useEffect, useRef } from 'react'
4-
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
54
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
65
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
6+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
7+
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
78

89
interface ToolSubBlockRendererProps {
910
blockId: string
@@ -44,53 +45,43 @@ export function ToolSubBlockRenderer({
4445
canonicalToggle,
4546
}: ToolSubBlockRendererProps) {
4647
const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}`
47-
const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId)
48-
4948
const toolParamValue = toolParams?.[effectiveParamId] ?? ''
5049
const isObjectType = OBJECT_SUBBLOCK_TYPES.has(subBlock.type)
5150

52-
const lastPushedToStoreRef = useRef<string | null>(null)
53-
const lastPushedToParamsRef = useRef<string | null>(null)
51+
const syncedRef = useRef<string | null>(null)
52+
const onParamChangeRef = useRef(onParamChange)
53+
onParamChangeRef.current = onParamChange
5454

5555
useEffect(() => {
56-
if (!toolParamValue && lastPushedToStoreRef.current === null) {
57-
lastPushedToStoreRef.current = toolParamValue
58-
lastPushedToParamsRef.current = toolParamValue
59-
return
60-
}
61-
if (toolParamValue !== lastPushedToStoreRef.current) {
62-
lastPushedToStoreRef.current = toolParamValue
63-
lastPushedToParamsRef.current = toolParamValue
64-
65-
if (isObjectType && typeof toolParamValue === 'string' && toolParamValue) {
66-
try {
67-
const parsed = JSON.parse(toolParamValue)
68-
if (typeof parsed === 'object' && parsed !== null) {
69-
setStoreValue(parsed)
70-
return
71-
}
72-
} catch {
73-
// Not valid JSON — fall through to set as string
74-
}
75-
}
76-
setStoreValue(toolParamValue)
77-
}
78-
}, [toolParamValue, setStoreValue, isObjectType])
56+
const unsub = useSubBlockStore.subscribe((state, prevState) => {
57+
const wfId = useWorkflowRegistry.getState().activeWorkflowId
58+
if (!wfId) return
59+
const newVal = state.workflowValues[wfId]?.[blockId]?.[syntheticId]
60+
const oldVal = prevState.workflowValues[wfId]?.[blockId]?.[syntheticId]
61+
if (newVal === oldVal) return
62+
const stringified =
63+
newVal == null ? '' : typeof newVal === 'string' ? newVal : JSON.stringify(newVal)
64+
if (stringified === syncedRef.current) return
65+
syncedRef.current = stringified
66+
onParamChangeRef.current(toolIndex, effectiveParamId, stringified)
67+
})
68+
return unsub
69+
}, [blockId, syntheticId, toolIndex, effectiveParamId])
7970

8071
useEffect(() => {
81-
if (storeValue == null && lastPushedToParamsRef.current === null) return
82-
const stringValue =
83-
storeValue == null
84-
? ''
85-
: typeof storeValue === 'string'
86-
? storeValue
87-
: JSON.stringify(storeValue)
88-
if (stringValue !== lastPushedToParamsRef.current) {
89-
lastPushedToParamsRef.current = stringValue
90-
lastPushedToStoreRef.current = stringValue
91-
onParamChange(toolIndex, effectiveParamId, stringValue)
72+
if (toolParamValue === syncedRef.current) return
73+
syncedRef.current = toolParamValue
74+
if (isObjectType && toolParamValue) {
75+
try {
76+
const parsed = JSON.parse(toolParamValue)
77+
if (typeof parsed === 'object' && parsed !== null) {
78+
useSubBlockStore.getState().setValue(blockId, syntheticId, parsed)
79+
return
80+
}
81+
} catch {}
9282
}
93-
}, [storeValue, toolIndex, effectiveParamId, onParamChange])
83+
useSubBlockStore.getState().setValue(blockId, syntheticId, toolParamValue)
84+
}, [toolParamValue, blockId, syntheticId, isObjectType])
9485

9586
const visibility = subBlock.paramVisibility ?? 'user-or-llm'
9687
const isOptionalForUser = visibility !== 'user-only'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 127 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,36 +1741,97 @@ export const ToolInput = memo(function ToolInput({
17411741
) : null
17421742
})()}
17431743

1744-
{requiresOAuth && oauthConfig && (
1745-
<div className='relative min-w-0 space-y-[6px]'>
1746-
<div className='font-medium text-[13px] text-[var(--text-primary)]'>
1747-
Account <span className='ml-0.5'>*</span>
1748-
</div>
1749-
<div className='w-full min-w-0'>
1750-
<ToolCredentialSelector
1751-
value={tool.params?.credential || ''}
1752-
onChange={(value: string) =>
1753-
handleParamChange(toolIndex, 'credential', value)
1754-
}
1755-
provider={oauthConfig.provider as OAuthProvider}
1756-
requiredScopes={
1757-
toolBlock?.subBlocks?.find((sb) => sb.id === 'credential')
1758-
?.requiredScopes ||
1759-
getCanonicalScopesForProvider(oauthConfig.provider)
1760-
}
1761-
serviceId={oauthConfig.provider}
1762-
disabled={disabled}
1763-
/>
1764-
</div>
1765-
</div>
1766-
)}
1767-
17681744
{(() => {
17691745
const renderedElements: React.ReactNode[] = []
17701746

1747+
const showOAuth =
1748+
requiresOAuth && oauthConfig && tool.params?.authMethod !== 'bot_token'
1749+
1750+
const renderOAuthAccount = (): React.ReactNode => {
1751+
if (!showOAuth || !oauthConfig) return null
1752+
const credentialSubBlock = toolBlock?.subBlocks?.find(
1753+
(s) => s.type === 'oauth-input'
1754+
)
1755+
return (
1756+
<div key='oauth-account' className='relative min-w-0 space-y-[6px]'>
1757+
<div className='font-medium text-[13px] text-[var(--text-primary)]'>
1758+
{credentialSubBlock?.title || 'Account'}{' '}
1759+
<span className='ml-0.5'>*</span>
1760+
</div>
1761+
<div className='w-full min-w-0'>
1762+
<ToolCredentialSelector
1763+
value={tool.params?.credential || ''}
1764+
onChange={(value: string) =>
1765+
handleParamChange(toolIndex, 'credential', value)
1766+
}
1767+
provider={oauthConfig.provider as OAuthProvider}
1768+
requiredScopes={
1769+
credentialSubBlock?.requiredScopes ||
1770+
getCanonicalScopesForProvider(oauthConfig.provider)
1771+
}
1772+
serviceId={oauthConfig.provider}
1773+
disabled={disabled}
1774+
/>
1775+
</div>
1776+
</div>
1777+
)
1778+
}
1779+
1780+
const renderSubBlock = (sb: BlockSubBlockConfig): React.ReactNode => {
1781+
const effectiveParamId = sb.id
1782+
const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
1783+
const canonicalGroup = canonicalId
1784+
? toolCanonicalIndex?.groupsById[canonicalId]
1785+
: undefined
1786+
const hasCanonicalPair = isCanonicalPair(canonicalGroup)
1787+
const canonicalMode =
1788+
canonicalGroup && hasCanonicalPair
1789+
? resolveCanonicalMode(
1790+
canonicalGroup,
1791+
{ operation: tool.operation, ...tool.params },
1792+
toolScopedOverrides
1793+
)
1794+
: undefined
1795+
1796+
const canonicalToggleProp =
1797+
hasCanonicalPair && canonicalMode && canonicalId
1798+
? {
1799+
mode: canonicalMode,
1800+
onToggle: () => {
1801+
const nextMode = canonicalMode === 'advanced' ? 'basic' : 'advanced'
1802+
collaborativeSetBlockCanonicalMode(
1803+
blockId,
1804+
`${tool.type}:${canonicalId}`,
1805+
nextMode
1806+
)
1807+
},
1808+
}
1809+
: undefined
1810+
1811+
const sbWithTitle = sb.title
1812+
? sb
1813+
: { ...sb, title: formatParameterLabel(effectiveParamId) }
1814+
1815+
return (
1816+
<ToolSubBlockRenderer
1817+
key={sb.id}
1818+
blockId={blockId}
1819+
subBlockId={subBlockId}
1820+
toolIndex={toolIndex}
1821+
subBlock={sbWithTitle}
1822+
effectiveParamId={effectiveParamId}
1823+
toolParams={tool.params}
1824+
onParamChange={handleParamChange}
1825+
disabled={disabled}
1826+
canonicalToggle={canonicalToggleProp}
1827+
/>
1828+
)
1829+
}
1830+
17711831
if (useSubBlocks && displaySubBlocks.length > 0) {
1832+
const allBlockSubBlocks = toolBlock?.subBlocks || []
17721833
const coveredParamIds = new Set(
1773-
displaySubBlocks.flatMap((sb) => {
1834+
allBlockSubBlocks.flatMap((sb) => {
17741835
const ids = [sb.id]
17751836
if (sb.canonicalParamId) ids.push(sb.canonicalParamId)
17761837
const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
@@ -1785,57 +1846,45 @@ export const ToolInput = memo(function ToolInput({
17851846
})
17861847
)
17871848

1788-
displaySubBlocks.forEach((sb) => {
1789-
const effectiveParamId = sb.id
1790-
const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
1791-
const canonicalGroup = canonicalId
1792-
? toolCanonicalIndex?.groupsById[canonicalId]
1793-
: undefined
1794-
const hasCanonicalPair = isCanonicalPair(canonicalGroup)
1795-
const canonicalMode =
1796-
canonicalGroup && hasCanonicalPair
1797-
? resolveCanonicalMode(
1798-
canonicalGroup,
1799-
{ operation: tool.operation, ...tool.params },
1800-
toolScopedOverrides
1801-
)
1802-
: undefined
1803-
1804-
const canonicalToggleProp =
1805-
hasCanonicalPair && canonicalMode && canonicalId
1806-
? {
1807-
mode: canonicalMode,
1808-
onToggle: () => {
1809-
const nextMode =
1810-
canonicalMode === 'advanced' ? 'basic' : 'advanced'
1811-
collaborativeSetBlockCanonicalMode(
1812-
blockId,
1813-
`${tool.type}:${canonicalId}`,
1814-
nextMode
1815-
)
1816-
},
1817-
}
1818-
: undefined
1849+
type RenderItem =
1850+
| { kind: 'subblock'; sb: BlockSubBlockConfig }
1851+
| { kind: 'oauth' }
18191852

1820-
const sbWithTitle = sb.title
1821-
? sb
1822-
: { ...sb, title: formatParameterLabel(effectiveParamId) }
1853+
const renderOrder: RenderItem[] = displaySubBlocks.map((sb) => ({
1854+
kind: 'subblock' as const,
1855+
sb,
1856+
}))
18231857

1824-
renderedElements.push(
1825-
<ToolSubBlockRenderer
1826-
key={sb.id}
1827-
blockId={blockId}
1828-
subBlockId={subBlockId}
1829-
toolIndex={toolIndex}
1830-
subBlock={sbWithTitle}
1831-
effectiveParamId={effectiveParamId}
1832-
toolParams={tool.params}
1833-
onParamChange={handleParamChange}
1834-
disabled={disabled}
1835-
canonicalToggle={canonicalToggleProp}
1836-
/>
1858+
if (showOAuth) {
1859+
const credentialIdx = allBlockSubBlocks.findIndex(
1860+
(sb) => sb.type === 'oauth-input'
18371861
)
1838-
})
1862+
if (credentialIdx >= 0) {
1863+
const sbPositions = new Map(allBlockSubBlocks.map((sb, i) => [sb.id, i]))
1864+
const insertAt = renderOrder.findIndex(
1865+
(item) =>
1866+
item.kind === 'subblock' &&
1867+
(sbPositions.get(item.sb.id) ?? Number.POSITIVE_INFINITY) >
1868+
credentialIdx
1869+
)
1870+
if (insertAt === -1) {
1871+
renderOrder.push({ kind: 'oauth' })
1872+
} else {
1873+
renderOrder.splice(insertAt, 0, { kind: 'oauth' })
1874+
}
1875+
} else {
1876+
renderOrder.unshift({ kind: 'oauth' })
1877+
}
1878+
}
1879+
1880+
for (const item of renderOrder) {
1881+
if (item.kind === 'oauth') {
1882+
const el = renderOAuthAccount()
1883+
if (el) renderedElements.push(el)
1884+
} else {
1885+
renderedElements.push(renderSubBlock(item.sb))
1886+
}
1887+
}
18391888

18401889
const uncoveredParams = displayParams.filter(
18411890
(param) =>
@@ -1873,6 +1922,11 @@ export const ToolInput = memo(function ToolInput({
18731922
)
18741923
}
18751924

1925+
{
1926+
const el = renderOAuthAccount()
1927+
if (el) renderedElements.push(el)
1928+
}
1929+
18761930
const filteredParams = displayParams.filter((param) =>
18771931
evaluateParameterCondition(param, tool)
18781932
)

apps/sim/blocks/blocks/schedule.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,25 @@ export const ScheduleBlock: BlockConfig = {
122122
required: true,
123123
mode: 'trigger',
124124
condition: { field: 'scheduleType', value: 'custom' },
125+
wandConfig: {
126+
enabled: true,
127+
prompt: `You are an expert at writing cron expressions. Generate a valid cron expression based on the user's description.
128+
129+
Cron format: minute hour day-of-month month day-of-week
130+
- minute: 0-59
131+
- hour: 0-23
132+
- day-of-month: 1-31
133+
- month: 1-12
134+
- day-of-week: 0-7 (0 and 7 are Sunday)
135+
136+
Special characters: * (any), , (list), - (range), / (step)
137+
138+
{context}
139+
140+
Return ONLY the cron expression, nothing else. No explanation, no backticks, no quotes.`,
141+
placeholder: 'Describe your schedule (e.g., "every weekday at 9am")',
142+
generationType: 'cron-expression',
143+
},
125144
},
126145

127146
{

apps/sim/blocks/blocks/slack.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
604604
case 'send': {
605605
baseParams.text = text
606606
if (threadTs) {
607-
baseParams.thread_ts = threadTs
607+
baseParams.threadTs = threadTs
608608
}
609609
// files is the canonical param from attachmentFiles (basic) or files (advanced)
610610
const normalizedFiles = normalizeFileInput(files)

0 commit comments

Comments
 (0)