Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,19 @@ Use Spectre.Console for rich UI — prompts auto-upgrade transparently:
app.UseSpectreConsole(); // existing IReplInteractionChannel calls render as Spectre prompts
```

When Spectre owns the screen, treat that as a separate rendering surface. Use `IAnsiConsole` for one-shot renderables and prompts, but do not mix a full-screen/live Spectre UI with regular REPL feedback on the same surface. If your app enters a TUI or live display flow, resolve `SpectreInteractionPresenter` from DI and capture interaction output for the duration of that scope:

```csharp
app.Map("dashboard", static async (
SpectreInteractionPresenter presenter,
IReplIoContext io,
CancellationToken ct) =>
{
using var capture = presenter.BeginCapture(io.Error);
await RunDashboardAsync(ct);
});
```

That keeps status/progress/problem events out of the main Spectre surface and avoids terminal control sequences fighting with your TUI.

See also: [Modules](module-presence.md) | [Route System](route-system.md) | [MCP Overview](mcp-overview.md) | [Testing](testing-toolkit.md) | [Configuration](configuration-reference.md)
3 changes: 2 additions & 1 deletion docs/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ app.Options(o =>
});
```

See also: [Commands](commands.md) | [Shell Completion](shell-completion.md) | [Interaction](interaction.md)
See also: [Commands](commands.md) | [Shell Completion](shell-completion.md) | [Interaction](interaction.md) | [Progress](progress.md)

## ReplOptions

Expand Down Expand Up @@ -136,6 +136,7 @@ These options are configured through `app.Options(...)`. Repl does not currently

- `DefaultProgressLabel` (`string`, default: `"Progress"`) — Default label for progress indicators.
- `ProgressTemplate` (`string`, default: `"{label}: {percent:0}%"`) — Progress display template. Supports placeholders: `{label}`, `{percent}`, `{percent:0}`, `{percent:0.0}`.
- `AdvancedProgressMode` (`AdvancedProgressMode`, default: `Auto`) — Controls whether compatible hosts emit advanced terminal progress sequences. See [Progress](progress.md#advanced-terminal-progress).
- `PromptFallback` (`PromptFallback`, default: `UseDefault`) — Behavior when interactive prompts are unavailable.

## ShellCompletionOptions
Expand Down
59 changes: 58 additions & 1 deletion docs/interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ await channel.PressAnyKeyAsync("Press any key to continue...", cancellationToken

## Progress reporting

This section summarizes the portable progress APIs. See [Progress](progress.md) for the full rendering model, `OSC 9;4` behavior, MCP mapping, and TUI guidance.

Handlers inject `IProgress<T>` to report progress. The framework creates the appropriate adapter automatically.

### Simple percentage: `IProgress<double>`
Expand Down Expand Up @@ -289,6 +291,8 @@ var app = ReplApp.Create(services =>

This enables third-party packages (e.g. Spectre.Console, Terminal.Gui, or GUI frameworks) to provide their own rendering without replacing the channel logic (validation, retry, prefill, timeout).

If you use `Repl.Spectre`, the package now registers `SpectreInteractionPresenter` as the default presenter when no custom presenter already exists. That implementation wraps the built-in console behavior and also supports temporary capture for screen-owned flows.

The presenter receives strongly-typed semantic events:

| Event type | When emitted |
Expand Down Expand Up @@ -383,6 +387,28 @@ public class SpectreInteractionHandler : IReplInteractionHandler

Use a **presenter** when you only want to change how things look. Use a **handler** when you want to replace the entire interaction for a given request type.

## Spectre and screen ownership

`IAnsiConsole.Write(...)` is great for one-shot renderables, banners, and prompt-driven flows. It is not a good fit for a full-screen or continuously refreshed TUI if normal REPL feedback is still writing to the same terminal surface.

In particular:

- Do not mix a Spectre live display / full-screen surface with normal REPL status or progress output on the same writer.
- `OSC 9;4` progress is terminal feedback for CLI-style execution, not a rendering primitive for TUIs.
- If your app temporarily owns the screen, capture interaction output away from the main surface.

With `Repl.Spectre`, use `SpectreInteractionPresenter.BeginCapture(...)`:

```csharp
var presenter = services.GetRequiredService<SpectreInteractionPresenter>();
var io = services.GetRequiredService<IReplIoContext>();

using var capture = presenter.BeginCapture(io.Error);
await RunFullScreenDashboardAsync(cancellationToken);
```

You can capture to any `IReplInteractionPresenter` or to a plain `TextWriter`. For application handlers, prefer a session-aware sink such as `IReplIoContext.Error` or a custom presenter registered by your host. The `TextWriter` overload emits plain text only — no ANSI styling, no line rewriting, and no `OSC 9;4`.

### Custom request types

Apps can define their own `InteractionRequest<TResult>` subtypes for app-specific controls:
Expand Down Expand Up @@ -525,7 +551,38 @@ With this setup:
- `AskConfirmationAsync` renders as a `ConfirmationPrompt`
- `AskTextAsync` renders as a `TextPrompt<string>`
- `AskSecretAsync` renders as a `TextPrompt<string>.Secret()`
- Collections returned from handlers render as bordered Spectre tables
- Collections returned from handlers render as lightweight Spectre tables

### Output formats

`UseSpectreConsole()` registers the `spectre` output format and makes it the default. You can still switch per-command:

- `--spectre` selects the Spectre renderer
- `--human` switches back to the standard text renderer
- `--output:<format>` remains the canonical format selector

`--help` respects the selected format:

- `human` renders the classic text help
- `spectre` renders dedicated Spectre help
- `json/xml/yaml/markdown` keep the structured help pipeline

### Presenter capture for future TUIs

When a command temporarily owns the terminal surface, capture interaction events explicitly:

```csharp
app.Map("dashboard", static async (
SpectreInteractionPresenter presenter,
IReplIoContext io,
CancellationToken ct) =>
{
using var capture = presenter.BeginCapture(io.Error);
await RunDashboardAsync(ct);
});
```

This is the intended integration point for future TUI tooling.

Command handlers remain unchanged — the upgrade from built-in prompts to Spectre prompts is transparent.

Expand Down
2 changes: 1 addition & 1 deletion docs/mcp-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ app.UseMcpServer(o => o.InteractivityMode = InteractivityMode.PrefillThenElicita

> **Why this matters:** Console-style writes blur the boundary between result data, progress, logs, and protocol traffic. In MCP, this ranges from confusing agent behavior to protocol corruption.

`WriteProgressAsync` maps to MCP progress notifications. `WriteStatusAsync` maps to log messages (`level: info`):
`WriteProgressAsync` maps to MCP progress notifications. `WriteStatusAsync` maps to log messages (`level: info`). See [Progress](progress.md#mcp) for the centralized progress model across console, hosted sessions, Spectre, and MCP:

```csharp
app.Map("import", async (IReplInteractionChannel interaction, CancellationToken ct) =>
Expand Down
23 changes: 22 additions & 1 deletion docs/output-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,27 @@ The active output format is resolved in this order:
| Format | Description |
|------------|--------------------------------------|
| `human` | Plain text, intended for terminals. |
| `spectre` | Lightweight Spectre.Console rendering for terminals. |
| `json` | JSON serialization. |
| `xml` | XML serialization. |
| `yaml` | YAML serialization. |
| `markdown` | Markdown table/document rendering. |

### Format aliases

The alias `yml` resolves to `yaml`. Additional aliases can be registered.
The built-in aliases are:

- `--human` -> `human`
- `--json` -> `json`
- `--xml` -> `xml`
- `--yaml` / `--yml` -> `yaml`
- `--markdown` -> `markdown`

When `Repl.Spectre` is enabled, it also registers:

- `--spectre` -> `spectre`

Additional aliases can be registered.

## Custom Transformers

Expand Down Expand Up @@ -69,6 +82,14 @@ The startup banner is controlled by:

The banner is only rendered when the active format is in the `BannerFormats` set and `BannerEnabled` is `true`.

## Help output

`--help` uses the active output format:

1. `human` renders the classic text help.
2. `spectre` renders dedicated Spectre help.
3. Structured formats (`json`, `xml`, `yaml`, `markdown`) use the machine-readable help pipeline.

## Render Width

The output width used for wrapping and table layout is resolved as:
Expand Down
217 changes: 217 additions & 0 deletions docs/progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# Progress

Progress in Repl is semantic feedback from a command handler to the active host. A handler reports that work is moving forward; the host decides whether that becomes a console line, terminal taskbar progress, MCP progress notifications, hosted-session feedback, or a captured side stream.

Use progress for user-facing execution feedback. Do not use it for logs, command results, or long-lived TUI rendering primitives.

## Quick Choice

| Need | Use |
|---|---|
| Simple percent complete | `IProgress<double>` |
| Label, current/total, state, or details | `IProgress<ReplProgressEvent>` |
| Direct async control from a handler | `IReplInteractionChannel` progress helpers |
| MCP-only progress/message control | `IMcpFeedback` |
| Spectre live/TUI progress bars | `IAnsiConsole.Progress(...)`, with Repl feedback captured away from the main surface |

## Simple Progress

Handlers can request `IProgress<double>`. The framework creates the adapter automatically.

```csharp
app.Map("sync", async (IProgress<double> progress, CancellationToken ct) =>
{
for (var i = 1; i <= 10; i++)
{
progress.Report(i * 10.0);
await Task.Delay(100, ct);
}

return "done";
});
```

`IProgress<double>` is connected to `IReplInteractionChannel.WriteProgressAsync(...)`. Reporting `42` is equivalent to sending a normal progress event with the configured default label and `Percent = 42`.

## Structured Progress

Use `IProgress<ReplProgressEvent>` when the progress update needs a label, computed percentage, state, unit, or details.

```csharp
app.Map("import", async (IProgress<ReplProgressEvent> progress, CancellationToken ct) =>
{
var total = 100;
for (var i = 1; i <= total; i++)
{
progress.Report(new ReplProgressEvent(
"Importing contacts",
Current: i,
Total: total,
Unit: "contacts"));

await Task.Delay(25, ct);
}

return "done";
});
```

`ReplProgressEvent.ResolvePercent()` uses `Percent` when set. Otherwise, it computes a percentage from `Current` and `Total` when both are available.

## Channel Helpers

Use `IReplInteractionChannel` when the handler is already async and you want explicit control.

```csharp
app.Map("import", async (IReplInteractionChannel channel, CancellationToken ct) =>
{
await channel.WriteProgressAsync("Preparing import", 10, ct);

await channel.WriteIndeterminateProgressAsync(
"Waiting for remote review",
"The agent is still processing.",
ct);

await channel.WriteWarningProgressAsync(
"Retrying duplicate check",
percent: 55,
details: "The remote worker timed out once.",
ct);

await channel.WriteErrorProgressAsync(
"Import failed",
percent: 80,
details: "The final retry window was exhausted.",
ct);

await channel.ClearProgressAsync(ct);
});
```

Available states:

| State | Meaning |
|---|---|
| `Normal` | Regular progress update |
| `Warning` | Work is continuing, but the user should pay attention |
| `Error` | The current workflow has entered an error state |
| `Indeterminate` | Work is active but there is no meaningful percentage yet |
| `Clear` | Clear any visible progress indicator |

`percent: null` does not mean indeterminate. Use `WriteIndeterminateProgressAsync(...)` or `State = ReplProgressState.Indeterminate` explicitly.

## Rendering Pipeline

All portable progress APIs converge on the same semantic pipeline:

```text
IProgress<double>
IProgress<ReplProgressEvent>
IReplInteractionChannel progress helpers
|
v
WriteProgressRequest / ReplProgressEvent
|
v
Host presenter or transport
```

The built-in hosts render that semantic event differently:

| Host | Behavior |
|---|---|
| Console/default host | Text fallback, optional in-place rewriting, optional advanced terminal progress |
| Spectre presenter | Same semantic event, with explicit capture support for TUI/live surfaces |
| MCP | `notifications/progress`, plus warning/error message notifications for structured states |
| Hosted sessions | Session-aware output and terminal capabilities drive rendering |

The framework clears visible progress automatically when a command completes, fails, or is cancelled.

## Advanced Terminal Progress

The console presenter can emit `OSC 9;4` progress sequences in addition to normal text feedback. These sequences are useful for terminal taskbar progress or hosted terminal integrations.

`InteractionOptions.AdvancedProgressMode` controls this behavior:

| Value | Behavior |
|---|---|
| `Auto` | Emit advanced progress only for known-compatible terminals or sessions advertising progress support |
| `Always` | Emit advanced progress whenever the host can write terminal control sequences |
| `Never` | Disable advanced progress and keep text-only rendering |

Advanced progress is emitted only when the console presenter can safely write terminal control sequences. It is disabled for protocol passthrough and for terminal multiplexer sessions such as `tmux` and `screen` in automatic mode.

`OSC 9;4` is additional feedback. It does not replace the text progress line.

## Text Labels And TUI Surfaces

Console progress has a text fallback. That fallback includes a label, so it can break a full-screen TUI or a Spectre live display if both write to the same terminal surface.

When a command temporarily owns the screen, capture regular Repl interaction feedback away from the main TUI surface:

```csharp
app.Map("dashboard", static async (
SpectreInteractionPresenter presenter,
IReplIoContext io,
CancellationToken ct) =>
{
using var capture = presenter.BeginCapture(io.Error);
await RunDashboardAsync(ct);
});
```

The `TextWriter` capture overload emits plain text only. It does not emit ANSI styling, line rewriting, or `OSC 9;4`.

For a custom TUI, another option is to capture to an `IReplInteractionPresenter` that ignores `ReplProgressEvent` or renders it inside an app-owned panel.

## Repl Progress Vs Spectre Progress

`IProgress<double>`, `IProgress<ReplProgressEvent>`, and `IReplInteractionChannel` are portable Repl feedback APIs. They work across console, hosted sessions, and MCP.

`Spectre.Console.Progress` is a Spectre rendering primitive. Use it when your command owns a Spectre surface and you want Spectre's progress bar UI. It is not automatically connected to Repl progress events.

If you use Spectre live displays or progress bars, avoid sending normal Repl progress to the same writer unless you capture or redirect it.

## Configuration

Progress display is configured through `ReplOptions.Interaction`:

```csharp
app.Options(o =>
{
o.Interaction.DefaultProgressLabel = "Sync";
o.Interaction.ProgressTemplate = "[{label}] {percent:0.0}%";
o.Interaction.AdvancedProgressMode = AdvancedProgressMode.Auto;
});
```

`ProgressTemplate` supports:

| Placeholder | Example |
|---|---|
| `{label}` | `Sync` |
| `{percent}` | `12.5` |
| `{percent:0}` | `13` |
| `{percent:0.0}` | `12.5` |

## MCP

Portable progress should normally use `IReplInteractionChannel`. In MCP mode, Repl maps those calls to MCP feedback:

| Repl API | MCP behavior |
|---|---|
| `WriteProgressAsync("Label", 40)` | `notifications/progress` with `progress = 40`, `total = 100` |
| `WriteIndeterminateProgressAsync(...)` | `notifications/progress` with a message and no `total` |
| `WriteWarningProgressAsync(...)` | `notifications/progress` plus a warning-level message notification |
| `WriteErrorProgressAsync(...)` | `notifications/progress` plus an error-level message notification |

Use `IMcpFeedback` only when the command is intentionally MCP-specific and needs direct control over MCP notifications.

## Rules Of Thumb

- Return command results as handler return values, not as progress.
- Use progress for transient execution feedback the current user should see.
- Use `ILogger` for operator diagnostics and centralized logs.
- Use `IProgress<double>` for simple percent updates.
- Use structured progress or channel helpers for warning/error/indeterminate states.
- In TUI or Spectre live surfaces, capture or redirect Repl progress so text labels do not fight the app-owned screen.
Loading
Loading