# Select Promptplus Control

> Use when building or modifying a .NET console application that uses, or could use, ConsolePlus and/or PromptPlus - to decide which library layer fits the need, whether an interactive control can even run in the target execution context, which of PromptPlus's 21 controls is the right fit, and how to implement it using that control's actual fluent API. Triggers on requests like "add a menu/prompt/progress bar/date picker to this console app", "which PromptPlus control should I use for X", "is this safe to run under CI or redirected input", "pick between ConsolePlus and PromptPlus", "how do I validate/mask/page this input", or any interactive console input/output design question in a .NET project. Only applies to console-type projects (an entry point with OutputType Exe, not WinExe) - not web apps, not WinForms/WPF apps, not libraries with no console entry point; a companion hook enforces this, but check the entry point yourself if the hook isn't installed. Supports PromptPlus 6.0+ only (its 5.x line is being di

- Skill: `fracerqueira/select-promptplus-control` (Agent Skill)
- Install (CLI): `npx skillmds@latest add fracerqueira/select-promptplus-control`
- Raw SKILL.md: https://api.skillmd.com/api/skills/fracerqueira/select-promptplus-control/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: FRACerqueira (https://skillmd.com/u/fracerqueira)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/fracerqueira/select-promptplus-control

---


# Choosing and implementing ConsolePlus / PromptPlus controls

This skill is a **router**, not a reference manual. PromptPlus alone has 21 controls with 4 doc
pages each - loading all of it up front would blow the context budget for no benefit on any single
request. Load only what Step 3 tells you to load, for only the control(s) you actually chose.

Do the steps in order. Do not skip Step 0 or Step 2 - they change what the later steps are allowed
to recommend.

## Step 0 - Resolve versions and pin the docs you'll fetch

`scripts/resolve_package_version.py` (in this plugin) tells you which published version is
policy-acceptable, and - separately - which GitHub tag's docs actually match what's installed.
These are **not the same question**: a project on PromptPlus 6.0.1 must get docs for 6.0.1, even if
the policy-recommended upgrade target is a different version.

**This skill supports PromptPlus 6.0 and later only.** PromptPlus's 5.x line is being discontinued;
always pass `--min-major-version 6` for PromptPlus (never for ConsolePlus.net, which has no such
floor). A project on PromptPlus below 6.0 is out of scope for `PromptPlus.Controls` guidance
entirely - see the hard-stop rule below, not a degraded fallback. There is no "proceed anyway with
older docs" path anymore.

Every doc fetch in this skill (Steps 1, 2, 4, 5) goes through `scripts/fetch_doc.py`, not a direct
WebFetch call - WebFetch runs page content through a small model with a prompt and returns *that
model's response*, which is fine for "summarize this page" but risks paraphrasing away exact method
signatures and table contents on a reference page. `fetch_doc.py` `curl`s the raw file, caches it
locally keyed by repo+ref+path (a tag's content never changes, so a cache hit is exactly as correct
as a fresh fetch), and prints the local path - Read that path directly for the verbatim content.
Its `--mutable-ref` flag exists for exactly one case: the transitional-period `main` target below,
which *does* move over time and is re-resolved to its current commit on a short TTL rather than
cached under the branch name forever.

```bash
python "${CLAUDE_PLUGIN_ROOT}/scripts/resolve_package_version.py" \
  --package-id PromptPlus --repo FRACerqueira/PromptPlus \
  --project-path <consumer.csproj or Directory.Packages.props> \
  --docs-probe-path docs/controls --min-major-version 6

python "${CLAUDE_PLUGIN_ROOT}/scripts/resolve_package_version.py" \
  --package-id ConsolePlus.net --repo FRACerqueira/ConsolePlus \
  --project-path <consumer.csproj or Directory.Packages.props> \
  --docs-probe-path docs/promptplus.md
```

**As of this writing, `6.0.0` (stable) is published and is the current `latest_acceptable_version`** -
the steady state (case 3 below) is the normal path now. Cases 1 and 2 below are still real and worth
knowing (a project can still be on an unsupported <6.0 install, or - hypothetically, if every 6.x
accepted release ever got unlisted from NuGet - back in the gap this plugin was built to handle
gracefully before a qualifying 6.x release shipped), but they are the exception now, not the default.

**Central Package Management gap:** if `--project-path` pointed at a `.csproj` and the script
returns `installed_version: null`, check the csproj yourself before concluding there's nothing
installed - under CPM the csproj only has `<PackageReference Include="PromptPlus" />` with no
`Version` attribute, and the real version lives in the nearest ancestor `Directory.Packages.props`
(a `<PackageVersion Include="PromptPlus" Version="..."/>` entry). Re-run with `--project-path`
pointed at that file instead. Don't let a null here silently fall back to `latest_acceptable_tag`
for a project that's actually on a different version - that's the exact wrong-docs failure this
two-tag design exists to prevent.

Read the JSON result (or its absence) in this order:

1. **The script exited non-zero with only an `"error"` key (no `docs_tag` at all)** - only possible
   if, hypothetically, no qualifying 6.x release exists on NuGet at all (nothing installed either) -
   not expected now that `6.0.0` is out (stable), but if it recurs (e.g. a future unlisting), default the
   doc target to `main` (it always carries the real, in-progress 6.0 docs - `docs/controls/` has
   existed on every commit since `v6.0.0-Beta1`) and say that's what you did - don't ask, this isn't
   genuinely ambiguous. Pass `--ref main --mutable-ref` to `fetch_doc.py` for every fetch here - that
   flag is what makes it re-resolve `main`'s current commit on a TTL instead of caching under the
   branch name forever (see the caching note above). TTL defaults to 60 minutes
   (`--main-refresh-minutes 60`) - deliberately short, since `main` is the one ref this plugin treats
   as moving. Pass a larger value only if you've decided that staleness risk doesn't matter for your
   use (e.g. `--main-refresh-minutes 1440` for 24h) - don't change the default silently.
2. **`status` is `"installed-below-minimum-supported"`** - stop for `PromptPlus.Controls` guidance.
   Tell the user plainly: "PromptPlus `<installed_version>` is below this skill's minimum supported
   version (6.0); control-selection guidance doesn't apply until you upgrade." Mention
   `latest_acceptable_version` (currently `6.0.0`, or whatever is newest and accepted by the time
   you read this) as the concrete upgrade target. Don't offer a degraded "proceed anyway with old
   docs" path. ConsolePlus work (Step 1's other layers) is unaffected - this floor is
   PromptPlus-specific.
3. **Otherwise (the normal case)**: use `docs_tag` as `--ref` for every `fetch_doc.py` call in Steps
   1, 2, 4, 5. `docs_structure` should always come back `"structured"` here (every 6.x tag has the
   full per-control doc set) - if it somehow doesn't, say so rather than silently degrading; don't
   fall back to a legacy-docs path, that no longer exists in this skill. If `latest_acceptable_version`
   is not `null` and `status` is `"outdated"` or `"installed-prerelease-not-accepted"` (e.g. installed
   on a `6.0.0-Beta*` build), mention the upgrade target - no backwards-recommendation risk here
   (`latest_acceptable_version` can no longer be a worse-documented release than what's installed).

If `python`/`python3` isn't available in this environment, say so, skip this step, and fall back to
WebFetch directly on the raw GitHub URL from `main` for Steps 1, 2, 4, 5 - degraded (no version pin,
and WebFetch's summarize-through-a-small-model behavior risks losing exact signatures/tables, see
the fidelity note above) but still better than guessing from memory alone. Say plainly that both
degradations apply when this happens.

## Step 1 - Which layer: ConsolePlus, the two `IWidgets`, or `PromptPlus.Controls`

Don't frame this as "pick a library" - `PromptPlus.Console` *is* the ConsolePlus driver, so using
PromptPlus never excludes ConsolePlus. Frame it as "which layer for this need":

| Need | Layer |
|---|---|
| Styled/colored output, logging, reports | ConsolePlus (`ConsolePlus.Console` / `PromptPlus.Console`, identical) |
| Banners, dashes, section headers, non-interactive widgets | Either `ConsolePlusLibrary.IWidgets` or `PromptPlusLibrary.IWidgets` |
| Cursor/screen control, alternate buffer, raw ANSI | ConsolePlus |
| Any keyboard-driven interactive prompt (menu, input, confirm, ...) | `PromptPlus.Controls` |

**Namespace trap:** `ConsolePlusLibrary.IWidgets` and `PromptPlusLibrary.IWidgets` are two different
types with the same name and different arity - e.g. `Dash` takes 2 params
(`text, style`) on ConsolePlus's `IWidgets` and 5 params (`value, style, dashOptions, extralines,
applycolorbackground`) on PromptPlus's. If the project already references PromptPlus, default to
`PromptPlus.Widgets` for the larger API surface, but say which one you picked and why if it matters
to the code being written.

For anything beyond this table, fetch ConsolePlus's positioning doc pinned to ConsolePlus's
`docs_tag`:
```bash
python "${CLAUDE_PLUGIN_ROOT}/scripts/fetch_doc.py" --repo FRACerqueira/ConsolePlus --ref <docs_tag> --path docs/promptplus.md
```
then Read the printed `path`.

## Step 2 - Can an interactive control even run here?

Check this **before** picking a control (Step 3) - it can rule out the whole
`PromptPlus.Controls` interactive surface for the current call site.

The rule (verified against PromptPlus's ADR0023 - don't cite the ADR filename itself, its
`V01R02` suffix will drift on the next revision; cite `global-behaviors.md` instead, which is
stable):

> Every interactive control's `Run()` throws `InvalidOperationException` immediately if
> `console.IsInputRedirected` is true - **not** `Profile.Interactive` (that's a CI-provider
> heuristic ConsolePlus sets on startup; it is not the signal this guard checks, and treating it as
> equivalent will produce wrong guidance). This replaces what used to be an indefinite hang with no
> diagnostic.
>
> **Exempt:** `ProgressBar`, `Task`, `MultiTasks`, `Timer` - these are "live" controls that complete
> on their own signal (progress reaching 100%, the wrapped task finishing, the countdown elapsing)
> and never actually wait on a keystroke, so they run fine under redirected input, CI, or a piped
> output. Also exempt: PromptPlus Demo Mode while a scripted key is queued.

Practical implication for this skill: if the target code runs in a context where
`Console.IsInputRedirected` may be true (CI, piped, service/daemon, scheduled task, `dotnet test`
runner, headless container) and the need is progress/status/wait-for-completion rather than a real
choice from the user, steer to the live-control cluster (Step 3, Cluster C) instead of an
interactive one - don't let the user pick `Select`/`Input`/etc. for a code path that will throw in
that context.

For deeper detail (exact exemption logic, Demo Mode interaction), only if `docs_structure` is
`"structured"`:
```bash
python "${CLAUDE_PLUGIN_ROOT}/scripts/fetch_doc.py" --repo FRACerqueira/PromptPlus --ref <docs_tag> --path docs/global-behaviors.md
```
then Read the printed `path`.

## Step 3 - Which control

21 controls cluster into 3 confusable families plus 8 singles. Getting the cluster right is most of
the decision; within a cluster, the dimensions below almost always settle it. When it's genuinely
ambiguous, **propose your pick and the reason to the user before implementing** - don't silently
guess between, say, `Select` and `TableSelect`.

### Cluster A - pick one-or-more from a list

| Control | Cardinality | Shape |
|---|---|---|
| `Select<T>` | one | flat list |
| `MultiSelect<T>` | several (checkboxes) | flat list |
| `TableSelect<T>` | one | tabular (named columns) |
| `TableMultiSelect<T>` | several | tabular |
| `TreeSelect<T>` | one | hierarchical (expand/collapse) |
| `TreeMultiSelect<T>` | several (tri-state) | hierarchical |

Ask: (1) one item or several? (2) is the data flat, does it have multiple meaningful columns, or is
it naturally nested? That's a 2x3 grid that picks the control outright.

### Cluster B - text entry

| Control | Shape |
|---|---|
| `Input` | free-form plain text |
| `Secret` | free-form, masked while typing (passwords) |
| `MaskEdit` family (12 factories, 4 fluent interfaces) | pattern-constrained: the value must match a template |

The `MaskEdit` family: all 12 factories live on `PromptPlus.Controls`, each `(prompt, description)`:

| Interface | Factories | Mask shape |
|---|---|---|
| `IMaskEditStringControl<string>` | `MaskEdit` | free-form token mask you write via `Mask(...)` |
| `IMaskEditNumberControl<T>` | `MaskInteger`, `MaskLong` | whole-number mask via `NumberFormat(...)` |
| `IMaskEditCurrencyControl<T>` | `MaskDecimal`, `MaskDecimalCurrency`, `MaskDouble`, `MaskDoubleCurrency` | fixed-decimal mask via `NumberFormat(...)` |
| `IMaskEditDateTimeControl<T>` | `MaskDateTime`, `MaskDate`, `MaskDateOnly`, `MaskTime`, `MaskTimeOnly` | culture-ordered date/time mask, no mask string |

Ask: is the value free text (→ `Input`/`Secret`, and is it a credential → `Secret`) or must it match
a fixed shape (phone, price, date, ...) → `MaskEdit` family, then pick the factory by .NET return
type (`decimal` → `MaskDecimal`, `DateOnly` → `MaskDateOnly`, etc).

### Cluster C - live / non-blocking-on-keypress (exempt from Step 2's guard)

| Control | Use when |
|---|---|
| `ProgressBar` | you can report a determinate 0-100% value from your own work |
| `Task` | one operation (sync or async), indeterminate - show it's working |
| `MultiTasks` | several operations, sequential or parallel, each with its own status line |
| `Timer` | suspend for a fixed duration while showing a live countdown |

Ask: determinate progress you drive → `ProgressBar`. One black-box operation → `Task`. Several
independent operations → `MultiTasks`. Pure countdown, no operation attached → `Timer`.

### Singles

| Control | One-liner |
|---|---|
| `Calendar` | interactive monthly grid; user navigates day-by-day, confirms one date |
| `ChartBar` | interactive horizontal bar chart; user navigates/re-sorts/re-layouts, picks one |
| `Confirm` | yes/no; culture-specific Yes/No key, returns immediately |
| `KeyPress` | wait for one keystroke, returns immediately, no Enter needed |
| `File` | lazy-loaded file-system tree; user picks one file or folder |
| `MultiFile` | lazy-loaded file-system tree; user checks several files/folders |
| `Slider` | pick a numeric value by moving a bar between min and max |
| `Switch` | toggle a single boolean on/off with the arrow keys |

`Confirm` vs `KeyPress`: `Confirm` is specifically yes/no with culture-aware keys; `KeyPress` is any
single key you define. A yes/no question is `Confirm` even though `KeyPress` could technically do it.

## Step 4 - Load the implementation details, only for the control(s) you chose

You should only reach this step with `docs_structure: "structured"` - Step 0's rules stop for
`PromptPlus.Controls` guidance before this point whenever the installed version is below 6.0. For
each file, pinned to `docs_tag`:
```bash
python "${CLAUDE_PLUGIN_ROOT}/scripts/fetch_doc.py" --repo FRACerqueira/PromptPlus --ref <docs_tag> --path docs/controls/<control>/index.md
python "${CLAUDE_PLUGIN_ROOT}/scripts/fetch_doc.py" --repo FRACerqueira/PromptPlus --ref <docs_tag> --path docs/controls/<control>/methods.md
python "${CLAUDE_PLUGIN_ROOT}/scripts/fetch_doc.py" --repo FRACerqueira/PromptPlus --ref <docs_tag> --path docs/controls/<control>/operations.md
```
then Read each printed `path`. `<control>` is the lowercase directory name (`select`, `maskedit`,
`tablemultiselect`, ...), not the C# type name. Fetch `styles.md` too only if the user asks about
theming/colors for that control. **Do not fetch docs for controls you didn't choose** - that defeats
the point of routing.

## Step 5 - Best practices while wiring it up

Apply regardless of which control was chosen:

- Every control exposes `.Options(o => ...)` for per-instance overrides (prompt text, description,
  `EnabledAbortKey`, `ShowTooltip`, `HideAfterFinish`, `HideOnAbort`, ...) rather than only global
  `PromptPlus.Config` - prefer `.Options()` when the behavior is specific to one call site, reserve
  `PromptPlus.Config` for app-wide defaults.
- Respect `EnabledAbortKey`/Esc - don't disable it without the user asking; it's the documented,
  expected abort path across every control.
- `HideAfterFinish`/`HideOnAbort` control whether the control's UI stays on screen after
  confirm/abort - relevant for wizards where only the final answers should remain visible.
- Culture (`DefaultCulture`) is applied only during `.Run()` and restored after, even on error -
  don't wrap controls in manual culture save/restore, it's redundant.
- Unhandled exceptions inside a control write to
  `%LocalAppData%/PromptPlus/PromptPlus.error.log` without throwing further - don't add your own
  duplicate logging around `.Run()` for that case unless the user wants app-specific logging too.

For the full property/behavior reference, only if `docs_structure` is `"structured"`:
```bash
python "${CLAUDE_PLUGIN_ROOT}/scripts/fetch_doc.py" --repo FRACerqueira/PromptPlus --ref <docs_tag> --path docs/global-behaviors.md
```
then Read the printed `path` (same cached file Step 2 may have already fetched - no duplicate cost).

## Scope check

This skill applies to console-type .NET projects only (an entry point with
`<OutputType>Exe</OutputType>`, not `WinExe` - that's the conventional `OutputType` for WinForms/WPF
apps and is deliberately treated as out of scope, see `check_console_project.py`'s
`is_console_entry` - and not `Microsoft.NET.Sdk.Web`). `hooks/hooks.json` +
`scripts/check_console_project.py` enforce this deterministically before the skill can even be
invoked. If that hook isn't installed for some reason, check the entry point yourself as a fallback -
walk up from the current project to find the one actually run (`dotnet run`), not necessarily the
csproj currently open, since a class library with no `OutputType` hosting PromptPlus calls but
consumed by a console `Exe` is a legitimate case. If no console entry point is reachable, say this
skill doesn't apply and stop.

