feat(tui): caret editing + paste normalization across input dialogs#61
Open
Magentron wants to merge 11 commits intoMiniCodeMonkey:mainfrom
Open
Conversation
Replace the hand-rolled PRD-name key handler in FirstTimeSetup with a bubbles/textinput.Model so caret editing, word-jump, and the rest of the default bindings work without us owning them. The textinput's Value() is the single source of truth; the raw prdName string field is gone. Ctrl+C, Esc, and Enter keep their existing custom semantics and are matched first; all other key messages are forwarded to ti.Update after filtering msg.Runes against [a-zA-Z0-9_-]. Init and confirmGitignore both emit textinput.Blink so the caret blinks on entry in both flows. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The `bubbles/textinput` model adopted in US-001 already implements the required key bindings (Left/Right, Home/End, Ctrl+Left/Right, Backspace at cursor, rune insertion at cursor, visible blinking caret) via its default KeyMap. This change adds a regression test suite that locks in each acceptance criterion against the FirstTimeSetup wrapper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…or unchanged
Filter both KeyRunes and KeySpace messages through filterValidPRDRunes so a
bare spacebar press is silently dropped. The previous filter only looked at
KeyRunes; bubbletea reports a single space as KeyMsg{Type: KeySpace,
Runes: []rune{' '}}, which slipped past the gate and inserted a literal space
into the buffer.
strings.TrimSpace is intentionally retained on submit as a defensive
belt-and-braces check even though whitespace can no longer enter the buffer
under normal input - cheap to keep, removes a line of subtle behavior coupling
to the input filter.
Adds regression tests for: spacebar filtering, multi-byte Unicode rune
filtering (close the corner case the old byte-length check missed), the
exact "Name cannot be empty" error string, error clearing on value change,
error preservation when a key is fully filtered out, ctrl+c cancel under
both showGitignore branches, esc cancel without gitignore, esc-back to
gitignore step (also clearing the error), textinput Width tracking the
modal content width on resize, and visual width parity between empty and
populated rendered fields.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The existing rune-filter path (filterValidPRDRunes over KeyRunes /
KeySpace) and the textinput's built-in paste/CharLimit handling already
satisfy every acceptance criterion — bracketed paste arrives as a single
KeyMsg{Type: KeyRunes, Paste: true}, which the filter scrubs before the
textinput splices the survivors at the caret and truncates to CharLimit.
Lock the behaviour in with regression tests covering:
- AC1: all-valid paste inserts at caret in one step
- AC2: mixed paste keeps only the valid subset, no error shown
- AC3: overlong paste truncates to maxPRDNameLength with prefix preserved
- AC4: mid-buffer paste splices (plus a filtering+splice combo)
- AC5: paste that changes value clears prdNameError (and the sister
case where an all-invalid paste leaves a standing error untouched)
- fallback: multi-rune KeyRunes without Paste=true is filtered too
The maxPRDNameLength constant from US-001 already governs the textinput's CharLimit (which bubbles uses for both keystroke and paste-time length enforcement), and no other call site hard-codes a length. This story adds regression tests so any future refactor that drifts from the constant or reintroduces a hard-coded limit fails loudly: - TestPRDName_CharLimitMatchesConstant pins ti.CharLimit == maxPRDNameLength - TestPRDName_TypingAtMaxLengthIsNoOp verifies typing at the max length is silent (no value change, no cursor advance, no error message) — consistent with how invalid-character filtering already behaves.
End-to-end regression suite driven through Update(...) for the PRD-name field: Left/Home/Ctrl+Left caret editing, paste filtering + truncation, max-length no-op, empty-submit error, and Init/gitignore→PRDName transition cmd wiring. Also adds a `-`/`_`-aware word-jump intercept in handlePRDNameKeys so Ctrl+Left on `foo-bar` lands after the `-` (bubbles' built-in wordBackward is whitespace-only and would jump to pos 0, since our filter strips whitespace from the buffer). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…me input Replace the hand-rolled AddInputChar/DeleteInputChar accumulator in PRDPicker with a bubbles/textinput.Model so the picker's new-PRD-name input gets caret editing, paste with charset filtering, and a max-length cap — matching the FirstTimeSetup StepPRDName behavior from the predecessor PRD. - Move the charset filter and word-jump helpers out of first_time_setup.go and into a new internal/tui/input_filters.go (filterPRDNameRunes, wordBackward, wordForward, prdNameSeparators) so both widgets share one set of code paths and can't drift on charset or separator rules. - PRDPicker.StartInputMode now focuses the textinput and returns textinput.Blink; app.go propagates that cmd at both call sites (app.go:608 and app.go:1907) so the caret renders (FR-10). CancelInputMode blurs and clears. - PRDPicker.UpdateInput implements the intercept-then-forward rune filter (KeyRunes + KeySpace) and overrides Ctrl+Left/Right + Alt-variants to treat \`-\` and \`_\` as word separators. app.go's input-mode dispatch now forwards the raw tea.KeyMsg after matching esc/enter, closing the multi-byte-rune corner case that the old \`len(msg.String()) == 1\` check silently dropped. - renderInputMode now reads p.ti.View(), deleting the hand-drawn cursor block and the placeholder special-casing (both moved onto the textinput itself). - first_time_setup_test.go's TestFilterValidPRDRunes body now references the new filterPRDNameRunes name in the same commit as the helper move. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-name edit The branch-name editor in the branch-warning modal now delegates to `bubbles/textinput`, giving the user caret editing, paste, and a 255- character cap (chosen so a long CLI-created PRD name can be seeded into `chief/<name>` without silent truncation). `AddInputChar`/`DeleteInputChar` are gone; `UpdateInput(tea.KeyMsg)` is the single entry point, with `filterBranchNameRunes` ([a-zA-Z0-9_/-]) and Ctrl+Left/Right word jumps that treat '-', '_', and '/' as separators so `chief/auth-system` jumps in path-aware hops. `StartEditMode()` now returns `tea.Cmd` and the `e` dispatch in app.go propagates it so the caret actually blinks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…th in both widgets Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ctrl+C was silently swallowed by the bubbles textinput in picker input mode and branch-warning edit mode, diverging from FirstTimeSetup's PRD-name step. Handle it in the same way (quit / open quit-confirm when a loop is running) and advertise it in the footer shortcuts. Also: extract isTextualKey() helper, cite the PRD path from the UpdateInput doc comments, and mark CursorEnd() on edit-mode re-entry as intentional. Adds regression tests for both the immediate-quit and quit-confirm branches; the latter also asserts the in-progress input/edit value survives canceling the confirmation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pastes into the three TUI text inputs (FirstTimeSetup PRD-name, PRDPicker new-PRD-name, BranchWarning branch-name) previously silently dropped invalid characters, mashing words together (e.g. "my feature/v2!" → "myfeaturev2"). Paste now goes through normalizePastedRunes: runs of invalid characters collapse to a single '-', leading/trailing invalid characters are stripped, and consecutive '-' dedupe within the pasted content (normalization is scoped to the paste — no cross-boundary dedupe against existing field text). "my feature/v2!" → "my-feature-v2" and "feat/oops bad!" → "feat/oops-bad" for the branch input. Paste detection (isPasteLike) covers bracketed paste (msg.Paste=true) and the non-bracketed fallback (multi-rune KeyRunes without Paste); single-rune typing keeps the original drop-based filter. Also fixes an unrelated pre-existing bug surfaced by running the ctrl+c tests: managerWithRunningPRD mutated a copy returned by loop.Manager.GetInstance (see the "Return a copy to avoid race conditions" comment), so IsAnyRunning always reported false and tryQuit fell through to tea.Quit. Added Manager.SetInstanceState for tests that need to force state without a Provider. Co-Authored-By: Claude Opus 4.7 <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.
Summary
FirstTimeSetupPRD-name,PRDPickernew-PRD-name,BranchWarningbranch-name) withbubbles/textinput,so Left/Right/Home/End/Ctrl+Left/Ctrl+Right and clipboard paste now behave like a normal terminal input. Charset filtering, max-length caps, blink/focus wiring, and width-sync
are preserved.
-(with leading/trailing invalid runes stripped) instead of being silentlydropped, so
"my feature/v2!"→"my-feature-v2"and"feat/oops bad!"→"feat/oops-bad". Detected via bracketed-paste flag or multi-runeKeyRunesfallback so terminalswithout bracketed-paste get the same UX. Single-keystroke typing keeps the drop-based filter.
tryQuitjustlike
FirstTimeSetup).Scope
Implemented as a user-story series (
US-001..US-009) followed by two targeted follow-ups:FirstTimeSetupPRD name: adoptbubbles/textinput, caret keys, paste-with-filter, max length, test matrixPRDPickernew-PRD-name: same adoptionBranchWarningbranch-name: same adoption-, ends stripped)New shared helpers in
internal/tui/input_filters.go:isAllowedPRDNameRune,isAllowedBranchNameRune,isTextualKey,isPasteLike,filterRunes,normalizePastedRunes,and the word-jump helpers (
wordBackward/wordForward).Manager.SetInstanceStateadded tointernal/loop/manager.goso tests can force a running loop without spinning up a Provider — unblocks the ctrl+c → quit-confirm regressiontests (the previous
inst := m.GetInstance(...); inst.State = ...pattern mutated a copy; see theGetInstancedocstring comment "Return a copy to avoid race conditions").New dependency:
github.com/charmbracelet/bubbles(first-party charmbracelet component library, same author asbubbleteawhich is already a direct dependency).Test plan
go test ./...passes locally.nnothing — type a PRD name using Left/Right/Home/End/Ctrl+Left/Ctrl+Right; caret moves as expected, invalid single keystrokes aresilently dropped.
"my feature/v2!"in the PRD-name field →"my-feature-v2".n, paste a name with invalid chars → interior runs collapse to-, trailing invalid stripped, cursor keys work.e, paste"feat/oops bad!"→"feat/oops-bad"(note/is preserved — it's in the branchcharset).
the in-progress input preserved.
maxPRDNameLength; paste >255 chars in branch-warning → truncates tomaxBranchNameLength.FirstTimeSetupflows (esc → back to gitignore step / quit, Enter → validate + advance) unchanged.Notes for reviewer
isValidPRDNameininternal/cmd/new.gois intentionally left alone — the length cap is TUI-only per US-005 Non-Goals.-xyzinto a field ending in-yields--xyz, bydesign — confirmed with product).
go.modbumps 1.24.0 → 1.24.2 and addsbubbles; transitive charm deps shift accordingly.🤖 Generated with Claude Code