added wand for custom cron, fixed slack inconsistency

This commit is contained in:
waleed
2026-02-13 19:53:10 -08:00
parent 529ff71b90
commit 0c51d4437a
10 changed files with 161 additions and 86 deletions

View File

@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_books"
color="#FFFFFF"
color="#E0E0E0"
/>
## Usage Instructions

View File

@@ -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

View File

@@ -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

View File

@@ -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 }.'

View File

@@ -1741,36 +1741,97 @@ export const ToolInput = memo(function ToolInput({
) : null
})()}
{requiresOAuth && oauthConfig && (
<div className='relative min-w-0 space-y-[6px]'>
<div className='font-medium text-[13px] text-[var(--text-primary)]'>
Account <span className='ml-0.5'>*</span>
</div>
<div className='w-full min-w-0'>
<ToolCredentialSelector
value={tool.params?.credential || ''}
onChange={(value: string) =>
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}
/>
</div>
</div>
)}
{(() => {
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 (
<div key='oauth-account' className='relative min-w-0 space-y-[6px]'>
<div className='font-medium text-[13px] text-[var(--text-primary)]'>
{credentialSubBlock?.title || 'Account'}{' '}
<span className='ml-0.5'>*</span>
</div>
<div className='w-full min-w-0'>
<ToolCredentialSelector
value={tool.params?.credential || ''}
onChange={(value: string) =>
handleParamChange(toolIndex, 'credential', value)
}
provider={oauthConfig.provider as OAuthProvider}
requiredScopes={
credentialSubBlock?.requiredScopes ||
getCanonicalScopesForProvider(oauthConfig.provider)
}
serviceId={oauthConfig.provider}
disabled={disabled}
/>
</div>
</div>
)
}
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 (
<ToolSubBlockRenderer
key={sb.id}
blockId={blockId}
subBlockId={subBlockId}
toolIndex={toolIndex}
subBlock={sbWithTitle}
effectiveParamId={effectiveParamId}
toolParams={tool.params}
onParamChange={handleParamChange}
disabled={disabled}
canonicalToggle={canonicalToggleProp}
/>
)
}
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(
<ToolSubBlockRenderer
key={sb.id}
blockId={blockId}
subBlockId={subBlockId}
toolIndex={toolIndex}
subBlock={sbWithTitle}
effectiveParamId={effectiveParamId}
toolParams={tool.params}
onParamChange={handleParamChange}
disabled={disabled}
canonicalToggle={canonicalToggleProp}
/>
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)
)

View File

@@ -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',
},
},
{

View File

@@ -40,6 +40,7 @@ export type GenerationType =
| 'neo4j-parameters'
| 'timestamp'
| 'timezone'
| 'cron-expression'
export type SubBlockType =
| 'short-input' // Single line input

View File

@@ -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
}
}

View File

@@ -57,7 +57,7 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
visibility: 'user-or-llm',
description: 'Message text to send (supports Slack mrkdwn formatting)',
},
thread_ts: {
threadTs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
@@ -84,7 +84,7 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
channel: isDM ? undefined : params.channel,
userId: isDM ? params.dmUserId : params.userId,
text: params.text,
thread_ts: params.thread_ts || undefined,
thread_ts: params.threadTs || undefined,
files: params.files || null,
}
},

View File

@@ -516,7 +516,7 @@ export interface SlackMessageParams extends SlackBaseParams {
dmUserId?: string
userId?: string
text: string
thread_ts?: string
threadTs?: string
files?: UserFile[]
}