diff --git a/Terminal.Gui.Cli.slnx b/Terminal.Gui.Cli.slnx
index f78cb7c..63b74b2 100644
--- a/Terminal.Gui.Cli.slnx
+++ b/Terminal.Gui.Cli.slnx
@@ -17,6 +17,7 @@
+
diff --git a/examples/survey/ProfileInput.cs b/examples/survey/ProfileInput.cs
new file mode 100644
index 0000000..934ed4e
--- /dev/null
+++ b/examples/survey/ProfileInput.cs
@@ -0,0 +1,85 @@
+using System.Globalization;
+
+namespace Terminal.Gui.Cli.Survey;
+
+/// Shared option descriptors and headless parsing for the survey command.
+public static class ProfileInput
+{
+ /// Per-command options accepted by the survey command.
+ public static IReadOnlyList Options { get; } =
+ [
+ new ("name", "n", typeof (string), "The person's name.", false, null),
+ new ("fruits", "f", typeof (string), "Comma-separated list of favorite fruits.", false, null),
+ new ("sport", "s", typeof (string), "Favorite sport.", false, null),
+ new ("age", "a", typeof (int), "Age in years (1-120).", false, null),
+ new ("password", "p", typeof (string), "Password (secret).", false, null),
+ new ("color", "c", typeof (string), "Favorite color (optional).", false, null),
+ new ("confirm", null, typeof (bool), "Show a confirmation step before finishing.", false, null)
+ ];
+
+ /// A sample profile used when invoked without options in headless mode.
+ public static SurveyAnswers Sample { get; } =
+ new ("Ada Lovelace", ["Apple", "Cherry"], "Apple", "Fencing", 36, "Passw0rd!", "Teal");
+
+ ///
+ /// Builds a from command-line options. Returns false with an
+ /// when a provided value is invalid. A missing name is not an error;
+ /// callers inspect to decide whether to prompt interactively.
+ ///
+ public static bool TryBuild (
+ CommandRunOptions options,
+ string? initial,
+ out SurveyAnswers answers,
+ out string? error)
+ {
+ ArgumentNullException.ThrowIfNull (options);
+ error = null;
+ answers = null!;
+
+ var name = options.CommandOptions.TryGetValue ("name", out var nameValue) &&
+ !string.IsNullOrWhiteSpace (nameValue)
+ ? nameValue
+ : options.Arguments.Count > 0
+ ? string.Join (" ", options.Arguments)
+ : initial ?? string.Empty;
+
+ var fruits =
+ options.CommandOptions.TryGetValue ("fruits", out var fruitsValue) &&
+ !string.IsNullOrWhiteSpace (fruitsValue)
+ ? fruitsValue.Split (',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
+ : [];
+
+ var sport = options.CommandOptions.TryGetValue ("sport", out var sportValue) &&
+ !string.IsNullOrWhiteSpace (sportValue)
+ ? sportValue
+ : "Unspecified";
+
+ var age = 0;
+
+ if (options.CommandOptions.TryGetValue ("age", out var ageText))
+ {
+ if (!int.TryParse (ageText, NumberStyles.None, CultureInfo.InvariantCulture, out age) || age < 1 ||
+ age > 120)
+ {
+ error = $"Invalid age '{ageText}'. Provide a whole number between 1 and 120.";
+ return false;
+ }
+ }
+
+ var password = options.CommandOptions.TryGetValue ("password", out var passwordValue) &&
+ !string.IsNullOrWhiteSpace (passwordValue)
+ ? passwordValue
+ : string.Empty;
+
+ var color = options.CommandOptions.TryGetValue ("color", out var colorValue) &&
+ !string.IsNullOrWhiteSpace (colorValue)
+ ? colorValue
+ : null;
+
+ // Determine favorite fruit: first fruit if only one, otherwise null (to be picked interactively)
+ var favoriteFruit = fruits.Length == 1 ? fruits[0] : null;
+
+ answers = new SurveyAnswers (name, fruits, favoriteFruit, sport, age, password, color);
+ return true;
+ }
+}
diff --git a/examples/survey/Program.cs b/examples/survey/Program.cs
new file mode 100644
index 0000000..7d59132
--- /dev/null
+++ b/examples/survey/Program.cs
@@ -0,0 +1,3 @@
+using Terminal.Gui.Cli.Survey;
+
+return await SurveyApp.CreateHost ().RunAsync (args);
diff --git a/examples/survey/README.md b/examples/survey/README.md
new file mode 100644
index 0000000..b7fddd2
--- /dev/null
+++ b/examples/survey/README.md
@@ -0,0 +1,44 @@
+# survey
+
+A sample CLI app built with `Terminal.Gui.Cli` that demonstrates the
+**Terminal.Gui + Spectre.Console** collaboration described in
+[spectre.console#2128](https://github.com/spectreconsole/spectre.console/issues/2128).
+
+It is a port of Spectre.Console's `Prompt` example: where Spectre uses blocking
+console prompts, this app uses a Terminal.Gui Wizard for interaction, then renders
+the collected profile with Spectre.Console.
+
+| Concern | Owner |
+|---------|-------|
+| Interaction (Wizard, navigation, validation) | Terminal.Gui |
+| Rich rendering (Panel, Table) | Spectre.Console |
+| Scriptable surfaces (`--json`, `--opencli`, agent guide) | `Terminal.Gui.Cli` |
+
+## Usage
+
+```bash
+# Interactive Terminal.Gui Wizard (Enter to accept, Esc to quit)
+survey
+
+# Headless: provide answers as options
+survey --name Ada --age 36 --sport Fencing --fruits "Apple,Cherry" --color Teal
+
+# Structured JSON envelope for scripts and agents
+survey --name Ada --age 36 --fruits "Apple,Cherry" --json
+
+# Browse help in the TUI markdown viewer
+survey help
+```
+
+## Commands
+
+| Command | Description |
+|----------|-------------|
+| `survey` | Collect a profile and return it as structured data. |
+| `help` | Show command help in a TUI markdown viewer. |
+
+## Running
+
+```bash
+dotnet run --project examples/survey -- survey --name Ada --json
+```
diff --git a/examples/survey/Resources/Help/help.md b/examples/survey/Resources/Help/help.md
new file mode 100644
index 0000000..db0e38d
--- /dev/null
+++ b/examples/survey/Resources/Help/help.md
@@ -0,0 +1,23 @@
+# survey
+
+A sample app showing how `Terminal.Gui.Cli` and `Spectre.Console` complement each
+other: Terminal.Gui handles interaction via a Wizard, Spectre.Console renders rich
+output, and the host adds scriptable JSON, OpenCLI, and an agent guide.
+
+## Commands
+
+| Command | Description |
+|----------|------------------------------------------------------|
+| `survey` | Collect a profile and return it as structured data. |
+| `help` | Show command help in a TUI markdown viewer. |
+
+See [survey](help:survey) for details.
+
+## Framework Options
+
+| Option | Description |
+|--------|-------------|
+| `--help` / `-h` | Show help |
+| `--version` | Show version |
+| `--opencli` | Emit OpenCLI metadata JSON |
+| `--json` | Emit JSON envelope output |
diff --git a/examples/survey/Resources/Help/survey.md b/examples/survey/Resources/Help/survey.md
new file mode 100644
index 0000000..bfd266d
--- /dev/null
+++ b/examples/survey/Resources/Help/survey.md
@@ -0,0 +1,31 @@
+# survey
+
+Collect a profile and return it as structured data.
+
+[Back to main help](help:help)
+
+## Usage
+
+```
+survey Launch the interactive Terminal.Gui wizard
+survey --name Ada --age 36 --sport Fencing
+survey --name Ada --fruits "Apple,Cherry" --json
+```
+
+## Options
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `--name`, `-n` | string | The person's name. |
+| `--fruits`, `-f` | string | Comma-separated list of favorite fruits. |
+| `--sport`, `-s` | string | Favorite sport. |
+| `--age`, `-a` | integer | Age in years (1-120). |
+| `--password`, `-p` | string | Password (secret). |
+| `--color`, `-c` | string | Favorite color (optional). |
+| `--confirm` | flag | Show a confirmation step before finishing. |
+
+## Behavior
+
+When `--name` is provided, the command runs headless and returns the profile. With
+`--json` it emits the full `SurveyAnswers` object. With no name in an interactive
+terminal, it launches a Terminal.Gui Wizard (press Enter to accept, Esc to quit).
diff --git a/examples/survey/Resources/agent-guide.md b/examples/survey/Resources/agent-guide.md
new file mode 100644
index 0000000..b826926
--- /dev/null
+++ b/examples/survey/Resources/agent-guide.md
@@ -0,0 +1,44 @@
+# Survey App Agent Guide
+
+This document describes how AI agents should interact with `survey`.
+
+## Available Commands
+
+### survey
+
+An input command that collects a profile and returns it as structured data.
+
+- **Alias:** `survey`
+- **Kind:** Input
+- **Result type:** `object` (`SurveyAnswers`)
+- **Options:** `--name`/`-n`, `--fruits`/`-f` (comma-separated), `--sport`/`-s`,
+ `--age`/`-a` (1-120), `--password`/`-p`, `--color`/`-c`
+
+**Usage:**
+
+```bash
+survey --name Ada --age 36 --sport Fencing --fruits "Apple,Cherry" --json
+```
+
+Provide `--name` to run headless (no TUI). Use `--json` for the structured envelope.
+
+## JSON Envelope
+
+`survey --json` emits the structured result. The result type is serialized through a
+source-generated JSON context registered on the host (no reflection):
+
+```json
+{
+ "schemaVersion": 1,
+ "status": "ok",
+ "value": {
+ "name": "Ada",
+ "fruits": ["Apple", "Cherry"],
+ "favoriteFruit": "Apple",
+ "sport": "Fencing",
+ "age": 36,
+ "password": "Passw0rd!",
+ "color": "Teal"
+ }
+}
+```
diff --git a/examples/survey/SpectreProfile.cs b/examples/survey/SpectreProfile.cs
new file mode 100644
index 0000000..7887c73
--- /dev/null
+++ b/examples/survey/SpectreProfile.cs
@@ -0,0 +1,71 @@
+using System.Globalization;
+using Spectre.Console;
+using Spectre.Console.Rendering;
+
+namespace Terminal.Gui.Cli.Survey;
+
+///
+/// Builds the Spectre.Console renderable for a profile and renders it to a text writer.
+/// Spectre is the rendering engine; Terminal.Gui (via the host and, in the TUI, the
+/// Terminal.Gui.Interop.Spectre bridge) handles presentation and interaction.
+///
+public static class SpectreProfile
+{
+ /// Builds the results table renderable describing the profile.
+ /// The survey answers to render.
+ ///
+ /// Optional background color for the table borders and padding. When rendering inside
+ /// a Terminal.Gui view (e.g. the confirm step), pass the superview's background so
+ /// the table blends in. Not used for final stdout output.
+ ///
+ public static IRenderable Build (SurveyAnswers answers, Color? backgroundColor = null)
+ {
+ ArgumentNullException.ThrowIfNull (answers);
+
+ Table table = new Table ()
+ .Border (TableBorder.Rounded)
+ .AddColumn (new TableColumn ("[bold]Question[/]"))
+ .AddColumn (new TableColumn ("[bold]Answer[/]"));
+
+ if (backgroundColor is not null)
+ {
+ table.BorderColor (backgroundColor.Value);
+ }
+
+ table.AddRow (new Markup ("Name"), new Markup ($"[green]{Markup.Escape (answers.Name)}[/]"));
+
+ var favFruit = answers.FavoriteFruit ?? "none";
+ table.AddRow (new Markup ("Favorite fruit"), new Markup (Markup.Escape (favFruit)));
+ table.AddRow (new Markup ("Favorite sport"), new Markup (Markup.Escape (answers.Sport)));
+ table.AddRow (new Markup ("Age"), new Markup (answers.Age.ToString (CultureInfo.InvariantCulture)));
+
+ var password = answers.Password.Length > 0 ? new string ('*', answers.Password.Length) : "[grey]none[/]";
+ table.AddRow (new Markup ("Password"), new Markup (password));
+
+ var color = answers.Color is null ? "[grey]unspecified[/]" : Markup.Escape (answers.Color);
+ table.AddRow (new Markup ("Favorite color"), new Markup (color));
+
+ return table;
+ }
+
+ /// Renders the profile to as ANSI (or plain text when not a terminal).
+ public static void RenderToAnsi (SurveyAnswers answers, TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull (writer);
+
+ IAnsiConsole console = AnsiConsole.Create (new AnsiConsoleSettings
+ {
+ Out = new AnsiConsoleOutput (writer),
+ Ansi = AnsiSupport.Detect,
+ ColorSystem = ColorSystemSupport.Detect,
+ Interactive = InteractionSupport.No
+ });
+
+ if (console.Profile.Width < 40)
+ {
+ console.Profile.Width = 100;
+ }
+
+ console.Write (Build (answers));
+ }
+}
diff --git a/examples/survey/SurveyAnswers.cs b/examples/survey/SurveyAnswers.cs
new file mode 100644
index 0000000..10c61a1
--- /dev/null
+++ b/examples/survey/SurveyAnswers.cs
@@ -0,0 +1,20 @@
+namespace Terminal.Gui.Cli.Survey;
+
+/// The structured result of the survey: a person's profile.
+public sealed record SurveyAnswers (
+ string Name,
+ IReadOnlyList Fruits,
+ string? FavoriteFruit,
+ string Sport,
+ int Age,
+ string Password,
+ string? Color)
+{
+ /// Renders the profile as a Spectre.Console table (with ANSI color codes) for terminal output.
+ public override string ToString ()
+ {
+ using StringWriter sw = new ();
+ SpectreProfile.RenderToAnsi (this, sw);
+ return sw.ToString ().TrimEnd ();
+ }
+}
diff --git a/examples/survey/SurveyApp.cs b/examples/survey/SurveyApp.cs
new file mode 100644
index 0000000..899686d
--- /dev/null
+++ b/examples/survey/SurveyApp.cs
@@ -0,0 +1,28 @@
+using System.Reflection;
+
+namespace Terminal.Gui.Cli.Survey;
+
+/// Builds the configured for the survey example.
+public static class SurveyApp
+{
+ /// Creates and configures the host with the survey and card commands registered.
+ public static CliHost CreateHost ()
+ {
+ Assembly assembly = typeof (SurveyApp).Assembly;
+
+ CliHost host = new (options =>
+ {
+ options.ApplicationName = "survey";
+ options.Version = "1.0.0";
+ options.DefaultCommand = "survey";
+ options.AgentGuide = "Terminal.Gui.Cli.Survey.agent-guide.md";
+ options.AgentGuideIsResource = true;
+ options.ResourceAssembly = assembly;
+ options.HelpProvider = new EmbeddedMarkdownHelpProvider (assembly);
+ options.ResultJsonResolver = SurveyJsonContext.Default;
+ });
+
+ host.Registry.Register (new SurveyCommand ());
+ return host;
+ }
+}
diff --git a/examples/survey/SurveyCommand.cs b/examples/survey/SurveyCommand.cs
new file mode 100644
index 0000000..9501af2
--- /dev/null
+++ b/examples/survey/SurveyCommand.cs
@@ -0,0 +1,521 @@
+using System.Collections.ObjectModel;
+using System.Globalization;
+using Terminal.Gui.App;
+using Terminal.Gui.Configuration;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.Interop.Spectre;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+using Attribute = Terminal.Gui.Drawing.Attribute;
+using Color = Spectre.Console.Color;
+
+namespace Terminal.Gui.Cli.Survey;
+
+///
+/// An input command that collects a person's profile via a Wizard and returns it as a typed
+/// . With --name (or other options) it runs headless and emits
+/// a structured --json envelope; otherwise it launches an interactive Terminal.Gui Wizard.
+///
+public sealed class SurveyCommand : ICliCommand
+{
+ ///
+ public string PrimaryAlias => "survey";
+
+ ///
+ public IReadOnlyList Aliases { get; } = ["survey"];
+
+ ///
+ public string Description => "Collect a profile and return it as structured data.";
+
+ ///
+ public CommandKind Kind => CommandKind.Input;
+
+ ///
+ public Type ResultType => typeof (SurveyAnswers);
+
+ ///
+ public IReadOnlyList Options => ProfileInput.Options;
+
+ ///
+ public bool AcceptsPositionalArgs => true;
+
+ ///
+ public async Task> RunAsync (
+ IApplication app,
+ string? initial,
+ CommandRunOptions options,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull (app);
+
+ if (!ProfileInput.TryBuild (options, initial, out SurveyAnswers answers, out var error))
+ {
+ return new CommandResult (CommandStatus.Error, null, "validation", error);
+ }
+
+ if (!string.IsNullOrWhiteSpace (answers.Name))
+ {
+ return new CommandResult (CommandStatus.Ok, answers, null, null);
+ }
+
+ if (Console.IsInputRedirected)
+ {
+ return new CommandResult (
+ CommandStatus.Error,
+ null,
+ "validation",
+ "A name is required. Pass --name in non-interactive mode.");
+ }
+
+ var confirm = options.CommandOptions.ContainsKey ("confirm");
+ SurveyAnswers? captured = await RunWizardAsync (app, confirm, cancellationToken);
+
+ if (captured is null)
+ {
+ return new CommandResult (CommandStatus.Cancelled, null, null, null);
+ }
+
+ return new CommandResult (CommandStatus.Ok, captured, null, null);
+ }
+
+ private static async Task RunWizardAsync (
+ IApplication app,
+ bool confirm,
+ CancellationToken cancellationToken)
+ {
+ Wizard wizard = new ()
+ {
+ Title = "Survey - Enter to accept, Esc to quit",
+ Width = Dim.Fill (),
+ Height = 17, // tall enough for the largest step (fruits tree fully expanded + label + buttons)
+ BorderStyle = LineStyle.Rounded,
+ SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Accent),
+ ShadowStyle = null
+ };
+ wizard.Border.Thickness = new Thickness (0, 1, 0, 0);
+
+ // --- Step 1: Name ---
+ WizardStep nameStep = new ()
+ {
+ Title = "Name",
+ HelpText = """
+ ## Your Name
+
+ Enter your **full name** or a nickname.
+
+ This will be displayed on your profile card.
+
+ > *Tip:* Press `Tab` to move to the Next button.
+ """
+ };
+ Label nameLabel = new () { Text = "_Name:" };
+ TextField nameField = new ()
+ {
+ X = Pos.Right (nameLabel) + 1,
+ Y = 0,
+ Width = Dim.Percent (50)
+ };
+ nameStep.Add (nameLabel, nameField);
+ wizard.AddStep (nameStep);
+
+ // --- Step 2: Favorite Fruits (multi-select) ---
+ WizardStep fruitsStep = new ()
+ {
+ Title = "Fruits",
+ HelpText = """
+ ## Favorite Fruits
+
+ Select your favorites from the list:
+
+ - Press `Space` to toggle a selection
+ - Use `↑`/`↓` to navigate
+
+ ### Categories
+
+ Some items are grouped under **Berries**:
+
+ 1. Strawberry
+ 2. Blueberry
+ 3. Raspberry
+ """
+ };
+ Label fruitsLabel = new () { Text = "_Favorite fruits (Space to toggle):" };
+ TreeView fruitsTree = CreateFruitsTreeView ();
+
+ fruitsStep.Add (fruitsLabel, fruitsTree);
+ wizard.AddStep (fruitsStep);
+
+ // --- Step 3: Conditional single-pick (only if >1 fruit selected) ---
+ WizardStep favFruitStep = new ()
+ {
+ Title = "Favorite Fruit",
+ HelpText = """
+ ## Pick One
+
+ You selected *multiple* fruits — now choose your **absolute favorite**.
+
+ This step only appears when more than one fruit is selected.
+ """
+ };
+ Label favFruitLabel = new () { Text = "Ok, but if you could only choose _one:" };
+ ListView favFruitList = new ()
+ {
+ X = 0,
+ Y = 1,
+ Width = Dim.Auto (),
+ Height = Dim.Fill ()
+ };
+ favFruitStep.Add (favFruitLabel, favFruitList);
+ wizard.AddStep (favFruitStep);
+
+ // --- Step 4: Favorite Sport ---
+ WizardStep sportStep = new ()
+ {
+ Title = "Sport",
+ HelpText = """
+ ## Favorite Sport
+
+ Pick from the list or type your own.
+
+ - **Soccer** – The beautiful game
+ - **Hockey** – Fast-paced ice sport
+ - **Basketball** – Slam dunks!
+
+ > If you type a custom sport, the selector deselects.
+ """
+ };
+ Label sportLabel = new () { Text = "Favorite _sport:" };
+ OptionSelector sportSelector = new ()
+ {
+ X = 0,
+ Y = 1,
+ Width = Dim.Fill (),
+ Labels = ["Soccer", "Hockey", "Basketball"],
+ Value = null
+ };
+ Label sportOrLabel = new ()
+ {
+ X = 0,
+ Y = Pos.Bottom (sportSelector) + 1,
+ Text = "Or _type your own:"
+ };
+ TextField sportTextField = new ()
+ {
+ X = Pos.Right (sportOrLabel) + 1,
+ Y = Pos.Bottom (sportSelector) + 1,
+ Width = Dim.Percent (50)
+ };
+
+ sportSelector.ValueChanged += (_, args) =>
+ {
+ if (args.NewValue is >= 0 && args.NewValue < sportSelector.Labels!.Count)
+ {
+ sportTextField.Text = sportSelector.Labels[args.NewValue.Value];
+ }
+ };
+
+ sportTextField.TextChanged += (_, _) =>
+ {
+ var text = sportTextField.Text.Trim ();
+
+ if (sportSelector.Value is not null &&
+ !string.Equals (text, sportSelector.Labels![sportSelector.Value.Value],
+ StringComparison.OrdinalIgnoreCase))
+ {
+ sportSelector.Value = null;
+ }
+ };
+
+ sportStep.Add (sportLabel, sportSelector, sportOrLabel, sportTextField);
+ wizard.AddStep (sportStep);
+
+ // --- Step 5: Age (validated) ---
+ WizardStep ageStep = new ()
+ {
+ Title = "Age",
+ HelpText = """
+ ## Your Age
+
+ Enter a number between **1** and **120**.
+
+ ### Validation Rules
+
+ - Must be a whole number
+ - No letters or symbols
+ - Range: `1–120`
+
+ An error message will appear if the value is invalid.
+ """
+ };
+ Label ageLabel = new () { Text = "_Age (1-120):" };
+ TextField ageField = new ()
+ {
+ X = Pos.Right (ageLabel) + 1,
+ Y = 0,
+ Width = Dim.Percent (50)
+ };
+ Label ageError = new ()
+ {
+ X = 0,
+ Y = 1,
+ Width = Dim.Fill (),
+ Visible = false
+ };
+ ageStep.Add (ageLabel, ageField, ageError);
+ wizard.AddStep (ageStep);
+
+ // --- Step 6: Password ---
+ WizardStep passwordStep = new ()
+ {
+ Title = "Password",
+ HelpText = """
+ ## Password
+
+ Enter a secret password. Characters are masked with `*`.
+
+ ### Guidelines
+
+ - **Minimum length:** none (for this demo)
+ - Input is *not* echoed to the terminal
+ - Stored only for display in the results card
+ """
+ };
+ Label passwordLabel = new () { Text = "_Password:" };
+ TextField passwordField = new ()
+ {
+ X = Pos.Right (passwordLabel) + 1,
+ Y = 0,
+ Width = Dim.Percent (50),
+ Secret = true
+ };
+ passwordStep.Add (passwordLabel, passwordField);
+ wizard.AddStep (passwordStep);
+
+ // --- Step 7: Favorite Color ---
+ WizardStep colorStep = new ()
+ {
+ Title = "Color",
+ HelpText = """
+ ## Favorite Color
+
+ Use the **ColorPicker** to choose a color:
+
+ - Adjust `H`, `S`, `V` sliders
+ - Or type a hex value directly (e.g. `#FF6600`)
+ - The color name is shown below
+
+ ### Color Spaces
+
+ The picker supports [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV) color space.
+ """
+ };
+ Label colorLabel = new () { Text = "Favorite _color:" };
+ ColorPicker colorPicker = new ()
+ {
+ X = 0,
+ Y = 1,
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+ };
+ colorPicker.Style.ShowTextFields = true;
+ colorPicker.Style.ShowColorName = true;
+ colorPicker.ApplyStyleChanges ();
+ colorStep.Add (colorLabel, colorPicker);
+ wizard.AddStep (colorStep);
+
+ // --- Optional Step 8: Confirmation with Spectre card rendering ---
+ WizardStep? confirmStep = null;
+ SpectreView? confirmView = null;
+
+ if (confirm)
+ {
+ confirmStep = new WizardStep
+ {
+ Title = "Confirm",
+ HelpText = """
+ ## Review & Confirm
+
+ Check your answers in the table on the left.
+
+ - Press **Finish** to accept
+ - Press **Back** to make changes
+ - Press `Esc` to cancel entirely
+
+ > Your results will be printed to the terminal after confirmation.
+ """
+ };
+ Label confirmLabel = new () { Text = "Review your answers and press Finish to _confirm:" };
+ confirmView = new SpectreView
+ {
+ X = 0,
+ Y = 2,
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+ };
+ confirmStep.Add (confirmLabel, confirmView);
+ wizard.AddStep (confirmStep);
+ }
+
+ // --- Step navigation ---
+ // Track direction so we only auto-skip the favFruitStep when moving forward.
+ var movingForward = true;
+ wizard.MovingBack += (_, _) => movingForward = false;
+ wizard.MovingNext += (_, _) => movingForward = true;
+
+ wizard.StepChanged += (_, _) =>
+ {
+ if (wizard.CurrentStep == favFruitStep)
+ {
+ List selected = GetSelectedFruits (fruitsTree);
+
+ switch (movingForward)
+ {
+ case true when selected.Count <= 1:
+ wizard.GoNext ();
+ break;
+ case false when selected.Count <= 1:
+ wizard.GoBack ();
+ break;
+ default:
+ favFruitList.SetSource (new ObservableCollection (selected));
+ break;
+ }
+ }
+ else if (wizard.CurrentStep == confirmStep && confirmView is not null)
+ {
+ SurveyAnswers preview = BuildAnswers (
+ nameField, fruitsTree, favFruitList, sportTextField, ageField, passwordField, colorPicker);
+
+ // Get the background color from the wizard step so the table blends in
+ Attribute attr = confirmStep!.GetAttributeForRole (VisualRole.Normal);
+ Color? spectreBg = null;
+
+ if (attr is { Background: var tgBg } && tgBg != Drawing.Color.None)
+ {
+ spectreBg = new Color (tgBg.R, tgBg.G, tgBg.B);
+ }
+
+ confirmView.Renderable = SpectreProfile.Build (preview, spectreBg);
+ }
+ };
+
+ // Validate age before allowing advancement past the age step
+ wizard.MovingNext += (_, args) =>
+ {
+ if (wizard.CurrentStep != ageStep)
+ {
+ return;
+ }
+
+ var ageText = ageField.Text.Trim ();
+
+ if (ageText.Length == 0 ||
+ !int.TryParse (ageText, NumberStyles.None, CultureInfo.InvariantCulture, out var age) ||
+ age < 1 || age > 120)
+ {
+ ageError.Text = "Please enter a valid age between 1 and 120.";
+ ageError.Visible = true;
+ args.Cancel = true;
+ }
+ else
+ {
+ ageError.Visible = false;
+ }
+ };
+
+ // Capture results when wizard finishes
+ SurveyAnswers? result = null;
+
+ wizard.Accepted += (_, _) =>
+ {
+ result = BuildAnswers (
+ nameField, fruitsTree, favFruitList, sportTextField, ageField, passwordField, colorPicker);
+ };
+
+ await app.RunAsync (wizard, cancellationToken);
+
+ return result;
+ }
+
+ private static SurveyAnswers BuildAnswers (
+ TextField nameField,
+ TreeView fruitsTree,
+ ListView favFruitList,
+ TextField sportTextField,
+ TextField ageField,
+ TextField passwordField,
+ ColorPicker colorPicker)
+ {
+ var name = nameField.Text.Trim ();
+ List selectedFruits = GetSelectedFruits (fruitsTree);
+
+ var favoriteFruit = selectedFruits.Count == 1
+ ? selectedFruits[0]
+ : favFruitList.Value is >= 0 && favFruitList.Value < (favFruitList.Source?.Count ?? 0)
+ ? favFruitList.Source!.ToList ()[favFruitList.Value.Value]?.ToString ()
+ : selectedFruits.Count > 0
+ ? selectedFruits[0]
+ : null;
+
+ var sport = sportTextField.Text.Trim ();
+
+ if (sport.Length == 0)
+ {
+ sport = "Unspecified";
+ }
+
+ var ageText = ageField.Text.Trim ();
+ int.TryParse (ageText, NumberStyles.None, CultureInfo.InvariantCulture, out var age);
+
+ var password = passwordField.Text ?? string.Empty;
+ var color = colorPicker.SelectedColor.ToString ();
+
+ return new SurveyAnswers (name, selectedFruits, favoriteFruit, sport, age, password, color);
+ }
+
+ /// Builds the hierarchical fruit tree with "Berries" as a parent category.
+ private static TreeView CreateFruitsTreeView ()
+ {
+ TreeView tree = new ()
+ {
+ X = 0,
+ Y = 1,
+ Width = Dim.Auto (),
+ Height = Dim.Fill (),
+ CheckboxMode = true
+ };
+
+ tree.AddObject (new TreeNode { Text = "Apple" });
+ tree.AddObject (new TreeNode { Text = "Apricot" });
+ tree.AddObject (new TreeNode { Text = "Banana" });
+
+ tree.AddObject (new TreeNode
+ {
+ Text = "Berries",
+ Children =
+ [
+ new TreeNode { Text = "Blackberry" },
+ new TreeNode { Text = "Blueberry" },
+ new TreeNode { Text = "Raspberry" },
+ new TreeNode { Text = "Strawberry" }
+ ]
+ });
+
+ tree.AddObject (new TreeNode { Text = "Mango" });
+ tree.AddObject (new TreeNode { Text = "Orange" });
+ tree.AddObject (new TreeNode { Text = "Pear" });
+
+ tree.ExpandAll ();
+
+ return tree;
+ }
+
+ /// Gets the list of checked leaf-node fruit names from the tree.
+ private static List GetSelectedFruits (TreeView fruitsTree)
+ {
+ return fruitsTree.GetCheckedObjects ()
+ .Where (node => node.Children.Count == 0) // leaf nodes only (skip "Berries" category)
+ .Select (node => node.Text)
+ .ToList ();
+ }
+}
diff --git a/examples/survey/SurveyJsonContext.cs b/examples/survey/SurveyJsonContext.cs
new file mode 100644
index 0000000..52c3ed9
--- /dev/null
+++ b/examples/survey/SurveyJsonContext.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui.Cli.Survey;
+
+///
+/// Source-generated JSON context for . Registered on the host via
+/// CliHostOptions.ResultJsonResolver so the --json envelope can serialize the result
+/// without reflection (constitution C4).
+///
+[JsonSourceGenerationOptions (
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonSerializable (typeof (SurveyAnswers))]
+public sealed partial class SurveyJsonContext : JsonSerializerContext;
diff --git a/examples/survey/Terminal.Gui.Cli.Survey.csproj b/examples/survey/Terminal.Gui.Cli.Survey.csproj
new file mode 100644
index 0000000..fd7ad8c
--- /dev/null
+++ b/examples/survey/Terminal.Gui.Cli.Survey.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ Terminal.Gui.Cli.Survey
+ survey
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Terminal.Gui.Cli.IntegrationTests/SurveyExampleTests.cs b/tests/Terminal.Gui.Cli.IntegrationTests/SurveyExampleTests.cs
new file mode 100644
index 0000000..1348c6b
--- /dev/null
+++ b/tests/Terminal.Gui.Cli.IntegrationTests/SurveyExampleTests.cs
@@ -0,0 +1,120 @@
+using System.Text.Json;
+using Terminal.Gui.Cli.Survey;
+using Xunit;
+
+namespace Terminal.Gui.Cli.IntegrationTests;
+
+///
+/// Exercises the survey example end-to-end through the real configured host: headless input
+/// and structured JSON (via the host's ResultJsonResolver).
+///
+public sealed class SurveyExampleTests
+{
+ private static readonly string[] FullProfileArgs =
+ [
+ "survey", "--name", "Ada", "--age", "36", "--sport", "Fencing", "--fruits", "Apple,Cherry", "--color", "Teal",
+ "--password", "secret123"
+ ];
+
+ [Fact]
+ public async Task Survey_Headless_ReturnsSummary ()
+ {
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await SurveyApp.CreateHost ()
+ .RunAsync (FullProfileArgs, TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.Ok, exitCode);
+ Assert.Contains ("Ada", stdout.ToString ());
+ Assert.Contains ("Fencing", stdout.ToString ());
+ Assert.Equal (string.Empty, stderr.ToString ());
+ }
+
+ [Fact]
+ public async Task Survey_Json_EmitsStructuredObject ()
+ {
+ string[] args = [.. FullProfileArgs, "--json"];
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await SurveyApp.CreateHost ()
+ .RunAsync (args, TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.Ok, exitCode);
+
+ using JsonDocument document = JsonDocument.Parse (stdout.ToString ());
+ JsonElement value = document.RootElement.GetProperty ("value");
+ Assert.Equal (JsonValueKind.Object, value.ValueKind);
+ Assert.Equal ("Ada", value.GetProperty ("name").GetString ());
+ Assert.Equal (36, value.GetProperty ("age").GetInt32 ());
+ Assert.Equal ("Teal", value.GetProperty ("color").GetString ());
+ Assert.Equal (2, value.GetProperty ("fruits").GetArrayLength ());
+ Assert.Equal ("secret123", value.GetProperty ("password").GetString ());
+ }
+
+ [Fact]
+ public async Task Survey_Json_OmitsNullColor ()
+ {
+ string[] args = ["survey", "--name", "Bob", "--age", "20", "--json"];
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await SurveyApp.CreateHost ()
+ .RunAsync (args, TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.Ok, exitCode);
+
+ using JsonDocument document = JsonDocument.Parse (stdout.ToString ());
+ JsonElement value = document.RootElement.GetProperty ("value");
+ Assert.False (value.TryGetProperty ("color", out _));
+ }
+
+ [Fact]
+ public async Task Survey_InvalidAge_ReturnsValidationError ()
+ {
+ string[] args = ["survey", "--name", "Ada", "--age", "999"];
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await SurveyApp.CreateHost ()
+ .RunAsync (args, TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.ValidationError, exitCode);
+ Assert.Contains ("Invalid age", stderr.ToString ());
+ }
+
+ [Fact]
+ public async Task Survey_EmptyArgs_WithName_RunsDefaultCommand ()
+ {
+ // When empty args are passed but --name is provided via the default command routing,
+ // the host should route to the survey command (not print help).
+ // We verify this by passing just "--name" which goes through default command dispatch.
+ string[] args = ["--name", "Ada", "--age", "25"];
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await SurveyApp.CreateHost ()
+ .RunAsync (args, TestContext.Current.CancellationToken, stdout, stderr);
+
+ // Default command routing should run the survey, not print help
+ Assert.Equal (ExitCodes.Ok, exitCode);
+ Assert.Contains ("Ada", stdout.ToString ());
+ Assert.DoesNotContain ("--help", stdout.ToString ());
+ }
+
+ [Fact]
+ public async Task Survey_ConfirmOption_Accepted ()
+ {
+ // The --confirm option should be recognized and not cause an error
+ string[] args = ["survey", "--name", "Ada", "--age", "25", "--confirm"];
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await SurveyApp.CreateHost ()
+ .RunAsync (args, TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.Ok, exitCode);
+ Assert.Contains ("Ada", stdout.ToString ());
+ }
+}
diff --git a/tests/Terminal.Gui.Cli.IntegrationTests/Terminal.Gui.Cli.IntegrationTests.csproj b/tests/Terminal.Gui.Cli.IntegrationTests/Terminal.Gui.Cli.IntegrationTests.csproj
index 35b5cfa..cd400f8 100644
--- a/tests/Terminal.Gui.Cli.IntegrationTests/Terminal.Gui.Cli.IntegrationTests.csproj
+++ b/tests/Terminal.Gui.Cli.IntegrationTests/Terminal.Gui.Cli.IntegrationTests.csproj
@@ -13,6 +13,7 @@
+