Skip to content

Carry the docs in dotnet cui, and declare its command line with System.CommandLine - #19

Merged
albertwoo merged 4 commits into
mainfrom
feat/cui-docs
Nov 12, 2026
Merged

albertwoo merged 4 commits into
mainfrom
feat/cui-docs

Conversation

@albertwoo

@albertwoo albertwoo commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

What changes

Two changes to dotnet cui, both about being readable by a person or an agent:

  1. The tool carries the documentation (D11) — docs, docs read, docs search answer from
    the guide embedded in the package, offline, before any project is resolved.
  2. The command line is declared, not parsed by hand (D12) — System.CommandLine owns the
    grammar, so the help cannot disagree with the parser.
dotnet tool install -g CuiSharp.Tools
dotnet cui docs                          # the 14 task topics and the bundled documents
dotnet cui docs read debugging           # a topic, whole
dotnet cui docs search "decimate chart"  # ranked sections, each with the read that opens it
dotnet cui api ChartOpts --json          # exact declarations
dotnet cui tree -a 15036                 # drive a live window by pid

The bug that prompted the rewrite

The hand-written help had drifted from the hand-written parser:

Help said Parser did
-a, --attach [id] -a took no value → the id became a positional word
— -t, --target <id> worked but was documented nowhere

So dotnet cui click "中文" -a 15036 searched for a widget whose label was 中文 15036 and
correctly reported that none existed. The label was fine; the grammar was not.

What the declared grammar gives a reader

  • a mistyped verb names the verb meant: clik Save → Did you mean `dotnet cui click`?
  • a bad value is a sentence about the value (as a whole number), not a CLR type name
  • --json exists only on the four commands that answer JSON, so it cannot be typed where it would
    be ignored
  • --version answers with a version instead of falling through to the help screen
  • usage errors exit 2 on stderr; a bare dotnet cui prints the help there rather than into a pipeline
  • a repeated option is refused instead of being decided by whichever occurrence came last
  • --help works per verb, and dotnet cui click --help does not demand the label it is explaining

Two rules survive because the wire protocol needs them: everything after a standalone -- goes to
the app untouched, and a value is a value even when it begins with a dash (--wait-text -loading).

Design

The verbs are the app's wire protocol, not the tool's invention. Grammar.Resolve re-serialises
one command's typed arguments into exactly one protocol line (click Save, wheel 120 400 -8), so
the parser changed and the protocol did not. Cli.Options keeps its shape and Cli.Run keeps
dispatching, so the headless client, the live client and the offline answers are untouched.

A topic is a named list of doc#section pointers resolved at load, not a second copy of the
prose — every sentence keeps one home, and a renamed heading fails a test rather than leaving a
topic that answers confidently and misleads. The pages are embedded, and a test compares the bytes
with the checked-in files.

Verification

  • Release build -warnaserror and dotnet format --verify-no-changes — clean
  • 947 + 28 tests passed, 0 failed (95 of them the CLI grammar)
  • regen.ps1 -NoNative -NoDiff — bindgen no diff, self-test 14/14, export-api: no diff
  • verify-template.ps1 including AOT — file-based app still 1,163,264 B; System.CommandLine is a
    dependency of the tool only, so no app binary moved
  • Installed tool, isolated --tool-path, empty directory, no repo, no network: --help, --version,
    docs, docs read, docs search, api --json, icons, ps all answer
  • Against a real live window: tree -a 15036, click "中文" -a 15036, click "中文" -a and
    --attach=15036 all drive it, and the click visibly switches the app's language badge

Honest limits

  • The generated usage line reads dotnet-cui [command], which is the installed shim's own name;
    dotnet cui is the documented invocation. S.CL 3.0 exposes no hook to rename the root.
  • A value that looks like a flag is still a value (--project --json sets the project to --json)
    because --wait-text -loading requires that rule.
  • -a 15036 and a bare -a are both accepted; the bare form needs exactly one live window.
  • Per-harness skill registration is deliberately not generated; dotnet cui docs is the portable
    form (docs/PLAN.md).
  • skills/cui-dotnet/api/*.md (59 pages, ~200 KB) are embedded, taking the tool package to 516 KB.

An assistant had to be pointed at the repository's skills folder to learn how to
write a CuiSharp UI, which meant every project either copied the framework's rules
into its own instruction file or left them out. The tool now carries those pages.

dotnet cui docs lists the fourteen task topics and the bundled documents; docs
read takes a topic, a page or a page#section; docs search ranks sections and
prints the read that opens each one. All three answer from resources inside
CuiSharp.Tools and return before any project is resolved, so they work offline in
an empty directory with no app, no build and no network.

A topic is a named list of doc#section pointers resolved at load, not a second
copy of the prose: every sentence keeps one home. A test walks every topic and
reads it, so a heading renamed in the guide fails the build instead of leaving a
topic that answers and misleads. Pages are embedded rather than copied, and
another test compares the bytes with the checked-in files.

Search weighs words by how rare they are in the bundle, so "decimate chart" finds
the decimation rule rather than every page that says "chart", and a section whose
own heading names the question ranks above a page that merely lists it.

README and the app template's AGENTS.md now carry the install line and the one
paragraph a project needs, instead of restating the guide.
…t by hand

Asking for help meant reading a 62-line string literal, and that literal had already
drifted from the parser: it promised `-a, --attach [id]` while `-a` took no value, so
the id stayed a positional word and `dotnet cui click "中文" -a 15036` reported that no
widget called "中文 15036" existed. `-t, --target` -- the one short flag that did take a
value -- was not documented at all.

The grammar is now declared once, in `Grammar.cs`: every verb is a Command whose
arguments carry their own type and arity, every option is an Option, and the help is
generated from that same declaration. `dotnet cui --help` and `dotnet cui click --help`
describe what the parser actually does, because they are the parser.

The verbs are still the app's wire protocol. Resolve re-serialises one command's
arguments into exactly one protocol line, so "click Save" and "wheel 120 400 -8" travel
unchanged; a label may now be quoted as one token, and the arity refuses an unquoted
second word rather than silently joining it.

What a reader gets that the hand parser could not give:
  - a mistyped verb names the verb that was meant (`clik` -> `click`);
  - a bad value is a sentence about the value (`as a whole number`), not a CLR type;
  - `--json` exists only on the four commands that answer JSON, so it cannot be typed
    where it would be ignored;
  - `--version` answers with a version instead of falling through to the help;
  - usage errors are exit 2 on stderr, and a bare `dotnet cui` prints the help there
    rather than into a pipeline;
  - a repeated option is refused instead of decided by whichever came last.

Two rules survive because the protocol needs them: everything after a standalone `--`
goes to the app untouched, and a value is a value even when it starts with a dash
(`--wait-text -loading`). The `docs <topic>` shorthand also survives, hidden from the
usage lines so it cannot look like an argument of `docs search`.

`Cli.Options` keeps its shape and `Cli.Run` keeps dispatching, so the headless client,
the live client and the offline answers are untouched; only the reading of a command
line changed. Tests now go through the same `Grammar.Parse` the tool dispatches on.

ToolArgumentTests and ApiCatalogCliTests were rewritten for the new grammar rather than
patched, including a walk of every command and every option spelling in the help, which
is what would have caught the original `-a` drift.
@albertwoo albertwoo changed the title Carry the docs in dotnet cui: one install, one paragraph, offline Carry the docs in dotnet cui, and declare its command line with System.CommandLine Nov 12, 2026
A review of the declared grammar found six things the green CI could not see, because
it tested the grammar's acceptance rather than its honesty.

Options were inherited from the root, so every command accepted every option. `docs
--attach`, `api ChartOpts --stdin` and `ps --wait ready` all parsed and were then
ignored: the help advertised them, the parser took them, and nothing used them. They
now live on the commands that can honour them, which also makes the offline promise
structural — `docs`, `api` and `icons` declare no way to name a project, so no
spelling of the line can make them resolve one. The price is that options follow the
verb (`dotnet cui tree --frames 2`), and a reader who puts them first is told exactly
that instead of being told the option does not exist.

Contradictions are refused rather than resolved: --project, --app and --attach are
three answers to one question, --no-build needs something to build, --framework picks
among a project's frameworks, and a NaN coordinate is not a place to point at.

`--stdin` became `dotnet cui session`. As a flag it could be written beside a verb, and
the verb was then silently dropped; as a command the line says which of the two things
is happening.

`dwell` and `waittext` require their duration. Bare `dwell` started an app and then
sent it a command it does not have; the protocol always needed the seconds.

A failed launch printed an unhandled Win32Exception out of Process.Start. It now exits
1 with one sentence.

`tree`'s description promised a `print` alias that did not exist; it exists now, and the
help for `key` names keys the parser accepts rather than `Ctrl+S`, which it never did.

`--json=false` answers with text: a bool option's result is trusted for its value, not
for the fact that it is present.

Finally the docs are a test. `EveryCommandLineTheDocsShowIsOneTheToolAccepts` walks the
`dotnet cui` lines in README, SKILL.md, llms.txt and the template, and would have caught
what the review found by hand: `click Button "Save type"` (two arguments where a query
is one) and `click editor > Save` (a path separator the shell reads as redirection),
both printed to readers for weeks and neither parseable.

The docs grammar is dispatched on instead of re-parsed: `docs.read` and `docs.search`
are commands, so the tool no longer joins a string back together to decide what it was
asked. The `docs <topic>` shorthand is gone with it — an argument on a command that has
subcommands is where that string round-trip came from — and the dropped verb is answered
with `dotnet cui docs read testing` instead.
Only CHANGELOG.md conflicted, and it conflicted by both sides adding a section above
0.3.2: this branch added `## [0.4.0] — unreleased` for the docs command and the declared
grammar, main added `## [Unreleased]` for the template icon and the win32 icon fix.

Resolved to one unreleased section under main's heading — it is the form the repository
already used before, and `release-notes.ps1 -Version Unreleased` is what prints it — with
both sets of entries kept and grouped Added / Changed / Fixed. The version number is left
to whoever cuts the release, so nothing here claims 0.4.0 will be the tag.

README.md, skills/cui-dotnet/SKILL.md and tools/verify-template.ps1 merged textually; each
was checked rather than trusted. The README and guide edits are in different sections
(template generation at the top, `dotnet cui` in the middle), and every `dotnet cui`
invocation main added already writes the verb before its options, so none of them meet the
new grammar's rule that options follow the command.

Verified on the merged tree: Release build -warnaserror, 952 + 28 tests, dotnet format
--verify-no-changes, regen -NoNative -NoDiff (bindgen no diff, self-test 14/14, export-api
no diff), and verify-template end to end — including main's new icon checks, with the
one-file publish now 1,199,104 B as docs/SIZE.md records.
@albertwoo
albertwoo merged commit 01f8a7a into main Nov 12, 2026
22 checks passed
@albertwoo
albertwoo deleted the feat/cui-docs branch November 12, 2026 02:29
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.

2 participants