From 0c51d4437a86982c18095110df22a0295f1211c9 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 13 Feb 2026 19:53:10 -0800 Subject: [PATCH] added wand for custom cron, fixed slack inconsistency --- .../content/docs/en/tools/google_books.mdx | 2 +- apps/docs/content/docs/en/tools/s3.mdx | 1 + apps/docs/content/docs/en/tools/slack.mdx | 2 +- apps/sim/app/api/wand/route.ts | 5 + .../components/tool-input/tool-input.tsx | 200 +++++++++++------- apps/sim/blocks/blocks/schedule.ts | 19 ++ apps/sim/blocks/types.ts | 1 + apps/sim/tools/params.ts | 11 +- apps/sim/tools/slack/message.ts | 4 +- apps/sim/tools/slack/types.ts | 2 +- 10 files changed, 161 insertions(+), 86 deletions(-) diff --git a/apps/docs/content/docs/en/tools/google_books.mdx b/apps/docs/content/docs/en/tools/google_books.mdx index 9baec6846..2b370d139 100644 --- a/apps/docs/content/docs/en/tools/google_books.mdx +++ b/apps/docs/content/docs/en/tools/google_books.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" ## Usage Instructions diff --git a/apps/docs/content/docs/en/tools/s3.mdx b/apps/docs/content/docs/en/tools/s3.mdx index 95715f0f1..f7780eb58 100644 --- a/apps/docs/content/docs/en/tools/s3.mdx +++ b/apps/docs/content/docs/en/tools/s3.mdx @@ -71,6 +71,7 @@ Retrieve an object from an AWS S3 bucket | --------- | ---- | -------- | ----------- | | `accessKeyId` | string | Yes | Your AWS Access Key ID | | `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | No | Optional region override when URL does not include region \(e.g., us-east-1, eu-west-1\) | | `s3Uri` | string | Yes | S3 Object URL \(e.g., https://bucket.s3.region.amazonaws.com/path/to/file\) | #### Output diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 35562a17e..0f4285e2a 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -79,7 +79,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format | `channel` | string | No | Slack channel ID \(e.g., C1234567890\) | | `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) | | `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) | -| `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) | +| `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) | | `files` | file[] | No | Files to attach to the message | #### Output diff --git a/apps/sim/app/api/wand/route.ts b/apps/sim/app/api/wand/route.ts index aba3b14ef..f6089ff1d 100644 --- a/apps/sim/app/api/wand/route.ts +++ b/apps/sim/app/api/wand/route.ts @@ -238,6 +238,11 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg finalSystemPrompt += currentTimeContext } + if (generationType === 'cron-expression') { + finalSystemPrompt += + '\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.' + } + if (generationType === 'json-object') { finalSystemPrompt += '\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 }.' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index f92b8150a..e8fe08e5b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1741,36 +1741,97 @@ export const ToolInput = memo(function ToolInput({ ) : null })()} - {requiresOAuth && oauthConfig && ( -
-
- Account * -
-
- - handleParamChange(toolIndex, 'credential', value) - } - provider={oauthConfig.provider as OAuthProvider} - requiredScopes={ - toolBlock?.subBlocks?.find((sb) => sb.id === 'credential') - ?.requiredScopes || - getCanonicalScopesForProvider(oauthConfig.provider) - } - serviceId={oauthConfig.provider} - disabled={disabled} - /> -
-
- )} - {(() => { const renderedElements: React.ReactNode[] = [] + const showOAuth = + requiresOAuth && oauthConfig && tool.params?.authMethod !== 'bot_token' + + const renderOAuthAccount = (): React.ReactNode => { + if (!showOAuth || !oauthConfig) return null + const credentialSubBlock = toolBlock?.subBlocks?.find( + (s) => s.type === 'oauth-input' + ) + return ( +
+
+ {credentialSubBlock?.title || 'Account'}{' '} + * +
+
+ + handleParamChange(toolIndex, 'credential', value) + } + provider={oauthConfig.provider as OAuthProvider} + requiredScopes={ + credentialSubBlock?.requiredScopes || + getCanonicalScopesForProvider(oauthConfig.provider) + } + serviceId={oauthConfig.provider} + disabled={disabled} + /> +
+
+ ) + } + + const renderSubBlock = (sb: BlockSubBlockConfig): React.ReactNode => { + const effectiveParamId = sb.id + const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] + const canonicalGroup = canonicalId + ? toolCanonicalIndex?.groupsById[canonicalId] + : undefined + const hasCanonicalPair = isCanonicalPair(canonicalGroup) + const canonicalMode = + canonicalGroup && hasCanonicalPair + ? resolveCanonicalMode( + canonicalGroup, + { operation: tool.operation, ...tool.params }, + toolScopedOverrides + ) + : undefined + + const canonicalToggleProp = + hasCanonicalPair && canonicalMode && canonicalId + ? { + mode: canonicalMode, + onToggle: () => { + const nextMode = canonicalMode === 'advanced' ? 'basic' : 'advanced' + collaborativeSetBlockCanonicalMode( + blockId, + `${tool.type}:${canonicalId}`, + nextMode + ) + }, + } + : undefined + + const sbWithTitle = sb.title + ? sb + : { ...sb, title: formatParameterLabel(effectiveParamId) } + + return ( + + ) + } + if (useSubBlocks && displaySubBlocks.length > 0) { + const allBlockSubBlocks = toolBlock?.subBlocks || [] const coveredParamIds = new Set( - displaySubBlocks.flatMap((sb) => { + allBlockSubBlocks.flatMap((sb) => { const ids = [sb.id] if (sb.canonicalParamId) ids.push(sb.canonicalParamId) const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] @@ -1785,57 +1846,45 @@ export const ToolInput = memo(function ToolInput({ }) ) - displaySubBlocks.forEach((sb) => { - const effectiveParamId = sb.id - const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id] - const canonicalGroup = canonicalId - ? toolCanonicalIndex?.groupsById[canonicalId] - : undefined - const hasCanonicalPair = isCanonicalPair(canonicalGroup) - const canonicalMode = - canonicalGroup && hasCanonicalPair - ? resolveCanonicalMode( - canonicalGroup, - { operation: tool.operation, ...tool.params }, - toolScopedOverrides - ) - : undefined + type RenderItem = + | { kind: 'subblock'; sb: BlockSubBlockConfig } + | { kind: 'oauth' } - const canonicalToggleProp = - hasCanonicalPair && canonicalMode && canonicalId - ? { - mode: canonicalMode, - onToggle: () => { - const nextMode = - canonicalMode === 'advanced' ? 'basic' : 'advanced' - collaborativeSetBlockCanonicalMode( - blockId, - `${tool.type}:${canonicalId}`, - nextMode - ) - }, - } - : undefined + const renderOrder: RenderItem[] = displaySubBlocks.map((sb) => ({ + kind: 'subblock' as const, + sb, + })) - const sbWithTitle = sb.title - ? sb - : { ...sb, title: formatParameterLabel(effectiveParamId) } - - renderedElements.push( - + if (showOAuth) { + const credentialIdx = allBlockSubBlocks.findIndex( + (sb) => sb.type === 'oauth-input' ) - }) + if (credentialIdx >= 0) { + const sbPositions = new Map(allBlockSubBlocks.map((sb, i) => [sb.id, i])) + const insertAt = renderOrder.findIndex( + (item) => + item.kind === 'subblock' && + (sbPositions.get(item.sb.id) ?? Number.POSITIVE_INFINITY) > + credentialIdx + ) + if (insertAt === -1) { + renderOrder.push({ kind: 'oauth' }) + } else { + renderOrder.splice(insertAt, 0, { kind: 'oauth' }) + } + } else { + renderOrder.unshift({ kind: 'oauth' }) + } + } + + for (const item of renderOrder) { + if (item.kind === 'oauth') { + const el = renderOAuthAccount() + if (el) renderedElements.push(el) + } else { + renderedElements.push(renderSubBlock(item.sb)) + } + } const uncoveredParams = displayParams.filter( (param) => @@ -1873,6 +1922,11 @@ export const ToolInput = memo(function ToolInput({ ) } + { + const el = renderOAuthAccount() + if (el) renderedElements.push(el) + } + const filteredParams = displayParams.filter((param) => evaluateParameterCondition(param, tool) ) diff --git a/apps/sim/blocks/blocks/schedule.ts b/apps/sim/blocks/blocks/schedule.ts index fb757543e..0757eca0e 100644 --- a/apps/sim/blocks/blocks/schedule.ts +++ b/apps/sim/blocks/blocks/schedule.ts @@ -122,6 +122,25 @@ export const ScheduleBlock: BlockConfig = { required: true, mode: 'trigger', condition: { field: 'scheduleType', value: 'custom' }, + wandConfig: { + enabled: true, + prompt: `You are an expert at writing cron expressions. Generate a valid cron expression based on the user's description. + +Cron format: minute hour day-of-month month day-of-week +- minute: 0-59 +- hour: 0-23 +- day-of-month: 1-31 +- month: 1-12 +- day-of-week: 0-7 (0 and 7 are Sunday) + +Special characters: * (any), , (list), - (range), / (step) + +{context} + +Return ONLY the cron expression, nothing else. No explanation, no backticks, no quotes.`, + placeholder: 'Describe your schedule (e.g., "every weekday at 9am")', + generationType: 'cron-expression', + }, }, { diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 8ac262bef..30158d734 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -40,6 +40,7 @@ export type GenerationType = | 'neo4j-parameters' | 'timestamp' | 'timezone' + | 'cron-expression' export type SubBlockType = | 'short-input' // Single line input diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index 89a9d0f8d..66bbdc292 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -827,11 +827,10 @@ export function formatParameterLabel(paramId: string): string { } /** - * SubBlock IDs that are "structural" — they control tool routing or auth, - * not user-facing parameters. These are excluded from tool-input rendering - * unless they have an explicit paramVisibility set. + * SubBlock IDs that control tool routing, not user-facing parameters. + * Excluded from tool-input rendering unless they have an explicit paramVisibility set. */ -const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation', 'authMethod', 'destinationType']) +const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation']) /** * SubBlock types that represent auth/credential inputs handled separately @@ -955,12 +954,8 @@ export function getSubBlocksForToolInput( } else if (sb.id in toolParamVisibility) { visibility = toolParamVisibility[sb.id] } else if (sb.canonicalParamId) { - // SubBlock has a canonicalParamId that doesn't directly match a tool param. - // This means the block's params() function transforms it before sending to the tool - // (e.g. listFolderId → folderId). These are user-facing inputs, default to user-or-llm. visibility = 'user-or-llm' } else { - // SubBlock has no corresponding tool param — skip it continue } } diff --git a/apps/sim/tools/slack/message.ts b/apps/sim/tools/slack/message.ts index 1fd60b685..b1f047403 100644 --- a/apps/sim/tools/slack/message.ts +++ b/apps/sim/tools/slack/message.ts @@ -57,7 +57,7 @@ export const slackMessageTool: ToolConfig