render: editable CheckBox, ChoicePicker, Slider, and DateTimeInput (finish #15) - #52
Merged
Conversation
Finish what #15 started: every input component now joins the focus ring and edits live, with edited state shadowing the static literal for both rendering and value readout, mirroring the hand-rolled TextField path. - CheckBox: Space/Enter toggles; toggling a bound value replaces the {binding} placeholder with real local state. - ChoicePicker: Up/Down move a highlight (clamped, no wrap), Space toggles the highlighted option; multipleSelection toggles membership while single-select replaces the selection (radio semantics). The edited value is a []string normalized to option-declaration order, honoring the native DynamicStringList shape. - Slider: Left/Right step within [min, max]; the v0.9 schema has no step field, so spans >= 1 step by 1 and smaller spans step by span/16 (one bar cell). Bound values seed from min. - DateTimeInput: reuses the TextField rune-edit path against its string value (appendText/deleteRune/editSeed), including the cleared-to-empty "(unset)" semantics. Focus chrome stays monochrome: focused inputs mark their value line with the same "▎" cue as the TextField cursor, and a focused picker draws a "▏" gutter with "▎" on the highlighted option. Idle rendering is unchanged. Edited values flow through the existing dispatch-time context gathering, so a Button's ActionEvent.Context now carries the edited bool/[]string/float64/string values; gatherFieldValues also reports Slider and DateTimeInput literals when unedited, matching the other kinds. deleteSurface clears the new edit state alongside fieldValues. The activateButtonCtx test helper no longer presses Enter on every focusable while hunting for the button — Enter now toggles a focused CheckBox, so it tabs until focus lands on an actual Button. Fixes #42. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
joestump
commented
Jul 18, 2026
joestump
left a comment
Collaborator
Author
There was a problem hiding this comment.
Adversarial review against the #42 Done-when. Verified locally at a914437: go build ./... && go vet ./... && go test ./... green, worktree clean.
Verdict: no blockers
What I actively hunted and confirmed correct:
- Key routing — traced
Key.String()through bubbletea v2 → ultraviolet: space genuinely reports"space"(never" "), so the explicitcase "space"inrender/render.go:259is required and correct, and its dual role holds: toggle on CheckBox/ChoicePicker, literal" "insertion on TextField/DateTimeInput (TestSpaceStillInsertsIntoTextFieldguards this). Enter still activates buttons first (render/render.go:253) and only then falls to the CheckBox toggle; arrows only act on the matching focused component, so nothing steals keys from text editing. - State across Apply merges —
applyComponents(render/composite.go:71) never touches the edit maps and re-anchors focus by ID;DeleteSurface(render/composite.go:49-52) clears all four new maps. Ran extra probes beyond the PR's tests: options shrinking under a live cursor (cursor clamps at read time inpickerCursor, no panic, next Space toggles a valid option), zero-option picker (guarded, no panic), slider literal outside[min,max](first step clamps into range). - ChoicePicker multi-value — edited value is a real
[]stringnormalized to option-declaration order (togglePickerOption,render/inputs.go:64-88); single-select is radio semantics;TestButtonContextCarriesEditedInputValuesasserts theClientMessage.Contexttypes end-to-end (bool /[]string/ float64 / string). - Nil/missing guards — every edit path re-resolves
s.byID[id]and checks the component kind;focusedComponentreturns a zero Component on an empty ring; degenerate slider range (max <= min) refuses to step. - Idle-render stability — unedited/unfocused output is byte-identical: the new slider readout (
strconv.FormatFloat(_, 'f', -1, 64),render/fields.go:191) matchesdynNumString's literal branch exactly, and all focus cues are gated onisFocused. - Test quality — the new tests assert rendered content,
FieldValues, and dispatchedContext, not mere execution; theactivateButtonCtxrewrite (render/context_test.go:30-56) correctly stops blind-Enter from corrupting CheckBox state. - Ticket compliance — all Done-when items met except
website/docs/wire-format.md, which is correctly deferred to the post-merge docs pass per repo policy and noted in the PR body.render/fields.go:4comment updated.
Nits (non-blocking)
- Stale picker selection can round-trip after a merge — verified by probe: select
"a", then anupdateComponentsreplaces the picker's options with only"x";FieldValues/Contextstill report["a"], a value naming no current option. This mirrors the established TextField edit-shadowing semantics and self-heals on the next toggle (normalization drops unknown values), but a sentence in thechoiceValuesdoc comment (render/render.go:1129) acknowledging it would help the next reader. render/render.go:480— theappendTextdoc comment wraps awkwardly after the edit: "…on first edit. On the first edit," reads as an accidental duplication; reflow the paragraph.render/context.go:76— the Slider case readsc.Slider.Value.Literalinline while every sibling kind goes through aresolveDynamic*helper; a two-lineresolveDynamicNumberwould keep the pattern uniform.
🤖 Posted on behalf of @joestump by Claude.
Resolves conflicts with the modal PR (#41 train car): Surface keeps both the editable-input state maps (checkValues, choiceValues, sliderValues, choiceCursor) and openModals; Update handles Enter for modals, buttons, and checkboxes plus Esc for modals; isInteractive now includes Modal so the focus ring covers buttons, input components, and modals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
joestump
added a commit
that referenced
this pull request
Jul 18, 2026
Union merge of interactive tab switching (#45) with the five feature PRs that landed on main since the branch forked (#48 createSurface no-op, #49 ChildList templates, #51 Modal open/close, #52 editable inputs, #53 InputSubmitted/ChoiceSelected dispatch). Nothing from either side was dropped: - render/render.go: Surface keeps ALL state — activeTabs (branch) plus fieldValues/checkValues/choiceValues/sliderValues/choiceCursor/ openModals/scope (main). isInteractive now also covers non-empty Tabs bars. collectFocusables walks only the ACTIVE tab's subtree, so inputs and modals inside inactive tabs are excluded from the focus ring, and only an open modal's content joins it. Key routing: left/right step a focused slider and switch tabs on a focused tab bar; h/l switch tabs only on the tab bar and stay literal runes on a focused TextField/DateTimeInput; enter/space/esc keep main's button/checkbox/choice/modal/InputSubmitted behavior. - render/composite.go: deleteSurface clears every state map including activeTabs; applyComponents preserves active-tab, focus, modal, and edited-value state across merges, with out-of-range tabs clamping to the first at read time. - render/tabs_test.go: two new tests pin the union — slider-focused vs tab-bar-focused left/right disambiguation, and inputs/modals inside an inactive tab excluded from the focus ring. - README.md / docs/wire-format.md: merged feature descriptions; the 'not yet' lists are now empty (issues #42–#47 all landed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Finishes the editing work #15 started: the four remaining input components are now focusable and editable, and their edited values round-trip to the agent in a Button's
ActionEvent.Context.{binding}placeholder with real local state.multipleSelectiontoggles membership; single-select replaces the selection (radio semantics). The edited value is a[]stringnormalized to option-declaration order, honoring the nativeDynamicStringListshape.[min, max]. The v0.9 schema has no step field, so spans >= 1 step by 1 and smaller spans step by span/16 (one bar cell), keeping fractional sliders adjustable. Bound values seed from min.appendText/deleteRune/editSeed), including cleared-to-empty "(unset)" semantics. No separate editor was needed.How
Each component mirrors the existing hand-rolled TextField pattern in
render/render.gorather than importing bubbles models: edits live in lazily initialized maps onSurface(checkValues,choiceValues,sliderValues, pluschoiceCursorfor the picker highlight; DateTimeInput sharesfieldValues) and shadow the static literal for both rendering and value readout. The new edit paths live inrender/inputs.go;collectFocusablesnow admits all input kinds via anisInteractivehelper;gatherFieldValuesreads edited state first so dispatch-time context gathering carries bool/[]string/float64/string values (Slider and DateTimeInput literals are now also reported when unedited, matching the other kinds).deleteSurfaceclears the new state; edits surviveApplymerges like TextField edits do.Chrome stays monochrome: focused inputs mark their value line with the same "▎" cue as the TextField cursor, and a focused picker draws a "▏" gutter with "▎" on the highlighted option. Idle rendering is byte-identical to before. The stale comment at
render/fields.go:4and the package/event docs are updated.The
activateButtonCtxtest helper previously pressed Enter on every focusable while hunting for the button; Enter now toggles a focused CheckBox, so it tabs until focus is on an actual Button before activating.Tests
go build ./... && go vet ./... && go test ./...green. Newrender/inputs_test.go(17 tests) follows thetextfield_test.gopatterns: focus-ring membership and order, toggle/highlight/step behavior with live re-render assertions, clamping at bounds and degenerate ranges, binding-placeholder shadowing for CheckBox and Slider, fractional-span stepping, DateTimeInput rune editing and clear-to-"(unset)", Space's dual role (toggle vs. text input), an end-to-end check that Button activation emits aClientMessagewhoseContextcarries all four edited value types, and edit survival acrossApplymerges.Notes
website/docs/wire-format.md"Not yet" item ("Editing beyond TextField") from the Done-when is deliberately not touched here — website/ docs are handled in the dedicated post-merge docs pass.Fixes #42
🤖 Posted on behalf of
@joestumpby Claude.