Skip to content

API ergonomics pass 1: Line.FromText, string overloads, AllowBack, secret-default mask (v0.2.0-rc.1)#3

Merged
ThatRendle merged 10 commits into
mainfrom
change/api-ergonomics-pass-1
May 28, 2026
Merged

API ergonomics pass 1: Line.FromText, string overloads, AllowBack, secret-default mask (v0.2.0-rc.1)#3
ThatRendle merged 10 commits into
mainfrom
change/api-ergonomics-pass-1

Conversation

@ThatRendle
Copy link
Copy Markdown
Contributor

Summary

Closes the three §14.4 ergonomics gaps surfaced by the dmon-wizard port (memory: section14-api-ergonomics-findings):

  • Line.FromText(string, Style?) factory — replaces the new LineBuilder().Text(s).Build() ceremony for label-only strings. Explicitly no implicit string → Line conversion.
  • String-accepting consumer overloadsIScrollback.Append(string), plus InputRequest/SelectRequest/MultiSelectRequest/ChoiceRequest accept string/params string[]/IReadOnlyList<string> items.
  • AllowBack flag on SelectRequest/ChoiceRequest — opt-in (default false); Backspace at the empty initial position produces DialogOutcome.Back. MultiSelectRequest deliberately omits this for now (ambiguous semantics at a multi-toggle position).
  • Secret-default mask in InputDialog — regression-pinned. See "Notable findings".
  • Demo + DmonWizardLine.FromText / string overloads applied to label-only sites; a live AllowBack=true step wired in WizardRenderer (no engine change needed — WizardEngine.cs already handled WizardStepOutcome.Back).
  • Versions bumped 0.1.0-rc.1 → 0.2.0-rc.1 in Dcli.csproj + Dcli.Testing.csproj. NuGet artifacts dcli.0.2.0-rc.1.{nupkg,snupkg} + dcli.testing.0.2.0-rc.1.{nupkg,snupkg} produced cleanly.

No breaking changes. Every public addition is an overload or an opt-in flag; existing call sites continue to compile and behave identically.

OpenSpec change archived as openspec/changes/archive/2026-05-28-api-ergonomics-pass-1/. Delta specs synced into the main openspec/specs/{styled-text,fixed-region}/spec.md. DEVLOG frozen with the shipped stamp.

Notable findings (logged in DEVLOG)

  1. §4 — the secret-default leak doesn't actually exist in current code. InputDialog.Render has unconditionally masked _isSecret=true rows via the width-preserving MaskRows path since the file was introduced in core-rendering-architecture (394c9ba). The proposal's premise ("bullets only kick in once the user edits") and design.md Decision 5's assumption that _userEdited already existed for InputChanged emission were both inaccurate about the as-shipped code. The spec's behavioural scenarios still hold and are now pinned by regression tests; a sticky _userEdited flag was introduced as a single source of truth so the spec's edit-state vocabulary has a code-level seam.
  2. §2 — file layout in tasks.md was off. IScrollback lives in ITerminal.cs, all four request records in DialogRequests.cs (not per-record files). Implemented in the real files; not splitting them was a Decision-6 scope call.
  3. §5 — future ergonomics-pass-2 candidate: Line.Bold(s) / Line.Dim(s) / Line.Fg(s, color) single-style shorthands would shrink another ~10+ sites in WizardRenderer.cs + Dcli.Demo/Program.cs. Out of scope for pass-1; recorded in DEVLOG "Open follow-ups".

Test plan

  • `dotnet build -c Release` — 0 warnings, 0 errors
  • `dotnet test -c Release` — 714 tests green (688 baseline + 26 new across §§1-4: 5 `Line.FromText`, 9 facade/fake string overloads, 7 `AllowBack`, 5 secret-default)
  • `dotnet format --verify-no-changes` — clean
  • `openspec validate api-ergonomics-pass-1 --strict` — valid (pre-archive)
  • `dotnet pack -c Release` — produced all 4 expected nupkg/snupkg artifacts at `0.2.0-rc.1`
  • Manual: `grep -rn "implicit operator" src/` returns zero hits
  • Optional manual sanity: `dotnet run --project samples/Dcli.Demo.DmonWizard` — verify the new `AllowBack=true` step accepts Backspace and routes to the previous wizard step

Commits

10 commits on `change/api-ergonomics-pass-1` (one per OpenSpec section + DEVLOG bookkeeping + sync + archive):

```
50c4456 openspec: archive api-ergonomics-pass-1 → 2026-05-28-api-ergonomics-pass-1
2ddfa81 openspec(api-ergonomics-pass-1): sync delta specs + freeze DEVLOG before archive
f3ac49b chore(api-ergonomics-pass-1): backfill §6 commit hash in DEVLOG
a9c64e1 chore(api-ergonomics-pass-1): validation, version bump 0.2.0-rc.1, pack (section 6)
3bde5fd feat(api-ergonomics-pass-1): demo ports + live AllowBack step (section 5)
9cbd883 feat(api-ergonomics-pass-1): secret-default masking in InputDialog (section 4)
fb90d34 feat(api-ergonomics-pass-1): AllowBack flag on SelectRequest/ChoiceRequest (section 3)
55cd4de feat(api-ergonomics-pass-1): string-accepting consumer overloads (section 2)
3dd518c feat(api-ergonomics-pass-1): Line.FromText factory (section 1)
b5619a8 openspec: propose api-ergonomics-pass-1 + scaffold DEVLOG
```

🤖 Generated with Claude Code

ThatRendle and others added 10 commits May 28, 2026 13:50
Open the change that closes three of the five §14.4 ergonomics gaps surfaced by
the dmon-wizard port: Line.FromText + string-accepting overloads (gap 5), opt-in
AllowBack on Select/Choice dialogs (gap 1), and secret-default masking on the
input dialog (gap 3). Surface-only; no breaking changes; all additions or
opt-in flags. Targets dcli 0.2.0-rc.1.

Also folds in a small docs task (§6.7) to refine the repo CLAUDE.md's
"Where to look for historical context" subsection to match the canonical wording
recommended by the personal devlog skill (covers both in-flight + archived
DEVLOGs; the form shipped in PR #2 only covered the archived case).

Scaffolds DEVLOG.md inside the change directory per the personal devlog skill's
convention (`~/.claude/skills/devlog/SKILL.md`). First exercise of that skill
end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 1.1 Add `public static Line FromText(string text, Style? style = null)`
  to src/Dcli/Line.cs; returns a Line with a single Segment(text, style ?? default).
- 1.2 XML docs on FromText describe it as the canonical short form for
  label-only Lines, and explicitly state that no implicit string→Line
  conversion is defined (deliberate API choice; design.md Decision 1).
- 1.3 StyledTextTests gains 5 facts covering default style, explicit style,
  empty string, multi-rune round-trip, and structural-equality with the
  manual `new Line(new[]{ new Segment(text) })` form.

Gates: build 0 warnings, 693 tests green (688 baseline + 5),
dotnet format clean, openspec validate --strict clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tion 2)

- 2.1 Add `void Append(string text)` to `IScrollback` in src/Dcli/ITerminal.cs.
- 2.2 Implement `Append(string)` in ScrollbackSurface as a null-guarded
  forwarder to `Append(Line.FromText(text))`.
- 2.3 Add `InputRequest(string? prompt, string? Default = null, bool IsSecret = false)`
  secondary ctor chaining to the existing `Line?` primary.
- 2.4-2.6 Add `IReadOnlyList<string>` and `params string[]` ctors to
  SelectRequest, MultiSelectRequest, ChoiceRequest. All route items through
  `Line.FromText` with default style, preserving the existing `Title`/`Prompt`
  as `Line?`. Null-guards run before LINQ materialisation.
- 2.7 FacadeTests gains a §2 round-trip section: each new overload is asserted
  equal to the canonical `Line` form via `Line` sequence equality, with one
  non-ASCII case (`"héllo🦊"`) covering the forwarding path.
- 2.8 FakeTerminalTests' tier-A FakeScrollback implements the new
  `Append(string)` and asserts string/Line forms record identically.
- 2.9 No `implicit operator` exists anywhere in src/; the doc seam stays on
  `Line.FromText` (added in §1).

File layout deviation noted in DEVLOG: tasks.md named per-record files that
don't exist (IScrollback lives in ITerminal.cs; all four request records live
in DialogRequests.cs); implemented in the real files.

`new InputRequest(null)` is ambiguous (documented behaviour); the natural
`new InputRequest()` and the explicit `(string?)null` / `(Line?)null` forms
are unambiguous and covered by tests.

Gates: build 0 warnings, 702 tests green (+9 over §1), format clean,
openspec validate --strict clean, `grep -rn 'implicit operator' src/` empty.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…quest (section 3)

- 3.1 SelectRequest gains `bool AllowBack = false` as a third positional
  parameter (additive, backward-compatible). String-accepting secondary ctors
  from §2 forward it.
- 3.2 ChoiceRequest gains the same `AllowBack` parameter and secondary-ctor
  forwarding.
- 3.3 Dialog.HandleKey gains a new Back rule between Escape and arrow nav:
  Backspace fires `OverlayCloseKind.Back` when `_allowBack && !_hasMoved &&
  _filterText.Length == 0`. The `_filterText.Length == 0` guard defends
  against any future widening of `TypeToFilter` to Select/Choice.
- 3.4 The ↑/↓ arms now flip `_hasMoved = true` before MoveUp/MoveDown so
  Backspace post-movement is suppressed for the rest of the overlay session.
- 3.5 `OverlayCloseKind` gains a `Back` member. `Terminal.OpenModalAsync`
  replaces the Submit/Cancel ternary with a switch expression: Submit →
  buildResult, Back → DialogResult(Back, default), default → Cancelled
  (covers both `Cancel` and `null` defensively). `SelectAsync`/`ChoiceAsync`
  forward `req.AllowBack` into the Dialog ctor. `MultiSelectAsync` does NOT
  pass `allowBack` — MultiSelect cannot return Back.
- 3.6 DialogSelectionTests gains a §3 block: AllowBack=true + Backspace-at-empty
  produces Back; AllowBack=true after ↓ is a no-op (dialog stays open; Esc
  produces Cancelled); AllowBack=false (default) ignores Backspace.
- 3.7 New ChoiceDialogTests.cs with the equivalent three Choice tests.
- 3.8 Reflection test pins `MultiSelectRequest` deliberately omitting
  AllowBack (Decision 4).
- DialogOutcome.Back XML doc updated: no longer "Not produced by any v1
  dialog" — now produced under opt-in AllowBack=true.

Gates: build 0 warnings, 709 tests green (+7 over §2), format clean,
openspec validate --strict clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ection 4)

Spec/code-reality finding: the secret-default leak Decision 5 defends against
does not actually exist in current code. `InputDialog.Render` runs
`_isSecret ? MaskRows(rows) : rows` unconditionally on every paint since the
file was introduced (commit 394c9ba, §12 of core-rendering-architecture). The
proposal/design's premise that "bullets only kick in once the user edits" is
inaccurate; the seeded default has always been masked on first paint via the
existing width-preserving MaskRows path. Decision 5's "reuse the existing
edit-detection flag (for InputChanged emission)" guidance is also moot — no
such flag exists and no InputChanged event is emitted.

The spec scenarios remain correct as behavioural requirements; §4 work
therefore degenerates to:

- 4.1+4.2 Introduce a single sticky `_userEdited` flag on InputDialog
  (false initially, flipped to true on the three buffer-mutating operations
  in HandleKey: printable-rune insert, Backspace, Delete). Caret-only moves
  and Enter/Escape do NOT flip it. The constructor's `_buffer.SetText(default)`
  does NOT flip it. The flag is sticky: once true, never reset (even if the
  buffer is shrunk back to its original text). Exposed via
  `internal bool UserEdited => _userEdited` for test inspection.
- 4.3 No change to `Text` — it always returns the real buffer contents.
- 4.4 Five InputDialogTests pin the spec scenarios: masked-on-first-paint
  (a regression guard, not a bug repro), masked-before-first-edit,
  Text-returns-real-default, one-edit-then-revert-sticky semantics, and
  the existing IsSecret round-trip.
- 4.5 Non-secret default regression guard test.

Future trap recorded in DEVLOG: paste / history-recall (mentioned in
`specs/fixed-region/spec.md` "Owned input editor" as edit triggers) are not
implemented today, so they don't currently need to flip `_userEdited`. If
either gets added later, they must.

Gates: build 0 warnings, 714 tests green (+5 over §3), format clean,
openspec validate --strict clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n 5)

- 5.1 WizardRenderer.cs: 12 → 10 LineBuilder sites. Two label-only Select
  item builders in RenderChooseOneAsync/RenderChooseManyAsync replaced with
  Line.FromText. Remaining 10 are genuinely styled (Bold/Dim/Fg/multi-segment).
- 5.2 Dcli.Demo/Program.cs: 43 → 37 LineBuilder sites. Six label-only
  replacements — two via the new Scrollback.Append(string) overload, four
  via Line.FromText in a MultiSelect items array. Remaining sites are
  styled compositions.
- 5.3 Option A: AllowBack=true is now set on the ChooseOne Select dialog
  in WizardRenderer; DialogOutcome.Back maps to WizardStepOutcome.Back.
  WizardEngine.cs already routed WizardStepOutcome.Back to the previous
  step, so no engine changes were required — the affordance is live.

Future ergonomics-pass-2 candidates recorded in DEVLOG (Line.Bold(s) /
Line.Dim(s) / Line.Fg(s, color) shorthands would shrink another ~10+ sites
in the same files; out of scope for pass-1).

Gates: build 0 warnings, 714 tests green (unchanged — samples only),
format clean, openspec validate --strict clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ck (section 6)

- 6.1 dotnet build -c Release — 0 warnings, 0 errors.
- 6.2 dotnet test -c Release — 714 tests green (688 baseline + 26 new
  across §§1-4: 5 Line.FromText, 9 facade/fake string overloads,
  7 AllowBack, 5 secret-default).
- 6.3 dotnet format --verify-no-changes — clean.
- 6.4 openspec validate api-ergonomics-pass-1 --strict — valid.
- 6.5 Version bumped 0.1.0-rc.1 → 0.2.0-rc.1 in Dcli.csproj and
  Dcli.Testing.csproj (additive minor — every change in this proposal
  is an overload or opt-in flag, no breaking changes).
- 6.6 dotnet pack -c Release produced:
  - dcli.0.2.0-rc.1.nupkg + dcli.0.2.0-rc.1.snupkg
  - dcli.testing.0.2.0-rc.1.nupkg + dcli.testing.0.2.0-rc.1.snupkg
- 6.7 CLAUDE.md "Where to look for historical context" subsection
  updated to the canonical devlog-skill wording from
  ~/.claude/skills/devlog/SKILL.md ("also keep" / "inside the change
  directory" instead of "may also keep" / "at the change's root"),
  asserting the convention rather than presenting it as optional.
- 6.8 DEVLOG completed: row-per-section status table, deviations recorded
  (the §2 file-layout deviation, §4's bug-doesn't-exist finding, §5's
  next-pass ergonomics candidates), resume-point shipped stamp.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ore archive

- Fold the change's delta specs into the main openspec/specs/:
  * styled-text/spec.md gains the ADDED requirement "Convenience construction
    from plain strings" (Line.FromText factory + string overloads on
    IScrollback.Append and *Request records; no implicit string → Line).
  * fixed-region/spec.md's "Awaitable modal dialogs" requirement is replaced
    to add the AllowBack opt-in flag on Select/Choice (Backspace-at-empty);
    MultiSelect and Input explicitly do NOT expose AllowBack.
  * fixed-region/spec.md's "Owned input editor" requirement is replaced to
    add secret-default masking (mask the seeded Default until first edit when
    IsSecret=true; Submit returns the real string regardless).

- Freeze the DEVLOG to shipped framing per ~/.claude/skills/devlog/SKILL.md:
  drop the in-flight banner, replace "How to resume" with "Final state at
  archive", and turn the resume-point pointer into a shipped stamp citing
  final commit f3ac49b, archive date 2026-05-28, and the published NuGet
  artifacts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ass-1

All six sections shipped. Delta specs already synced into main specs in the
preceding commit. DEVLOG frozen with the shipped stamp.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ThatRendle ThatRendle merged commit 0f61fc9 into main May 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant