@@ -132,6 +132,63 @@ export interface ListFileInput extends Input {
132132 mimetype ?: string ;
133133}
134134
135+ /**
136+ * Represents a list of select inputs.
137+ * Emitted for any array/list whose element is a select type (a primitive
138+ * literal union or a single string/number literal — e.g. `LIST<HTTP_METHOD>`,
139+ * `('GET' | 'POST')[]`) so the UI can render a dedicated multi-select instead
140+ * of the generic list input its underlying type would otherwise produce.
141+ */
142+ export interface ListSelectInput extends Omit < ListInput , 'input' > {
143+ input ?: "list-select" ;
144+ }
145+
146+ /**
147+ * Represents a list of boolean inputs.
148+ * Emitted for any array/list of plain booleans (e.g. `LIST<BOOLEAN>`,
149+ * `boolean[]`) so the UI can render a dedicated multi-boolean input instead of
150+ * the generic list of individual boolean inputs its underlying type would
151+ * otherwise produce. Carries the per-item schemas in `items`, like a generic
152+ * {@link ListInput}.
153+ */
154+ export interface ListBooleanInput extends Omit < ListInput , 'input' > {
155+ input ?: "list-boolean" ;
156+ }
157+
158+ /**
159+ * Represents a list of number inputs.
160+ * Emitted for any array/list of plain numbers (e.g. `LIST<NUMBER>`, `number[]`)
161+ * so the UI can render a dedicated multi-number input instead of the generic
162+ * list of individual number inputs its underlying type would otherwise produce.
163+ * Carries the per-item schemas in `items`, like a generic {@link ListInput}.
164+ */
165+ export interface ListNumberInput extends Omit < ListInput , 'input' > {
166+ input ?: "list-number" ;
167+ }
168+
169+ /**
170+ * Represents a list of text inputs.
171+ * Emitted for any array/list of plain strings (e.g. `LIST<TEXT>`, `string[]`)
172+ * so the UI can render a dedicated multi-text input instead of the generic list
173+ * of individual text inputs its underlying type would otherwise produce. Carries
174+ * the per-item schemas in `items`, like a generic {@link ListInput}.
175+ */
176+ export interface ListTextInput extends Omit < ListInput , 'input' > {
177+ input ?: "list-text" ;
178+ }
179+
180+ /**
181+ * Represents a list of sub-flow inputs.
182+ * Emitted for any array/list whose element is a callable/sub-flow type (e.g.
183+ * `LIST<FLOW>`, `(() => void)[]`) so the UI can render a dedicated multi-sub-flow
184+ * input instead of the generic list of individual sub-flow inputs its underlying
185+ * type would otherwise produce. Carries the per-item schemas in `items`, like a
186+ * generic {@link ListInput}.
187+ */
188+ export interface ListSubFlowInput extends Omit < ListInput , 'input' > {
189+ input ?: "list-sub-flow" ;
190+ }
191+
135192/**
136193 * Represents a data object input type with structured properties.
137194 * Includes property definitions and required field tracking.
@@ -176,6 +233,11 @@ export type Schema =
176233 | ColorInput
177234 | FileInput
178235 | ListFileInput
236+ | ListSelectInput
237+ | ListBooleanInput
238+ | ListNumberInput
239+ | ListTextInput
240+ | ListSubFlowInput
179241 | DataInput
180242 | ListInput
181243 | TypeInput
@@ -335,7 +397,8 @@ export const getSchema = (
335397 // Check primitive literal union first (e.g., "a" | "b" | "c") or a single
336398 // string/number literal (e.g., "GET"). A bare literal has only one allowed
337399 // value, so it should still surface as a select rather than a free-form text/number input.
338- if ( isPrimitiveLiteralUnion ( parameterType ) || isStringOrNumberLiteral ( parameterType ) ) {
400+ // (Boolean has already been handled above; see isSelectType.)
401+ if ( isSelectType ( parameterType ) ) {
339402 return { input : "select" , type, ...combinedSuggestions } ;
340403 }
341404 if ( isNumber ( parameterType ) ) {
@@ -364,13 +427,45 @@ export const getSchema = (
364427 return { input : "list-file" , type, mimetype, ...combinedSuggestions } ;
365428 }
366429
430+ // Per-item schemas, computed the same way for a generic list and a
431+ // list-select (whose `items` mirror a normal list's). A union element is
432+ // split into one schema per member; a single element yields one schema.
367433 const itemSchemas = itemTypes . flatMap ( itemType => {
368- const itemTypes = itemType . isUnion ( ) ? itemType . types : [ itemType ] ;
369- return itemTypes . map ( ( itemType ) =>
370- getSchema ( checker , node , itemType , functionDeclarations , functions , suggestions , undefined , visited , recursionCache )
434+ const memberTypes = itemType . isUnion ( ) ? itemType . types : [ itemType ] ;
435+ return memberTypes . map ( ( memberType ) =>
436+ getSchema ( checker , node , memberType , functionDeclarations , functions , suggestions , undefined , visited , recursionCache )
371437 )
372438 } )
373439
440+ // A list of a select type (LIST<HTTP_METHOD>, ('GET' | 'POST')[], ...)
441+ // surfaces a dedicated multi-select. Its `items` are the element schemas,
442+ // exactly like a generic list — only the input kind differs so the UI can
443+ // render a combined multi-select. Checked before the plain-primitive
444+ // cases below because a single literal (e.g. LIST<1>) is a select, not a
445+ // plain number.
446+ if ( itemTypes . length === 1 && isSelectType ( itemTypes [ 0 ] ) ) {
447+ return { input : "list-select" , type, items : itemSchemas , ...combinedSuggestions } ;
448+ }
449+
450+ // A homogeneous list of a plain primitive surfaces a dedicated
451+ // multi-<primitive> input. Its `items` are the element schemas, exactly
452+ // like a generic list — only the input kind differs so the UI can render
453+ // a combined multi-<primitive> input. Ordering mirrors the top-level
454+ // primitive checks: boolean first, then number, then string — select
455+ // literals have already been handled above. Custom-input elements
456+ // (DATE → date, COLOR → color, FILE → file) are structurally
457+ // intersection/object types that fail these checks, so they keep the
458+ // per-item schema of the generic list below.
459+ if ( itemTypes . length === 1 ) {
460+ const element = itemTypes [ 0 ] ;
461+ if ( isBoolean ( element ) ) return { input : "list-boolean" , type, items : itemSchemas , ...combinedSuggestions } ;
462+ if ( isNumber ( element ) ) return { input : "list-number" , type, items : itemSchemas , ...combinedSuggestions } ;
463+ if ( isString ( element ) ) return { input : "list-text" , type, items : itemSchemas , ...combinedSuggestions } ;
464+ // A homogeneous list of a callable/sub-flow element surfaces a
465+ // dedicated multi-sub-flow input. Checked after the primitives (none
466+ // of which are callable) and before the generic list fallback.
467+ if ( isSubFlow ( element ) ) return { input : "list-sub-flow" , type, items : itemSchemas , ...combinedSuggestions } ;
468+ }
374469
375470 return {
376471 input : "list" ,
@@ -484,6 +579,19 @@ export const getSchema = (
484579 * @param nodeSchema - The schema derived from the node's concrete (narrowed) parameter type
485580 * @returns A single merged schema
486581 */
582+ // Every list input kind whose schema carries per-element `items` — the generic
583+ // list and all specialized list-* variants that mirror it. mergeSchemas recurses
584+ // into these so element-level suggestions are preserved. list-file is excluded:
585+ // it carries a `mimetype`, not `items`.
586+ const LIST_INPUTS = new Set < string > ( [
587+ "list" ,
588+ "list-select" ,
589+ "list-boolean" ,
590+ "list-number" ,
591+ "list-text" ,
592+ "list-sub-flow" ,
593+ ] ) ;
594+
487595export const mergeSchemas = (
488596 functionSchema : Schema | undefined ,
489597 nodeSchema : Schema ,
@@ -520,10 +628,20 @@ export const mergeSchemas = (
520628 } ;
521629 }
522630
523- if ( functionSchema . input === "list" ) {
524- const fItems = functionSchema . items ?? [ ] ;
631+ // The generic list and every specialized list-* variant (list-select,
632+ // list-boolean/number/text, list-sub-flow) carry their element schemas in
633+ // `items`. Merge those pairwise so element-level suggestions — e.g. the
634+ // per-literal values on a list-select or the sub-flow function suggestions on
635+ // a LIST<CONSUMER<T>> element — survive the merge instead of being dropped in
636+ // favour of the suggestion-less function schema. Suggestions must never be
637+ // lost, whatever the list kind. Only merges when both sides are the same list
638+ // kind. (list-file carries a `mimetype`, not `items`, so it is not listed.)
639+ if ( LIST_INPUTS . has ( functionSchema . input as string ) ) {
640+ const fItems = ( functionSchema as ListInput ) . items ?? [ ] ;
525641 const nItems =
526- nodeSchema . input === "list" ? ( nodeSchema . items ?? [ ] ) : [ ] ;
642+ nodeSchema . input === functionSchema . input
643+ ? ( ( nodeSchema as ListInput ) . items ?? [ ] )
644+ : [ ] ;
527645 const items =
528646 fItems . length === nItems . length && fItems . length > 0
529647 ? fItems . map ( ( f , i ) => mergeSchemas ( f , nItems [ i ] ) )
@@ -821,6 +939,23 @@ function isStringOrNumberLiteral(type: ts.Type): boolean {
821939 ) ;
822940}
823941
942+ /**
943+ * Checks whether a type surfaces as a select input.
944+ *
945+ * A select is a primitive literal union (e.g. `"a" | "b" | "c"`) or a single
946+ * string/number literal (e.g. `"GET"`). Boolean is excluded so that `true` /
947+ * `boolean` continue to render as a boolean input — mirroring the ordering in
948+ * {@link getSchema}, where the boolean check runs first. Used both for the
949+ * plain select input and to detect a {@link ListSelectInput} element.
950+ *
951+ * @param type - The type to check
952+ * @returns True if the type surfaces as a select
953+ */
954+ function isSelectType ( type : ts . Type ) : boolean {
955+ if ( isBoolean ( type ) ) return false ;
956+ return isPrimitiveLiteralUnion ( type ) || isStringOrNumberLiteral ( type ) ;
957+ }
958+
824959/**
825960 * Checks if a type is a union of primitive types only.
826961 *
0 commit comments