Status: draft v0.5 · companion to the PR/FAQ in issue #5155
This is the implementation spec. It assumes the PR/FAQ is broadly accepted and covers what to build, where it lives, what changes in Terminal.Gui to support it, how it ships, and how it's tested.
- New repo
tui-cs/cletcontaining all clet code: abstractions, registry, JSON, built-in clets, CLI binary, release automation. - Targeted changes to
tui-cs/Terminal.Guicore (§3) that benefit TG generally and unblock clet specifically. - Fourteen input clets and one browser clet (
md) statically registered in v1.0. - Native installer channels: Homebrew (tui-cs tap), WinGet, .NET tool. NativeAOT for native channels.
- Independent SemVer; major version tied to
schemaVersionchanges per §4.3.1 (see D-022). - JSON output contract (schemaVersion 1).
- Inline input rendering; alt-screen viewer rendering.
- Theming via TG's
ConfigurationManager.
- Extracting clet abstractions into a published NuGet for third-party consumption (
Clet.Abstractions). Today,ICletis internal-to-the-binary; v2 may publish it. - Third-party clet runtime loading (
Assembly.LoadFrominto the AOT'd CLI). passwordclet.- Additional viewer clets (
json,log,diff). - Telemetry beyond a one-shot opt-in install ping.
- Embedded/inline-in-other-TG-app use of clets.
Two repos. One assembly that matters (the CLI exe). One release cadence.
tui-cs/Terminal.Gui tui-cs/clet
├── Terminal.Gui/ ├── src/
│ (core; §3 tweaks land here, │ └── Clet/
│ no clet-specific types) │ Abstractions/ (IClet, ICletRegistry, ...)
├── Tests/ │ Registry/
│ (TG core tests only; │ Json/ (CletJsonContext, SchemaV1)
│ clet tests live in tui-cs/clet) │ Clets/Input/ (14 input clets)
└── .github/workflows/ │ Clets/Viewer/ (MarkdownClet)
notify-clet-on-release.yml (NEW) │ Hosting/ (Program.cs, CLI parser)
│ Help/ (overview.md)
├── tests/
│ Clet.UnitTests/
│ Clet.IntegrationTests/
│ Clet.SmokeTests/
│ Clet.UITests/
│ SPEC.md
├── docs/
│ threat-model.md
│ runbooks/release-rollback.md
├── specs/
│ clet-spec.md (this file)
│ decisions.md
│ press-release.md
└── .github/workflows/
ci.yml
release.yml
Process model. The clet binary is a thin shell:
- Parses CLI args (hand-rolled parser — see D-006).
- Looks up the alias in its in-process
ICletRegistry. - Initializes a Terminal.Gui
IApplication. - Calls
clet.RunBoxedAsync(...). - Serializes the result, emits to stdout, exits with the right code.
All of (3)+(4) is plain Terminal.Gui hosting against TG's public API. The clet itself is a Terminal.Gui View. Nothing in TG core knows about clets; nothing in clets requires private TG API.
TG core (rendering, drivers, Views, keybindings) → tui-cs/Terminal.Gui maintainers.
CLI host, registry, JSON envelope, packaging, clets → tui-cs/clet maintainers.
Cross-repo bugs file in tui-cs/clet first; the clet maintainer reproduces, isolates, and escalates upstream if the root cause is in TG core.
All prerequisite TG changes have landed on develop:
- Inline rendering — shipping and exercised by
md, the inline examples, andtui-cs/ai. - AOT compatibility — tracked in TG core; remaining issues surface by building/running
clet. ConfigurationManagerpath-based loading — broadly used and tested.MarkdownView — vetted for the read-only, dismissable, themed shape clet needs.- Terminal-driver inline-capable detection — in place.
Application.RunAsync(Toplevel, CancellationToken)(#5157) — landed. Clet binds directly.FileDialogtyped result (#5158) — landed asDialog<IReadOnlyList<string>?>.
clet builds against the TG version named by <TerminalGuiVersion> in src/Clet/Clet.csproj; the release workflow overrides it from the dispatch payload. See D-020.
On cancel, clet emits {"schemaVersion":1,"status":"cancelled"} and nothing else — no value, no code, no partial result — regardless of whether IValue<T>.Value is readable on the underlying View. This is clet's wire contract; TG's disposition of IValue<T>.Value on cancellation is a TG-internal concern.
FileDialog inherits from Dialog<IReadOnlyList<string>?>. The pick-file and pick-directory clets bind to this directly. The §4.3.2 per-clet value shape table specifies the resulting JSON wire format (string for single-select, array of strings for --multi).
This repo holds everything: abstractions, registry, JSON, built-in clets, the CLI binary, and release automation. One assembly is published; everything else is build-time only or test-only.
tui-cs/clet/
├── Clet.slnx
├── src/
│ └── Clet/ (single Exe; PublishAot=true; net10.0)
│ ├── Abstractions/
│ │ IClet.cs (IClet + IClet<T> with RunBoxedAsync DIM)
│ │ IViewerClet.cs (with RunBoxedAsync DIM)
│ │ ICletRegistry.cs
│ │ BoxedCletResult.cs
│ │ CletKind.cs (Input | Viewer)
│ │ CletRunOptions.cs
│ │ CletRunResult.cs (non-generic + generic)
│ │ CletRunStatus.cs
│ │ CletOptionDescriptor.cs
│ ├── Registry/
│ │ CletRegistry.cs
│ │ BuiltInClets.cs (hand-written)
│ ├── Json/
│ │ CletJsonContext.cs ([JsonSerializable] source-gen)
│ │ SchemaV1.cs
│ ├── Clets/
│ │ ├── Input/
│ │ │ SelectClet.cs, TextClet.cs, IntClet.cs, DecimalClet.cs,
│ │ │ ConfirmClet.cs, MultiSelectClet.cs, PickFileClet.cs,
│ │ │ PickDirectoryClet.cs, DateClet.cs, TimeClet.cs,
│ │ │ DurationClet.cs, ColorClet.cs, AttributePickerClet.cs,
│ │ │ RangeClet.cs
│ │ │ (+ helpers: LabelParser.cs, FileFilterParser.cs, RangeView.cs)
│ │ └── Viewer/
│ │ MarkdownClet.cs
│ ├── Help/
│ │ overview.md (embedded resource for --help)
│ ├── Hosting/
│ │ Program.cs
│ │ CommandLineRoot.cs (hand-rolled CLI parser; D-006)
│ │ AliasDispatcher.cs
│ │ OutputFormatter.cs
│ │ ExitCodes.cs
│ │ MarkdownHelpRenderer.cs
│ │ CletStyling.cs
│ └── Properties/
│ AssemblyInfo.cs (InternalsVisibleTo)
├── tests/
│ ├── SPEC.md (testing spec)
│ ├── Clet.UnitTests/
│ ├── Clet.IntegrationTests/
│ ├── Clet.SmokeTests/
│ └── Clet.UITests/
└── docs/
├── threat-model.md
└── runbooks/release-rollback.md
One src project (Clet). Abstractions, registry, JSON, built-in clets, and Program.Main all compile into one assembly.
All types are internal to the Clet assembly in v1.0. v2 may extract them to Clet.Abstractions. See source files in src/Clet/Abstractions/ for canonical definitions. Key shapes:
IClet— declaresPrimaryAlias,Aliases,Description,Kind,ResultType,Options, andRunBoxedAsync(IApplication, string?, CletRunOptions, CancellationToken).IClet<TValue> : IClet— adds typedRunAsync(...)returningCletRunResult<TValue>. ProvidesRunBoxedAsyncas a default interface method.IViewerClet : IClet— typedRunAsync(...)returningCletRunResult(no value). Also providesRunBoxedAsyncvia DIM.BoxedCletResult—(CletRunStatus, object?, string?, string?)record struct for non-generic dispatch.CletRunOptions—Title,JsonOutput,Timeout,Fullscreen,CletOptions,Arguments.CletRunResult/CletRunResult<T>—Status,Value(generic only),ErrorCode,ErrorMessage.ICletRegistry—Register(IClet),TryResolve(string, out IClet?),All.
{
"$id": "https://tui-cs.github.io/clet/schema/v1.json",
"type": "object",
"required": ["schemaVersion", "status"],
"properties": {
"schemaVersion": { "const": 1 },
"status": { "enum": ["ok", "cancelled", "error", "no-result"] },
"value": { },
"code": { "type": "string" },
"message": { "type": "string" }
},
"allOf": [
{ "if": { "properties": { "status": { "const": "ok" } } },
"then": { "anyOf": [ { "required": ["value"] }, { "not": { "required": ["value"] } } ] } },
{ "if": { "properties": { "status": { "const": "error" } } },
"then": { "required": ["code", "message"] } }
]
}Normative envelopes:
{ "schemaVersion": 1, "status": "ok", "value": <T> } // input clet success
{ "schemaVersion": 1, "status": "ok" } // viewer clet dismiss
{ "schemaVersion": 1, "status": "cancelled" } // cancel (any clet)
{ "schemaVersion": 1, "status": "error", "code": "...", "message": "..." }
{ "schemaVersion": 1, "status": "no-result" }No type field on the envelope. Result types are advertised once per alias by clet list --json. Consumers cache the registry; they do not branch on a per-call CLR type name. See D-001.
Cancel is decoupled from TG. See §3.1.
Schema is pinned in src/Clet/Json/SchemaV1.cs. JSON contract tests (tests/SPEC.md §2.5) validate emitted output against this schema.
schemaVersion: 1 is the contract for the entire clet 1.x line. Changes within 1.x are additive only — existing fields never change meaning, never become required, never change type. A schemaVersion: 2 is permitted only at a clet 2.0.0 boundary, and clet 2.x must accept --schema-version 1 to emit the v1 envelope for at least one minor release, giving consumers a parallel-period to migrate.
For schema-lock at v0.5, the shape of value is fixed per alias.
| Alias | value shape |
|---|---|
text |
string (newlines preserved as \n) |
int |
integer |
decimal |
number (JSON number; consumer decides float vs decimal) |
confirm |
boolean |
select |
string (label text of the selected item — see D-008) |
multi-select |
array of strings (label texts, in display order — see D-009) |
pick-file |
string (path) |
pick-file --multi |
array of strings (paths, ascending sort) |
pick-directory |
string (path) |
date |
string, ISO-8601 date (YYYY-MM-DD) |
time |
string, ISO-8601 time (HH:MM:SS) |
duration |
string, ISO-8601 duration (PT1H30M) |
color |
string, #RRGGBB (lowercase hex) |
attribute-picker |
object, {"fg": "#RRGGBB", "bg": "#RRGGBB", "style": "..."} |
linear-range |
object, shape depends on --mode. Single: {"mode":"single","value":"<label>","index":N}. Multi: {"mode":"multi","values":[...],"indices":[...]}. Range: {"mode":"range","kind":"closed|left|right|none",...} with start/end fields conditional on kind. See D-032. |
BuiltInClets.RegisterAll(ICletRegistry) hand-registers all 18 clets. Auto-discovery via a source generator was explored and dropped — there is no [Clet] attribute in shipped code, and no source-generator project in the repo.
Each input clet wraps a TG View in RunnableWrapper<TView, TResult>, which auto-extracts the typed result via IValue<TResult>. See src/Clet/Clets/Input/SelectClet.cs for the canonical example.
Pattern:
- The clet does not own the run loop or the application lifecycle; the host (
Program.Main) does. RunnableWrapper<TView, TResult>is the TG primitive that bridges Views to typed results.await app.RunAsync(wrapper, ct)uses theIApplication.RunAsync(Toplevel, CancellationToken)overload (§3).- Viewer clets follow the same shape but return
CletRunResult(noT). SelectCletreturnsstring?(the label text), notint?(the index) — see D-008.
See src/Clet/Hosting/Program.cs. The host creates a CancellationTokenSource, registers all clets, and delegates to CommandLineRoot.InvokeAsync(args, token, Console.Out, Console.Error).
clet <alias> [positional...] [--initial <value>] [--title|--prompt <text>] [--json] [--timeout 30s] [--fullscreen] [--cat] [--no-browse] [--rows <n>] [--output <path>] [--<opt> <value>]...
clet list [--json]
clet help <alias>
clet --help
clet --version
--version output. clet --version prints X.Y.Z (Terminal.Gui A.B.C) where X.Y.Z is clet's own SemVer and A.B.C is the TG version the binary was built against. See D-022.
--help banner. clet --help (and bare clet) prints the ASCII logo followed by usage:
╔═╗╦ ╔═╗╔╦╗
║ ║ ╠═ ║
╚═╝╩═╝╚═╝ ╩
Built-in flags. --initial, --title (alias --prompt), --json, --timeout, --fullscreen, --cat, --no-browse, --rows, and --output are parsed at the host level and apply to every clet. Anything else of the form --<name> <value> is validated against the dispatched clet's option descriptors and then forwarded as a clet-specific option (see each clet's clet help <alias>). Unknown options are rejected with a usage error (exit 2) instead of consuming the next token as a value. Bare positional tokens are forwarded as CletRunOptions.Arguments for clets that consume them (e.g. select, multi-select, md); clets that do not consume positional args reject them with a usage error (exit 2) before the clet runs. See D-025 for the AcceptsPositionalArgs design and D-014 for why --title is a host flag.
--cat (non-interactive rendering). When --cat is passed to a viewer clet (currently md), content is rendered as ANSI-formatted text directly to stdout — no alt-screen, no interactive session. Useful for piping (clet md --cat README.md | less -R), CI logs, and AI agents. Content is resolved from file arguments, --initial, or stdin, same as the normal viewer path. If no content is available, exits with usage error (exit 2). See D-027.
--no-browse (disable browser mode). When --no-browse is passed to md, clicking local .md links shows the URL in the status bar instead of navigating. By default, md runs as a browser: following local links navigates to them with a back/forward history stack (Ctrl+Left / Ctrl+Right or ← → buttons in the status bar), and fragment anchors (file.md#heading) scroll to the matching heading.
--output <path> / -o <path> (file output). Writes a successful clet result (plain text or JSON) to the specified file instead of stdout. OutputFormatter creates the file with non-overwriting semantics; existing paths are refused rather than truncated, including paths that are symlinks. Non-success results are not written to the output file and are emitted through the normal stdout/stderr paths. This works around the Terminal.Gui limitation where stdout redirection ($(), |, >) swallows the TUI (see tui-cs/Terminal.Gui#5207). If the file cannot be written, an error is emitted to stderr and the process exits with code 2. See D-028.
Input-size caps. --initial is capped at 64 K characters (code units) after alias resolution; unknown aliases remain usage errors rather than being reported as oversized-input validation failures. clet md stdin is capped at 8 M characters. On exceed for known aliases/content paths: exit 65, error code input-too-large, JSON envelope {"schemaVersion":1,"status":"error","code":"input-too-large","message":"..."}. These caps prevent OOM from untrusted piped input (see Appendix A). Per-clet options (--<name> <value>) are not yet capped; tracked as a follow-up.
Defaults. Input clets render inline. Viewer clets (md) render fullscreen. --fullscreen forces fullscreen for input clets; it's a no-op for viewers.
Theming. No --theme flag. Theme selection goes through ConfigurationManager's existing mechanisms.
Help rendering. clet --help and clet help <alias> render Markdown to ANSI escape sequences and write to stdout (print mode), then exit immediately — no interactive viewer (see D-016). Root help reads from embedded src/Clet/Help/overview.md; per-alias help is generated dynamically from IClet metadata by MarkdownHelpRenderer.BuildAliasHelpMarkdown(). Help is pipeable (clet --help | less) and consumable by AI agents reading stdout.
| Status | Exit |
|---|---|
| Ok (input or viewer) | 0 |
| NoResult | 1 |
| Usage error | 2 |
| Validation error | 65 |
| I/O error | 74 |
| Cancelled | 130 |
In Clet.csproj:
<PublishAot>true</PublishAot>
<InvariantGlobalization>false</InvariantGlobalization>
<StackTraceSupport>true</StackTraceSupport>
<DebuggerSupport>false</DebuggerSupport>
<EventSourceSupport>false</EventSourceSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>Target binary size: ~8MB. Cold-start budget: <100ms on Apple Silicon, <100ms on Linux x64, <150ms on Windows x64.
tui-cs/Terminal.Gui and tui-cs/Editor fire repository_dispatch events to tui-cs/clet for main-branch package publishes (tg-main-published and editor-main-published). Develop-package dispatches do not publish clet builds.
Additionally, the release workflow fires on pushes to clet's own main branch (changes in src/ or tests/) and manual workflow_dispatch.
The actual workflow is .github/workflows/release.yml. It builds AOT binaries for the target RIDs. Code signing is deferred post-1.0 per D-012; Homebrew ships a build-from-source formula.
Before any publish step, every built binary runs a smoke matrix. The gate is process-level: it spawns the AOT'd binary, drives it from outside, and asserts on exit code + stdout JSON.
Driver: tui-cs/TUIcast in deterministic-script mode. TUIcast spawns the binary inside a PTY, writes keystrokes to the PTY fd, and captures an asciinema stream. Deterministic mode takes a comma-separated keystroke script ("wait:500,ArrowDown,Enter").
Cases:
clet --version— returnsX.Y.Z (Terminal.Gui A.B.C).clet list --json— validates against the schema.- For each input clet: TUIcast spawns with
--initial <stub> --json --timeout 1sand a per-clet keystroke script; verify exit 0 and JSON envelope. - For
md: spawns against a fixture markdown file with"wait:500,q"; verify exit 0 and{"schemaVersion":1,"status":"ok"}. - Cancellation: spawn with
--timeout 100ms, no keystrokes; verify exit 130 and{"schemaVersion":1,"status":"cancelled"}.
Asciinema artifact. TUIcast captures every smoke run as .cast; on failure, the cast is uploaded as a workflow artifact.
Any failure halts the publish workflow.
After all matrix jobs and smoke tests pass. Channel determines which publish steps run:
| Channel | Trigger | NuGet | Homebrew | WinGet |
|---|---|---|---|---|
| Prerelease | push to main, main-branch TG/Editor dispatch, or manual dispatch while <Version> has -rc |
prerelease | — | — |
| Stable | push to main, main-branch TG/Editor dispatch, or manual dispatch while <Version> has no prerelease suffix |
stable | build-from-source | manifest PR |
.NET tool (NuGet) — follows the mdv pattern: <PackAsTool>true</PackAsTool>, <ToolCommandName>clet</ToolCommandName>, <PackageId>clet</PackageId> on src/Clet/Clet.csproj. Install: dotnet tool install -g clet. See D-019 (packaging) and D-024 (package id).
Homebrew tap (tui-cs/homebrew-tap) — release channel only. Build-from-source formula per D-012.
WinGet (PR to microsoft/winget-pkgs) — release channel only. Unsigned binary with SmartScreen warning acceptable for early adopters per D-012.
If any publish step fails:
- The workflow opens or updates an issue titled
Release clet v<VERSION> failed (<channel>). - Release failures page; repeated failures for the same open incident are added as comments.
- Already-published channels are noted; rollback is manual (see
docs/runbooks/release-rollback.md).
clet maintains its own SemVer, independent of Terminal.Gui's version. Major bumps = schemaVersion changes (§4.3.1). Minor bumps = new clets or significant CLI additions. Patch bumps = bug fixes, including rebuilds against new TG versions. The TG version is surfaced in --version output for diagnostics only. See D-022.
Auto-increment. The release workflow reads the base version from Clet.csproj <Version>. For the rc phase (1.0.0-rc), it finds the latest matching v1.0.0-rc.* tag and increments the prerelease build number. For stable phases (1.0.0), it finds the latest stable vMAJOR.MINOR.* tag and increments patch. Manual workflow_dispatch can override the computed version (required when dispatching from a non-main branch).
Directory.Build.props declares <TerminalGuiVersion> and <TerminalGuiEditorVersion> known-good pins, and the Clet project references both via MSBuild properties. The release workflow passes -p:TerminalGuiVersion=${{ env.TG_VERSION }} and -p:TerminalGuiEditorVersion=${{ env.TGE_VERSION }}. Release restores fail if either resolved version is older than the latest stable NuGet package.
Full testing strategy lives in tests/SPEC.md. Summary:
- Nine test layers, each with a clear "what does this catch" purpose. Three harness families: in-process logic (no
Application.Init), in-process UI (IApplication+InputInjection+Driver.Contentssnapshots, frame-stepped), process-level (TUIcast over PTY). - The four-terminal manual matrix (#23) is the v0.5 gate.
- The JSON contract tests are the schema-lock guard (
SchemaV1). - The smoke tests are the release gate.
Schedule follows TG releases, not a calendar.
| Milestone | Tracking | Exit criteria |
|---|---|---|
| v0.1 alpha | #2 | Repo bootstrapped; abstractions, registry, JSON in place; select clet working in unit + integration tests. No runnable binary — see v0.11. |
| v0.11 | #9 | Runnable binary. CLI host per §4.6/§4.7. clet --help / --version / help <alias> / list --json / <alias> --json work end-to-end. Process-level smoke harness (Process.Start-based; TUIcast keystroke harness deferred to v0.3 — D-007). |
| v0.3 alpha | #3 | All 14 input clets functional. JSON schema drafted. AOT publish green. TUIcast keystroke harness wired up. |
| v0.5 beta | #4 | Naming/schema/exit-codes locked; inline rendering verified on four-terminal matrix; Markdown View integration verified; threat model published (docs/threat-model.md); dotnet tool install -g clet works locally (D-019, D-024); main-channel rc workflow proven. Release-tag trigger proof and Homebrew/WinGet draft manifests moved to v0.9 RC. |
| v0.75 rc | #33 | Friends-and-family rc. >=5 external testers; >=3 Issues filed by non-maintainers; maintainer dogfooding for >=2 weeks; >=1 AI agent harness consuming --json; all P0 rc bugs resolved or deferred. |
| v0.9 RC | #5 | All §6 test layers passing. Release workflow proven against a real TG release. Homebrew formula + WinGet manifest in working-draft form. One real release cycle exercised. Rollback runbook exercised once. |
| v1.0 GA | #6 | Tied to TG v2 GA. Brew, WinGet, NuGet channels live. Documentation published. |
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| AOT issue surfaces during build or smoke test | Medium | Medium | AOT publish tests (tests/SPEC.md §2.7) catch before publish; fall back to self-contained single-file (~30MB) if blocking. |
| Native installer pipeline (Homebrew/WinGet) ops cost | Medium | Medium | §5.3 smoke gate + release-pipeline dry-runs catch most issues; docs/runbooks/release-rollback.md documents withdrawal. |
Markdown View quality regression vs glow |
Low | Medium | TG-side golden-file corpus (#5156); quarterly comparison run. |
| Develop publishes create NuGet version sprawl | — | Develop no longer publishes. Main-only rc channel eliminates version sprawl. | |
| First real release fails mid-publish | Medium | High | Weekly release-pipeline dry-runs; docs/runbooks/release-rollback.md walks through per-channel withdrawal. Runbook exercised before v0.9 RC. |
| Naming concerns about "clet" | Low | Low | Acknowledge in docs; outlast. |
- Telemetry. Not in v1.0 scope; revisit at v1.1 with a privacy review.
- Homebrew tap repo name.
tui-cs/homebrew-tapassumed; confirm it exists or create. - Code signing certs. Deferred post-1.0 per D-012. Confirm ownership/renewal before signing is re-enabled.
Resolved (D-015). Both file arguments and stdin, with precedence: file args ->mdcontent source.--initial-> stdin -> error.- PR/FAQ update upstream. Issue #5155's PR/FAQ still references
Terminal.Gui.Cletsas a separate assembly. Update the issue body to match this spec before v1.0. - "Any-View ambition" / auto-discovered clets. Deferred to v2 per D-021. See §11.
Steps 1-10 are done (through v0.5). Remaining:
- Publish channels: Homebrew (build-from-source), then WinGet, then NuGet tool push.
- v0.75 rc — friends-and-family testing (#33).
- v0.9 RC — release workflow proven against real TG release; Homebrew/WinGet manifests in working-draft form; rollback runbook exercised.
- v1.0 GA.
The original PR/FAQ pitched clet as exposing any IValue<T> View to the shell automatically. v1.0 ships 15 hand-written clets instead. This section captures the design exploration; D-021 records the deferral to v2.
15 clets, each ~50-150 lines under src/Clet/Clets/{Input,Viewer}/. Each declares aliases, description, kind, result type, options, and a RunAsync. BuiltInClets.RegisterAll is hand-written (D-004/D-021).
- Most per-clet code is metadata, not behavior.
TextCletis 65 lines; unique behavior is 2 lines. - Cross-cutting concerns live above clets.
--title(D-014), scheme (D-013), link safety (D-017), exit codes, JSON envelope — all host-level. - Wire format isn't derivable from
IValue<T>.SelectClet'sIValue<int?>becomes astringon the wire.PickFile'sIValue<IReadOnlyList<string>?>becomes string or array depending on--multi. The §4.3.2 table is hand-curated. - Initial-value parsing is per-clet.
IntCletparses"42"asint;SelectCletparses as label-to-index lookup;PickFileCletdoesn't take an initial.
TG-side (the harder half): [Shellable] attribute or marker interface, wire-format declaration (ToWire hook), initial-value parser, per-View option surface.
clet-side: Real source generator scanning TG assemblies for [Shellable] types, emitting registration + dispatch glue.
Cross-cutting: Schema-lock coupling (TG attribute changes become wire-format changes); plugin-loading exclusion still applies (runtime LoadFrom remains out of scope).
Don't pursue in v1.x. The leverage only kicks in with a long tail of post-v1.0 Views or third-party Views wanting shell exposure — both are post-v1.0 stories. At v2, use §11.3 as the starting checklist for TG-side co-design. See D-021.
- Is the v2 third-party-clets story still wanted?
- Would TG core accept
[Shellable]and friends, or must they live in a companion package? - Can
IValue<T>plus conventions cover 90% of clets, with overrides for the rest? - Does adding a
[Shellable]View on TG develop trigger a clet wire-format change?
Full document published at docs/threat-model.md.
- Untrusted inputs:
--initial, env vars, stdin content, fixture file paths,--title, clet-specific options. - Input-size caps:
--initialis capped at 64 K characters after alias resolution;clet mdstdin is capped at 8 M characters. On exceed for known aliases/content paths: exit 65, error codeinput-too-large, JSON envelope{"schemaVersion":1,"status":"error","code":"input-too-large","message":"..."}. Per-clet options are not yet capped; tracked as a follow-up. - Sanitization: A
TerminalEscapeSanitizerstrips ESC, BEL, 8-bit CSI/OSC, and C1 7-bit pairs from all user-supplied content before it reaches the terminal driver or Terminal.Gui views. Applied atMarkdownClet(inline + file content) andMarkdownHelpRenderer.RenderToAnsi(input + rendered output). clet does not rely on TG to filter terminal escapes (D-030). - Markdown link policy: Default
SurfaceOnly(links shown, never auto-opened).--allow-link-openflag deferred — not wired in v1.0; safe-by-default for AI agent use. See D-017, D-031. - File access:
clet mdconfines file reads by default to.md,.markdown, and.txtfiles under the working directory, with per-file, aggregate-size, glob-count, and binary-content checks.--allow-file <path>explicitly opts in a file or directory outside that default boundary;--allow-binaryopts out of NUL-byte rejection.pick-fileandpick-directorytreat--rootas a starting directory, not a sandbox, and rely on the OS permission model for interactive file selection. - Plugin loading: None in v1.0. (Closes the entire LoadFrom-based attack surface.)
- PR/FAQ: issue #5155
- TG core docs:
docfx/docs/application.md,docfx/docs/View.md,docfx/docs/cancellable-work-pattern.md - Contributor rules:
.claude/rules/,CLAUDE.md