tui: detect array-of-enum on items.enum alone (#1752)

* tui: detect array-of-enum on items.enum alone (#1751)

The TUI schemaToForm array-of-enum branch was nested under an outer
`if (property.enum)` guard, so a standard array-of-enums schema —
`{ type: "array", items: { enum: [...] } }` with no top-level `enum` — never
reached it and fell through to a plain string input. The web SchemaForm already
guards on `items?.enum` alone; this aligns the TUI with it.

Restructure the enum handling so the array-of-enums case keys on
`property.type === "array" && property.items?.enum` independently of a top-level
`enum`, falling back to the single-select `property.enum` branch otherwise. The
enumNames labeling from #1691 applies unchanged.

Tests: the array-of-enum cases now use the standard `items.enum`-only shape
(dropping the redundant top-level `enum` the old nesting required), plus a case
asserting a redundant top-level `enum` still works.

Closes #1751

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

* docs(pr-screenshots): TUI array-of-enum before/after proof shots (#1751)

Before/after of the TUI tool form for an array-of-enums field with no top-level
`enum`: before the fix it renders as a plain text field (raw "m"); with the fix
it renders as a select resolving the enumNames title ("Medium").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-07-23 19:12:45 -04:00 • committed by GitHub
commit c2b866110d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 72 additions and 28 deletions

View file

@ -57,13 +57,34 @@ describe("schemaToForm", () => {
});
});
it("builds a select field from an array-of-enum", () => {
it("builds a select field from an array-of-enum on items.enum alone", () => {
const form = schemaToForm(
{
properties: {
// Standard array-of-enums shape: options come from `items.enum` with
// NO top-level `enum`. The array branch keys on `items.enum` alone
// (matching the web guard), so this renders as a select.
tags: {
type: "array",
items: { enum: ["a", "b"] },
},
},
},
"arrayEnum",
);
expect(form.sections[0]!.fields[0]).toMatchObject({
type: "select",
options: [
{ label: "a", value: "a" },
{ label: "b", value: "b" },
],
});
});
it("still builds a select for an array-of-enum that also carries a top-level enum", () => {
const form = schemaToForm(
{
properties: {
// The array branch is nested under the outer `enum` guard, so a
// top-level `enum` must also be present for it to be reached; the
// options are taken from `items.enum`.
tags: {
type: "array",
enum: ["a", "b"],
@ -71,7 +92,7 @@ describe("schemaToForm", () => {
},
},
},
"arrayEnum",
"arrayEnumRedundant",
);
expect(form.sections[0]!.fields[0]).toMatchObject({
type: "select",
@ -133,7 +154,6 @@ describe("schemaToForm", () => {
properties: {
pets: {
type: "array",
enum: ["pet-1", "pet-2"],
items: { enum: ["pet-1", "pet-2"], enumNames: ["Cats", "Dogs"] },
},
},
@ -155,7 +175,6 @@ describe("schemaToForm", () => {
properties: {
pets: {
type: "array",
enum: ["pet-1", "pet-2"],
items: { enum: ["pet-1", "pet-2"], enumNames: ["Cats"] },
},
},

View file

@ -69,27 +69,24 @@ export function schemaToForm(
let field: FormField;
// Handle enum -> select
if (property.enum) {
if (property.type === "array" && property.items?.enum) {
// For array of enums, we'll use select but handle it differently
// Note: ink-form doesn't have multiselect, so we'll use select
field = {
type: "select",
...baseField,
options: toSelectOptions(
property.items.enum,
property.items.enumNames,
),
} as FormField;
} else {
// Single select
field = {
type: "select",
...baseField,
options: toSelectOptions(property.enum, property.enumNames),
} as FormField;
}
// Handle enum -> select. Detect the array-of-enums case on `items.enum`
// alone (matching the web SchemaForm guard) — a standard array-of-enums
// schema carries no top-level `enum`, so gating on it would drop the field
// to a plain string input.
if (property.type === "array" && property.items?.enum) {
// ink-form has no multiselect, so we render a single select.
field = {
type: "select",
...baseField,
options: toSelectOptions(property.items.enum, property.items.enumNames),
} as FormField;
} else if (property.enum) {
// Single select
field = {
type: "select",
...baseField,
options: toSelectOptions(property.enum, property.enumNames),
} as FormField;
} else {
// Map JSON Schema types to ink-form types
switch (property.type) {