Skip to content

Interactive Prompts

Some commands need to ask the user questions, show progress, or handle long-running work gracefully. Repl provides a typed interaction channel for all of these.

Inject IReplInteractionChannel (namespace Repl.Interaction) to ask the user questions mid-command:

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}).");
});

In REPL mode this presents an interactive prompt. In CLI mode with --answer:* flags, answers are pre-filled deterministically.

string[] options = ["admin", "editor", "viewer"];
var roleIndex = await ui.AskChoiceAsync("role", "Assign role:", options, ct: ct);
var role = options[roleIndex];

AskChoiceAsync returns the zero-based index of the selected item. AskMultiChoiceAsync returns IReadOnlyList<int>.

app.Map("delete {id:int}", async (int id, IReplInteractionChannel ui, IContactStore store, CancellationToken ct) =>
{
var contact = store.Get(id);
var confirmed = await ui.AskConfirmationAsync("confirm", $"Delete '{contact.Name}'? This cannot be undone.");
if (!confirmed)
return Results.Cancelled("Delete cancelled.");
store.Delete(id);
return Results.Success($"Deleted '{contact.Name}'.");
});

Each WriteProgressAsync call updates the progress display. Fire-and-forget style — no disposable to manage:

app.Map("import {file}", 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…", (double)(i + 1) / lines.Length, ct);
}
return Results.Success($"Imported {lines.Length} records.");
});
app.Map("ping {host}", async (string host, CancellationToken ct) =>
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(5));
try
{
var reply = await Ping(host, cts.Token);
return $"{host}: {reply.RoundtripTime}ms";
}
catch (OperationCanceledException)
{
return Results.Error("timeout", $"Ping to {host} timed out after 5s.");
}
});

Pre-fill prompts by name from the command line — useful for scripts and tests without Repl.Testing:

Terminal window
myapp add --answer:name=Alice --answer:email=alice@example.com

This makes interactive commands fully scriptable without changing handler code. Prompt names match the first argument to AskTextAsync / AskChoiceAsync / AskConfirmationAsync.

With Repl.Spectre, all prompts upgrade to rich Spectre.Console widgets automatically:

app.UseSpectreConsole();

No handler changes required. See the Spectre cookbook for details.


  • Interactivity — complete prompt API: all prompt types, progress reporting, MCP interaction tiers, and --answer:* flag semantics.