Repository navigation
Carry the docs in dotnet cui, and declare its command line with System.CommandLine - #19
Merged
Merged
Conversation
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.
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.
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 changes
Two changes to
dotnet cui, both about being readable by a person or an agent:docs,docs read,docs searchanswer fromthe guide embedded in the package, offline, before any project is resolved.
grammar, so the help cannot disagree with the parser.
The bug that prompted the rewrite
The hand-written help had drifted from the hand-written parser:
-a, --attach [id]-atook no value → the id became a positional word-t, --target <id>worked but was documented nowhereSo
dotnet cui click "中文" -a 15036searched for a widget whose label was中文 15036andcorrectly reported that none existed. The label was fine; the grammar was not.
What the declared grammar gives a reader
clik Save→Did you mean `dotnet cui click`?as a whole number), not a CLR type name--jsonexists only on the four commands that answer JSON, so it cannot be typed where it wouldbe ignored
--versionanswers with a version instead of falling through to the help screendotnet cuiprints the help there rather than into a pipeline--helpworks per verb, anddotnet cui click --helpdoes not demand the label it is explainingTwo rules survive because the wire protocol needs them: everything after a standalone
--goes tothe 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.Resolvere-serialisesone command's typed arguments into exactly one protocol line (
click Save,wheel 120 400 -8), sothe parser changed and the protocol did not.
Cli.Optionskeeps its shape andCli.Runkeepsdispatching, so the headless client, the live client and the offline answers are untouched.
A topic is a named list of
doc#sectionpointers resolved at load, not a second copy of theprose — 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
-warnaserroranddotnet format --verify-no-changes— cleanregen.ps1 -NoNative -NoDiff— bindgen no diff, self-test 14/14,export-api: no diffverify-template.ps1including AOT — file-based app still 1,163,264 B; System.CommandLine is adependency of the tool only, so no app binary moved
--tool-path, empty directory, no repo, no network:--help,--version,docs,docs read,docs search,api --json,icons,psall answertree -a 15036,click "中文" -a 15036,click "中文" -aand--attach=15036all drive it, and the click visibly switches the app's language badgeHonest limits
dotnet-cui [command], which is the installed shim's own name;dotnet cuiis the documented invocation. S.CL 3.0 exposes no hook to rename the root.--project --jsonsets the project to--json)because
--wait-text -loadingrequires that rule.-a 15036and a bare-aare both accepted; the bare form needs exactly one live window.dotnet cui docsis the portableform (
docs/PLAN.md).skills/cui-dotnet/api/*.md(59 pages, ~200 KB) are embedded, taking the tool package to 516 KB.