Interactivity
Repl provides a typed interaction channel (IReplInteractionChannel) that works across all execution modes: interactive REPL, hosted sessions, MCP tools, and automated scripts. The same handler code asks questions in a terminal, receives answers from an MCP client, or reads pre-filled values from --answer:* flags.
The interaction channel
Section titled “The interaction channel”Inject IReplInteractionChannel (namespace Repl.Interaction) into your handler:
using Repl.Interaction;
app.Map("add", async (IReplInteractionChannel ui, IContactStore store, CancellationToken ct) =>{ var name = await ui.AskTextAsync("name", "Full name:", ct: ct); var email = await ui.AskTextAsync("email", "Email address:", ct: ct);
var contact = store.Add(name, email); return Results.Success($"Added {contact.Name} (#{contact.Id}).");});The first argument to every prompt method is a name — a stable identifier used to match --answer:name=value flags and to route MCP elicitation responses to the right prompt.
Prompt types
Section titled “Prompt types”Text input
Section titled “Text input”var name = await ui.AskTextAsync("name", "Full name:", ct: ct);Secret / password (masked)
Section titled “Secret / password (masked)”var password = await ui.AskSecretAsync("password", "Password:", ct: ct);In MCP mode, AskSecretAsync is prefill-only — the agent must supply the value upfront; elicitation and sampling are never used for secrets.
Confirmation
Section titled “Confirmation”var confirmed = await ui.AskConfirmationAsync("confirm", "Delete this record? This cannot be undone.", ct: ct);if (!confirmed) return Results.Cancelled("Deletion cancelled.");Single-choice selection
Section titled “Single-choice selection”string[] options = ["admin", "editor", "viewer"];var roleIndex = await ui.AskChoiceAsync("role", "Assign role:", options, ct: ct);var role = options[roleIndex];AskChoiceAsync returns the index of the selected item. In a capable terminal, this renders as an arrow-key menu; in plain mode it falls back to numbered choices.
Multi-choice selection
Section titled “Multi-choice selection”string[] allPerms = ["read", "write", "delete", "export"];var indices = await ui.AskMultiChoiceAsync("permissions", "Select permissions:", allPerms, ct: ct);var selected = indices.Select(i => allPerms[i]).ToArray();AskMultiChoiceAsync returns IReadOnlyList<int> — the indices of all selected items.
Utility
Section titled “Utility”await ui.WriteWarningAsync("Rate limit approaching.", ct);await ui.WriteProblemAsync("Connection failed.", ct);await ui.WriteStatusAsync("Processing...", ct);await ui.PressAnyKeyAsync("Press any key to continue.", ct);await ui.ClearScreenAsync(ct);Progress reporting
Section titled “Progress reporting”Determinate progress
Section titled “Determinate progress”Call WriteProgressAsync on each update — pass the label, a double? between 0 and 1, and the cancellation token:
app.Map("import {file}", static async (string file, IReplInteractionChannel ui, CancellationToken ct) =>{ var lines = await File.ReadAllLinesAsync(file, ct);
for (int i = 0; i < lines.Length; i++) { ct.ThrowIfCancellationRequested(); await ImportLine(lines[i], ct); await ui.WriteProgressAsync( $"Importing {lines.Length} records…", (double)(i + 1) / lines.Length, ct); }
return Results.Success($"Imported {lines.Length} records.");});Each WriteProgressAsync call is independent — there is no progress session object to manage.
Indeterminate progress (spinner)
Section titled “Indeterminate progress (spinner)”await ui.WriteIndeterminateProgressAsync("Connecting…", ct: ct);// … work …await ui.WriteProgressAsync("Connecting…", 1.0, ct); // signals completionAutomating prompts
Section titled “Automating prompts”--answer:* flags (CLI / CI scripts)
Section titled “--answer:* flags (CLI / CI scripts)”Pre-fill prompts by name from the command line. Each --answer:name=value targets the prompt whose name parameter matches:
# Answers the 'name' prompt with "Alice", 'email' with "alice@example.com"myapp add --answer:name=Alice --answer:email=alice@example.comPositional answers (by order) also work:
myapp add --answer:1=Alice --answer:2=alice@example.comAnswers in tests (Repl.Testing)
Section titled “Answers in tests (Repl.Testing)”Pass answers as a dictionary to the second overload of RunCommandAsync:
var result = await session.RunCommandAsync( "add", new Dictionary<string, string> { ["name"] = "Alice", ["email"] = "alice@example.com", }, ct);MCP interaction tiers
Section titled “MCP interaction tiers”When a command is called via MCP, Repl tries to answer prompts in this order:
| Tier | Mechanism | Notes |
|---|---|---|
| 1 | Prefill — from tool call arguments | Always tried first |
| 2 | Elicitation — structured form through the agent | Requires elicitation capability |
| 3 | Sampling — LLM generates an answer | Requires sampling capability |
| 4 | Default / Fail | Depends on InteractivityMode |
Configure the mode when enabling MCP:
app.UseMcpServer(o =>{ o.InteractivityMode = InteractivityMode.PrefillThenElicitation; // or: PrefillThenFail | PrefillThenDefaults | PrefillThenSampling});Upgrading with Spectre.Console
Section titled “Upgrading with Spectre.Console”Add Repl.Spectre and register:
app.UseSpectreConsole();All IReplInteractionChannel calls upgrade automatically to Spectre.Console widgets:
| Call | Spectre widget |
|---|---|
AskTextAsync | TextPrompt<string> |
AskSecretAsync | TextPrompt<string> (masked) |
AskConfirmationAsync | ConfirmationPrompt |
AskChoiceAsync | SelectionPrompt |
AskMultiChoiceAsync | MultiSelectionPrompt |
WriteProgressAsync | Live Progress display |
WriteIndeterminateProgressAsync | Live Status spinner |
No handler changes required.
Mnemonic shortcuts
Section titled “Mnemonic shortcuts”Prefix a choice item with _ to designate it as the keyboard shortcut:
var actions = new[] { "_Retry", "_Abort", "_Skip" };var idx = await ui.AskChoiceAsync("action", "What next?", actions, ct: ct);The user can press R, A, or S without navigating the menu.
Cancellation
Section titled “Cancellation”Every IReplInteractionChannel method accepts a CancellationToken. When Ctrl+C is pressed or the session is terminated, the token is cancelled, and the prompt throws OperationCanceledException — which Repl catches and reports as Results.Cancelled.
Press Esc to cancel a single prompt without cancelling the whole command (the method returns the prompt’s default value instead of throwing).