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
5 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) {

View file

@ -1,3 +1,31 @@
# TUI array-of-enum renders as a select on `items.enum` alone (#1751) — proof screenshots
Follow-up to #1691. The TUI `schemaToForm` array-of-enum branch was nested under
`if (property.enum)`, so a standard array-of-enums schema —
`{ type: "array", items: { enum, enumNames } }` with **no** top-level `enum` —
fell through to a plain text input. The fix keys the array case on `items.enum`
alone (matching the web `SchemaForm`). Captured non-interactively (a standalone
render of the real `ink-form` built by the real `schemaToForm`).
The demo schema has a `Sizes` array-of-enum field (`enum: [s,m,l]` +
`enumNames: [Small, Medium, Large]`, `default: "m"`, **no** top-level `enum`)
and a `Priority` single-enum control (has a top-level `enum`).
![Before #1751 — the array-of-enum field is a plain text input showing the raw default "m"](tui-1751-before-string.png)
**Before the fix** (reproducing the old nesting): `Sizes` is a plain **text
field** showing the raw default **`m`** — `enumNames` never applies because it
isn't a select at all.
![With the #1751 fix — the array-of-enum field is a select resolving "m" to its title "Medium"](tui-1751-after-select.png)
**With the fix:** `Sizes` is now a **select**; the default `m` resolves to its
title **`Medium`**. The `Priority` control (which has a top-level `enum`) shows
`High` in both — unchanged, confirming the reorder only affects the intended
`items.enum`-only case.
---
# TUI enum forms honor `enumNames` (#1691) — proof screenshots
The TUI tool-test form (`schemaToForm` → `ink-form`) rendered against a legacy

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB