Skip to content

Commit ab8d822

Browse files
fix(copilot): restore subblock options (#1430)
1 parent 5bb9b46 commit ab8d822

File tree

1 file changed

+187
-16
lines changed

1 file changed

+187
-16
lines changed

apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts

Lines changed: 187 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,44 @@ export interface CopilotSubblockMetadata {
1818
title?: string
1919
required?: boolean
2020
description?: string
21+
placeholder?: string
22+
layout?: string
23+
mode?: string
24+
hidden?: boolean
25+
condition?: any
26+
// Dropdown/combobox options
27+
options?: { id: string; label?: string; hasIcon?: boolean }[]
28+
// Numeric constraints
29+
min?: number
30+
max?: number
31+
step?: number
32+
integer?: boolean
33+
// Text input properties
34+
rows?: number
35+
password?: boolean
36+
multiSelect?: boolean
37+
// Code/generation properties
38+
language?: string
39+
generationType?: string
40+
// OAuth/credential properties
41+
provider?: string
42+
serviceId?: string
43+
requiredScopes?: string[]
44+
// File properties
45+
mimeType?: string
46+
acceptedTypes?: string
47+
multiple?: boolean
48+
maxSize?: number
49+
// Other properties
50+
connectionDroppable?: boolean
51+
columns?: string[]
52+
wandConfig?: any
53+
availableTriggers?: string[]
54+
triggerProvider?: string
55+
dependsOn?: string[]
56+
canonicalParamId?: string
57+
defaultValue?: any
58+
value?: string // 'function' if it's a function, undefined otherwise
2159
}
2260

2361
export interface CopilotToolMetadata {
@@ -38,7 +76,8 @@ export interface CopilotBlockMetadata {
3876
name: string
3977
description: string
4078
bestPractices?: string
41-
commonParameters: Record<string, any>
79+
commonParameters: CopilotSubblockMetadata[]
80+
inputs?: Record<string, any>
4281
triggerAllowed?: boolean
4382
authType?: 'OAuth' | 'API Key' | 'Bot Token'
4483
tools: CopilotToolMetadata[]
@@ -77,15 +116,16 @@ export const getBlocksMetadataServerTool: BaseServerTool<
77116

78117
if (SPECIAL_BLOCKS_METADATA[blockId]) {
79118
const specialBlock = SPECIAL_BLOCKS_METADATA[blockId]
80-
const { operationParameters } = splitParametersByOperation(
119+
const { commonParameters, operationParameters } = splitParametersByOperation(
81120
specialBlock.subBlocks || [],
82121
specialBlock.inputs || {}
83122
)
84123
metadata = {
85124
id: specialBlock.id,
86125
name: specialBlock.name,
87126
description: specialBlock.description || '',
88-
commonParameters: specialBlock.inputs || {},
127+
commonParameters: commonParameters,
128+
inputs: specialBlock.inputs || {},
89129
tools: [],
90130
triggers: [],
91131
operationParameters,
@@ -127,7 +167,7 @@ export const getBlocksMetadataServerTool: BaseServerTool<
127167
}
128168

129169
const blockInputs = computeBlockLevelInputs(blockConfig)
130-
const { operationParameters } = splitParametersByOperation(
170+
const { commonParameters, operationParameters } = splitParametersByOperation(
131171
Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks : [],
132172
blockInputs
133173
)
@@ -159,7 +199,8 @@ export const getBlocksMetadataServerTool: BaseServerTool<
159199
name: blockConfig.name || blockId,
160200
description: blockConfig.longDescription || blockConfig.description || '',
161201
bestPractices: blockConfig.bestPractices,
162-
commonParameters: blockInputs,
202+
commonParameters: commonParameters,
203+
inputs: blockInputs,
163204
triggerAllowed: !!blockConfig.triggerAllowed,
164205
authType: resolveAuthType(blockConfig.authMode),
165206
tools,
@@ -189,23 +230,94 @@ export const getBlocksMetadataServerTool: BaseServerTool<
189230
} catch {}
190231

191232
if (metadata) {
192-
result[blockId] = metadata as CopilotBlockMetadata
233+
result[blockId] = pruneNullishDeep(metadata) as CopilotBlockMetadata
193234
}
194235
}
195236

196237
return GetBlocksMetadataResult.parse({ metadata: result })
197238
},
198239
}
199240

200-
function simplifySubBlock(sb: any): CopilotSubblockMetadata {
201-
const simplified: CopilotSubblockMetadata = {
241+
function processSubBlock(sb: any): CopilotSubblockMetadata {
242+
// Start with required fields
243+
const processed: CopilotSubblockMetadata = {
202244
id: sb.id,
203245
type: sb.type,
204246
}
205-
if (sb.title) simplified.title = sb.title
206-
if (sb.required) simplified.required = sb.required
207-
if (sb.description) simplified.description = sb.description
208-
return simplified
247+
248+
// Process all optional fields - only add if they exist and are not null/undefined
249+
const optionalFields = {
250+
// Basic properties
251+
title: sb.title,
252+
required: sb.required,
253+
description: sb.description,
254+
placeholder: sb.placeholder,
255+
layout: sb.layout,
256+
mode: sb.mode,
257+
hidden: sb.hidden,
258+
canonicalParamId: sb.canonicalParamId,
259+
defaultValue: sb.defaultValue,
260+
261+
// Numeric constraints
262+
min: sb.min,
263+
max: sb.max,
264+
step: sb.step,
265+
integer: sb.integer,
266+
267+
// Text input properties
268+
rows: sb.rows,
269+
password: sb.password,
270+
multiSelect: sb.multiSelect,
271+
272+
// Code/generation properties
273+
language: sb.language,
274+
generationType: sb.generationType,
275+
276+
// OAuth/credential properties
277+
provider: sb.provider,
278+
serviceId: sb.serviceId,
279+
requiredScopes: sb.requiredScopes,
280+
281+
// File properties
282+
mimeType: sb.mimeType,
283+
acceptedTypes: sb.acceptedTypes,
284+
multiple: sb.multiple,
285+
maxSize: sb.maxSize,
286+
287+
// Other properties
288+
connectionDroppable: sb.connectionDroppable,
289+
columns: sb.columns,
290+
wandConfig: sb.wandConfig,
291+
availableTriggers: sb.availableTriggers,
292+
triggerProvider: sb.triggerProvider,
293+
dependsOn: sb.dependsOn,
294+
}
295+
296+
// Add non-null optional fields
297+
for (const [key, value] of Object.entries(optionalFields)) {
298+
if (value !== undefined && value !== null) {
299+
;(processed as any)[key] = value
300+
}
301+
}
302+
303+
// Handle condition normalization
304+
const condition = normalizeCondition(sb.condition)
305+
if (condition !== undefined) {
306+
processed.condition = condition
307+
}
308+
309+
// Handle value field (check if it's a function)
310+
if (typeof sb.value === 'function') {
311+
processed.value = 'function'
312+
}
313+
314+
// Process options with icon detection
315+
const options = resolveSubblockOptions(sb)
316+
if (options) {
317+
processed.options = options
318+
}
319+
320+
return processed
209321
}
210322

211323
function resolveAuthType(
@@ -218,6 +330,65 @@ function resolveAuthType(
218330
return undefined
219331
}
220332

333+
function resolveSubblockOptions(
334+
sb: any
335+
): { id: string; label?: string; hasIcon?: boolean }[] | undefined {
336+
try {
337+
// Resolve options if it's a function
338+
const rawOptions = typeof sb.options === 'function' ? sb.options() : sb.options
339+
if (!Array.isArray(rawOptions)) return undefined
340+
341+
const normalized = rawOptions
342+
.map((opt: any) => {
343+
if (!opt) return undefined
344+
345+
// Handle both string and object options
346+
const id = typeof opt === 'object' ? opt.id : opt
347+
if (id === undefined || id === null) return undefined
348+
349+
const result: { id: string; label?: string; hasIcon?: boolean } = {
350+
id: String(id),
351+
}
352+
353+
// Add label if present
354+
if (typeof opt === 'object' && typeof opt.label === 'string') {
355+
result.label = opt.label
356+
}
357+
358+
// Check for icon presence
359+
if (typeof opt === 'object' && opt.icon) {
360+
result.hasIcon = true
361+
}
362+
363+
return result
364+
})
365+
.filter((o): o is { id: string; label?: string; hasIcon?: boolean } => o !== undefined)
366+
367+
return normalized.length > 0 ? normalized : undefined
368+
} catch {
369+
return undefined
370+
}
371+
}
372+
373+
function pruneNullishDeep<T>(value: T): T {
374+
if (value === null || value === undefined) return value
375+
if (Array.isArray(value)) {
376+
const prunedArray = (value as unknown[])
377+
.map((v) => pruneNullishDeep(v))
378+
.filter((v) => v !== undefined && v !== null)
379+
return prunedArray as unknown as T
380+
}
381+
if (typeof value === 'object') {
382+
const output: Record<string, any> = {}
383+
for (const [k, v] of Object.entries(value as Record<string, any>)) {
384+
const pruned = pruneNullishDeep(v)
385+
if (pruned !== undefined && pruned !== null) output[k] = pruned
386+
}
387+
return output as unknown as T
388+
}
389+
return value
390+
}
391+
221392
function normalizeCondition(condition: any): any | undefined {
222393
try {
223394
if (!condition) return undefined
@@ -242,14 +413,14 @@ function splitParametersByOperation(
242413

243414
for (const sb of subBlocks || []) {
244415
const cond = normalizeCondition(sb.condition)
245-
const simplified = simplifySubBlock(sb)
416+
const processed = processSubBlock(sb)
246417

247418
if (cond && cond.field === 'operation' && !cond.not && cond.value !== undefined) {
248419
const values: any[] = Array.isArray(cond.value) ? cond.value : [cond.value]
249420
for (const v of values) {
250421
const key = String(v)
251422
if (!operationParameters[key]) operationParameters[key] = []
252-
operationParameters[key].push(simplified)
423+
operationParameters[key].push(processed)
253424
}
254425
} else {
255426
// Override description from blockInputs if available (by id or canonicalParamId)
@@ -258,12 +429,12 @@ function splitParametersByOperation(
258429
for (const key of candidates) {
259430
const bi = (blockInputsForDescriptions as any)[key as string]
260431
if (bi && typeof bi.description === 'string') {
261-
simplified.description = bi.description
432+
processed.description = bi.description
262433
break
263434
}
264435
}
265436
}
266-
commonParameters.push(simplified)
437+
commonParameters.push(processed)
267438
}
268439
}
269440

0 commit comments

Comments
 (0)