Skip to content

Fix schemas for non-string widget options - #63

Merged
dkedar7 merged 2 commits into
dkedar7:mainfrom
Sanjays2402:fix/62-widget-schema-values
Jul 19, 2026
Merged

Fix schemas for non-string widget options#63
dkedar7 merged 2 commits into
dkedar7:mainfrom
Sanjays2402:fix/62-widget-schema-values

Conversation

@Sanjays2402

Copy link
Copy Markdown
Contributor

Fixes #62.

AppTest preserves the current value's type for non-string options but stringifies the option list. Include the observed typed value alongside the accepted string forms so selectbox, radio, select-slider, and multiselect values satisfy their advertised enum.

Added a regression covering all affected widget kinds; it fails without the source change and passes with it.

Sanjays2402 and others added 2 commits July 17, 2026 04:35
Include observed typed choice values in advertised enums so select and multiselect models validate against their own schemas. Add a regression covering all affected widget kinds.\n\nFixes dkedar7#62
…one (dkedar7#62)

Completes the non-string-option schema fix. Keying the enum off the current
value alone made the reported value a member of its own enum, but left the
schema unable to express any *other* option in its typed form: a
selectbox([1,2,3]) sitting on 1 advertised {"enum": [1,"1","2","3"]}, so an
agent following the schema was told only the string form of 2 was valid —
while set_widget(id, 2) is in fact accepted (dkedar7#51). An agent must be able to
construct any valid set from what's advertised, not merely echo back the
selection it was handed.

The current value's type is still the only evidence available (the widget
protobuf carries options already stringified), but it is now used to recover
the typed form of *every* option, guarded by a str(recovered) == option
round-trip check so a typed member always denotes the option it came from.
Non-recoverable options ('two' among ints) stay string-only, and bool gets its
own mapping since bool('False') is True.

Also restores the shared TYPED_OPTIONS_APP multiselect to its no-default form
and covers that case separately: with nothing selected there is no type
evidence, so the enum stays the string form — correct and settable, just less
informative, now asserted so the fallback stays deliberate.

Resolves the merge with dkedar7#61: the selectbox/radio null special-case this
branch was written against is gone; _make_nullable now widens by value, so a
placeholder with typed options still round-trips null through the shared path.

Co-Authored-By: Sanjay Santhanam <Sanjays2402@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwy1qnSc3WhDHkXfxNb7jX
@dkedar7

dkedar7 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Thanks for this @Sanjays2402 — the core insight is right and it's the one I'd have reached for: the widget protobuf carries options already stringified (options: "2023"), so the current value's type really is the only evidence of the true option type available at runtime. I confirmed that directly against AppTest before touching anything.

I've pushed two commits on top (maintainer edits were enabled — shout if you'd rather I hadn't) to close two gaps:

1. The enum only expressed the selected option in its typed form. On selectbox('Pick', [1,2,3]) sitting on 1, the branch advertised {"enum": [1, "1", "2", "3"]}. The reported value is a member of its own enum — the invariant #62 names — but an agent that wants to select 2 is told only "2" is valid, while set_widget(id, 2) is in fact accepted (#51). Since the point of the schema is to let an agent construct any valid set, the typed form is now recovered for every option: {"enum": [1, 2, 3, "1", "2", "3"]}, which is also what the issue's suggested fix asked for.

That recovery is guarded by a str(recovered) == option round-trip check, so a typed member always denotes the option it came from. It matters for two cases the direct approach would get wrong: mixed lists ([1, 'two', 3.0]'two' and '3.0' stay string-only rather than being dropped or mis-parsed), and bools — bool('False') is True, which would have advertised a member the widget rejects.

2. The default=[2] added to TYPED_OPTIONS_APP. That edit made the multiselect assertion pass, but it also removed the only no-selection case in the fixture — and no-selection is exactly where this approach has nothing to key off. I've restored the original st.multiselect('Nums', [1,2,3]), added a separate picked widget with a default, and asserted the empty case explicitly: its enum stays the string form, which is still correct and settable (verified set_widget('nums', ['2'])[2]), just less informative. Documented as a deliberate limitation rather than left as a silent hole.

I also resolved the merge against #61, which landed a few minutes ago — the selectbox/radio null special-case this branch was written against is gone, replaced by a value-keyed _make_nullable, so a placeholder with typed options still round-trips null through the shared path.

Verified on the CLI and over the real MCP stdio transport (this project's bar for a dogfood fix), plus 6 regression tests — 3 of which fail against the unfixed schema path.

Merging this as yours. Genuinely useful contribution — thanks for digging into a subtle one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants