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
31 changes: 25 additions & 6 deletions .agents/skills/pretty-console-expert/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ using static System.Console; // optional
3. Choose APIs by intent.
- Styled output: `Console.WriteInterpolated`, `Console.WriteLineInterpolated`.
- Inputs/prompts: `Console.TryReadLine`, `Console.ReadLine`, `Console.Confirm`, `Console.RequestAnyInput`.
- Dynamic rendering: `Console.Overwrite`, `Console.ClearNextLines`, `Console.SkipLines`.
- Dynamic rendering and line control: `Console.Overwrite`, `Console.ClearNextLines`, `Console.SkipLines`, `Console.NewLine`.
- Progress UI: `ProgressBar.Update`, `ProgressBar.Render`, `Spinner.RunAsync`.
- Menus/tables: `Console.Selection`, `Console.MultiSelection`, `Console.TreeMenu`, `Console.Table`.
- Low-level override only: use `Console.Write(...)` / `Console.WriteLine(...)` span+`ISpanFormattable` overloads only when you intentionally bypass the handler for a custom formatting pipeline.

4. Route output deliberately.
- Keep normal prompts, menus, tables, durable user-facing output, and machine-readable non-error output on `OutputPipe.Out` unless there is a specific reason not to.
- Use `OutputPipe.Error` for transient live UI and for actual errors/diagnostics/warnings so stdout stays pipe-friendly and error output remains distinguishable.
- Do not bounce a single interaction between `Out` and `Error` unless you intentionally want that split; mixed-pipe prompts and retry messages are usually awkward in consumer CLIs.

## Handler Special Formats

- Use `:duration` with `TimeSpan` to render compact elapsed time text from the handler:
Expand All @@ -39,16 +44,26 @@ using static System.Console; // optional

- Prefer interpolated-handler APIs over manually concatenated strings.
- Avoid span/formattable `Write`/`WriteLine` overloads in normal app code; reserve them for rare advanced/manual formatting scenarios.
- If the intent is only to end the current line or emit a blank line, use `Console.NewLine(pipe)` instead of `WriteLineInterpolated($"")` or reset-only interpolations such as `$"{ConsoleColor.Default}"`.
- Keep ANSI/decorations inside interpolation holes (for example, `$"{Markup.Bold}..."`) instead of literal escape codes inside string literals.
- Route transient UI (spinner/progress/overwrite loops) to `OutputPipe.Error` to keep stdout pipe-friendly.
- For very frequent concurrent status updates, prefer a single-reader bounded `Channel<T>` that owns `Console.Overwrite(...)`; let workers `TryWrite` snapshots into a capacity-1 channel with `FullMode = DropWrite` so producers stay non-blocking and intermediate frames can be skipped.
- After the last overwrite/progress frame, clear the UI region once with `Console.ClearNextLines(totalLines, pipe)` or intentionally keep it with `Console.SkipLines`.
- Route transient UI (spinner/progress/overwrite loops) to `OutputPipe.Error` to keep stdout pipe-friendly, and use `OutputPipe.Error` for genuine errors/diagnostics. Keep ordinary non-error interaction flow on `OutputPipe.Out`.
- Spinner/progress/overwrite output is caller-owned after rendering completes. Explicitly remove it with `Console.ClearNextLines(totalLines, pipe)` or intentionally keep the region with `Console.SkipLines(totalLines)`.
- Only use the bounded `Channel<T>` snapshot pattern when multiple producers must update the same live region at high frequency. For single-producer or modest-rate updates, keep the rendering loop simple.

## Practical Patterns

- For wizard-like flows, wrap `Console.Selection(...)` / `Console.MultiSelection(...)` in retrying `Console.Overwrite(...)` loops so each step reuses one screen region instead of scrolling.
- For wizard-like flows, wrap `Console.Selection(...)` / `Console.MultiSelection(...)` in retrying `Console.Overwrite(...)` loops so each step reuses one screen region instead of scrolling. Keep the whole prompt/retry exchange on `OutputPipe.Out` unless the message is genuinely diagnostic.
- Prefer `Console.Overwrite(state, static ...)` for fixed-height live regions such as `status + progress`; it avoids closure captures and keeps the rendered surface explicit through `lines`.
- For dynamic spinner headers tied to concurrent work, keep the mutable step/progress state outside the spinner and read it with `Volatile.Read` / `Interlocked` inside the handler factory.
- For dynamic spinner/progress headers tied to concurrent work, keep the mutable step/progress state outside the renderer and read it with `Volatile.Read` / `Interlocked` inside the handler factory.
- If a live region should disappear after completion, pair the last render with an explicit `ClearNextLines(...)`. If it should remain visible as completed output, advance past it with `SkipLines(...)`.

## Testing CLI Code

- When a CLI already routes its behavior through callable command handlers or functions that use PrettyConsole directly, prefer in-process tests over spawning the whole app with `Process`.
- Inject `ConsoleContext.Out`, `ConsoleContext.Error`, and `ConsoleContext.In` with `StringWriter`/`StringReader`, invoke the same handler the CLI entrypoint uses internally, and assert on writers plus returned exit codes/results.
- Keep separate writers for `Out` and `Error` so pipe routing remains testable.
- Save and restore the original `ConsoleContext` streams in `try/finally` or a scoped helper.
- Reserve `Process` for true end-to-end coverage such as entrypoint wiring, shell integration, environment/current-directory behavior, published-binary checks, or argument parsing that is only exercised at the process boundary.

## API Guardrails (Current Surface)

Expand All @@ -57,6 +72,7 @@ using static System.Console; // optional
- Use `ProgressBar.Render(...)`, not `ProgressBar.WriteProgressBar(...)`.
- Use `ConsoleContext`, not `PrettyConsoleExtensions`.
- Use `ConsoleColor` helpers/tuples (for example `ConsoleColor.Red / ConsoleColor.White`), not removed `ColoredOutput`/`Color` types.
- Use `Console.NewLine(pipe)` when you only need a newline or blank line; do not use `WriteLineInterpolated` with empty/reset-only payloads just to move the cursor.
- Use `Confirm(ReadOnlySpan<string> trueValues, ref PrettyConsoleInterpolatedStringHandler handler, bool emptyIsTrue = true)` (boolean parameter is last).
- Use handler factory overloads for dynamic spinner/progress headers:
`(builder, out handler) => handler = builder.Build(OutputPipe.Error, $"...")`.
Expand All @@ -66,6 +82,7 @@ using static System.Console; // optional
```csharp
// Colored/status output
Console.WriteLineInterpolated($"{ConsoleColor.Green / ConsoleColor.DefaultBackground}OK{ConsoleColor.Default}");
Console.NewLine();

// Typed input
if (!Console.TryReadLine(out int port, $"Port ({ConsoleColor.Cyan}5000{ConsoleColor.Default}): "))
Expand All @@ -78,6 +95,7 @@ bool deploy = Console.Confirm(["y", "yes", "deploy"], $"Deploy now? ", emptyIsTr
var spinner = new Spinner();
await spinner.RunAsync(workTask, (builder, out handler) =>
handler = builder.Build(OutputPipe.Error, $"Syncing..."));
Console.ClearNextLines(1, OutputPipe.Error); // or Console.SkipLines(1) to keep the final row

// Progress rendering
var bar = new ProgressBar { ProgressColor = ConsoleColor.Green };
Expand All @@ -88,5 +106,6 @@ ProgressBar.Render(OutputPipe.Error, 65, ConsoleColor.Green);
## Reference File

Read [references/v5-api-map.md](references/v5-api-map.md) when you need exact usage snippets, migration mapping from old APIs, or a compile-fix checklist.
Read [references/testing-with-consolecontext.md](references/testing-with-consolecontext.md) when the task involves testing a PrettyConsole-based CLI or command handler.

If public API usage changes in the edited project, ask whether to update `README.md` and changelog/release-notes files.
2 changes: 1 addition & 1 deletion .agents/skills/pretty-console-expert/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface:
display_name: "PrettyConsole Expert"
short_description: "Use PrettyConsole APIs correctly for fast console UIs"
default_prompt: "Implement PrettyConsole features with current APIs, migration-safe names, and allocation-conscious patterns."
default_prompt: "Implement PrettyConsole features with current APIs, migration-safe names, allocation-conscious patterns, deliberate stdout/stderr routing for live UI and diagnostics, and explicit cleanup for live console UI."
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Testing PrettyConsole CLIs With `ConsoleContext`

Use this file when a task involves testing a CLI or command layer that already uses PrettyConsole internally.

## When This Applies

Prefer this approach when:

- the CLI logic already lives in callable commands, handlers, or functions
- those commands write via PrettyConsole APIs such as `Console.WriteInterpolated(...)`, `Console.WriteLineInterpolated(...)`, `Console.NewLine(...)`, `Console.TryReadLine(...)`, or `Console.Confirm(...)`
- you want fast, deterministic tests for stdout/stderr routing, prompts, parsed input, and returned exit codes/results

Do not default to `Process` in those cases. PrettyConsole already gives you an in-process seam through `ConsoleContext.Out`, `ConsoleContext.Error`, and `ConsoleContext.In`.

## Default Testing Pattern

1. Save the current `ConsoleContext.Out`, `ConsoleContext.Error`, and `ConsoleContext.In`.
2. Replace them with test doubles such as `StringWriter` and `StringReader`.
3. Invoke the same command handler or function the CLI entrypoint uses internally.
4. Assert on:
- returned exit code or result
- stdout text from `ConsoleContext.Out`
- stderr text from `ConsoleContext.Error`
5. Restore the original streams in `finally`.

## Output Assertions

- Use separate writers for `Out` and `Error`. Do not merge them unless the test intentionally treats both pipes as one stream.
- Assert on the selected pipe instead of parsing combined process output.
- If the test only cares about visible content, assert on the text written to the injected `StringWriter`.
- When testing pipe routing, assert that expected content landed on the correct writer and the other one stayed empty.

Example:

```csharp
var originalOut = ConsoleContext.Out;
var originalError = ConsoleContext.Error;
var stdout = new StringWriter();
var stderr = new StringWriter();

try {
ConsoleContext.Out = stdout;
ConsoleContext.Error = stderr;

int exitCode = await RunCommandAsync(options, cancellationToken);

await Assert.That(exitCode).IsEqualTo(0);
await Assert.That(stdout.ToString()).Contains("Completed");
await Assert.That(stderr.ToString()).IsEmpty();
} finally {
ConsoleContext.Out = originalOut;
ConsoleContext.Error = originalError;
}
```

## Input Assertions

- Feed stdin with `ConsoleContext.In = new StringReader("value" + Environment.NewLine)`.
- Call the real command/handler code directly.
- Assert on both the parsed result and the prompt written to the selected output pipe.

Example:

```csharp
var originalOut = ConsoleContext.Out;
var originalIn = ConsoleContext.In;
var stdout = new StringWriter();

try {
ConsoleContext.Out = stdout;
ConsoleContext.In = new StringReader("42" + Environment.NewLine);

int value = ReadPortFromUser();

await Assert.That(value).IsEqualTo(42);
await Assert.That(stdout.ToString()).Contains("Port");
} finally {
ConsoleContext.Out = originalOut;
ConsoleContext.In = originalIn;
}
```

## Suggested Shape For Consumer CLIs

Prefer this structure when a consumer CLI is testable but currently shells out to itself:

- thin entrypoint: parse args, compose dependencies, call a command handler/function
- command handler/function: returns an `int`, `Task<int>`, result object, or domain value
- all console I/O stays inside PrettyConsole calls, so tests can intercept it through `ConsoleContext`

This lets tests call the command handler directly instead of launching a child process and scraping console output.

## When `Process` Is Still Appropriate

Use `Process` only when the behavior truly exists at the process boundary, for example:

- verifying entrypoint wiring or host bootstrapping
- testing the real argument parser if it only runs in `Main`
- checking environment variables, current directory, exit codes from the actual executable, or shell integration
- validating published-binary behavior or packaging/install flows

Even then, keep the number of process-level tests small and focused. Most behavioral coverage should stay in-process.

## Anti-Pattern To Avoid

Avoid this default approach for PrettyConsole-based CLIs:

- launch the full executable with `Process`
- pass arguments
- capture combined output
- scrape strings to infer stdout/stderr behavior that the code already exposes directly through `ConsoleContext`

That style is slower, less precise, and makes it harder to verify separate pipes or inject interactive input.

## Extra Notes

- If the command uses both `Out` and `Error`, always keep two writers so routing bugs stay visible.
- If the command uses `Console.TryReadLine(...)`, `Console.ReadLine(...)`, or `Console.Confirm(...)`, inject `ConsoleContext.In` instead of trying to fake terminal input through a spawned process.
- If you need to verify transient UI behavior at a lower level, prefer calling the underlying render/update methods directly and asserting on the injected writers before reaching for process tests.
29 changes: 26 additions & 3 deletions .agents/skills/pretty-console-expert/references/v5-api-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ PrettyConsole methods are extension members on `System.Console`.
- `Console.Overwrite(...)`
- `Console.ClearNextLines(...)`
- `Console.SkipLines(...)`
- `Console.NewLine(...)`
- Progress:
- `ProgressBar.Update(...)`
- `ProgressBar.Render(...)`
Expand All @@ -48,6 +49,12 @@ PrettyConsole methods are extension members on `System.Console`.
- `Console.TreeMenu(...)`
- `Console.Table(...)`

### Output routing

- Keep prompts, menus, tables, final user-facing output, and machine-readable non-error output on `OutputPipe.Out` unless you intentionally need a different split.
- Use `OutputPipe.Error` for transient live UI and for actual errors/diagnostics/warnings so stdout remains pipe-friendly and error output stays distinct.
- Avoid mixing a single interactive exchange across `Out` and `Error` unless the split is intentional.

### Interpolated-handler special formats

- `TimeSpan` with `:duration`:
Expand All @@ -74,6 +81,12 @@ Use these only when intentionally bypassing the interpolated handler for a custo
- `Console.Write(ReadOnlySpan<char> ...)`
- `Console.WriteLine<T>(...)`

### New lines and blank lines

- Use `Console.NewLine(pipe)` when the intent is only to end the current line or emit a blank line.
- Do not use `Console.WriteLineInterpolated($"")` or payloads like `$"{ConsoleColor.Default}"` just to force a newline.
- Use `WriteLineInterpolated(...)` when you are actually writing content and also want the trailing newline.

## 4. Old -> New Migration Table

- `IndeterminateProgressBar` -> `Spinner`
Expand All @@ -88,7 +101,8 @@ Use these only when intentionally bypassing the interpolated handler for a custo

```csharp
Console.WriteInterpolated($"[{ConsoleColor.Cyan}info{ConsoleColor.Default}] {message}");
Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Yellow}warn{ConsoleColor.Default}");
Console.WriteLineInterpolated(OutputPipe.Error, $"{ConsoleColor.Red}error{ConsoleColor.Default}: {message}");
Console.NewLine();
```

### Typed input
Expand Down Expand Up @@ -167,9 +181,14 @@ Console.Overwrite(() => {
Console.ClearNextLines(2, OutputPipe.Error);
```

`Spinner.RunAsync(...)`, `ProgressBar.Update(...)`, and overwrite-based regions do not clean up the final area for you. Choose one of these explicitly after the last frame:

- `Console.ClearNextLines(totalLines, pipe)` to remove the live UI
- `Console.SkipLines(totalLines)` to keep the final rendered rows and continue below them

### High-frequency concurrent status updates

Use one reader task to own all `Console.Overwrite(...)` calls and let concurrent workers publish snapshots through a bounded channel:
Use one reader task to own all `Console.Overwrite(...)` calls and let concurrent workers publish snapshots through a bounded channel only when multiple producers need to update the same live region at high frequency:

```csharp
using System.Threading.Channels;
Expand Down Expand Up @@ -200,10 +219,14 @@ Why this works:
- capacity `1` + `DropWrite` avoids backpressure on workers during high-frequency updates
- this pattern is best when dropped intermediate states are acceptable and only recent snapshots matter

For single-producer or modest-rate updates, prefer a simpler render loop without the channel.

## 6. Performance Checklist

- Prefer interpolated handlers over string concatenation.
- Treat span/formattable `Write`/`WriteLine` overloads as advanced escape hatches, not default app-level APIs.
- Use `Console.NewLine(pipe)` for bare line breaks instead of empty/reset-only `WriteLineInterpolated(...)` calls.
- Keep ANSI/decorations in interpolation holes, not raw literal spans.
- Use `OutputPipe.Error` for transient rendering.
- Use `OutputPipe.Error` for transient rendering and genuine errors/diagnostics, but keep ordinary non-error interaction flow on `OutputPipe.Out`.
- Clean up live UI explicitly after the last frame with `ClearNextLines(...)` or keep it intentionally with `SkipLines(...)`.
- Avoid introducing wrapper abstractions when direct PrettyConsole APIs already solve the task.
4 changes: 3 additions & 1 deletion PrettyConsole/PrettyConsole.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<PackageProjectUrl>https://github.com/dusrdev/PrettyConsole/</PackageProjectUrl>
<RepositoryUrl>https://github.com/dusrdev/PrettyConsole/</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Version>5.4.1</Version>
<Version>5.4.2</Version>
<Nullable>enable</Nullable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
Expand All @@ -43,6 +43,8 @@

<PropertyGroup>
<PackageReleaseNotes>
- Improve perf of ReadOnlySpan based overloads of Write and WriteLine.
- Skill improvements
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
4 changes: 2 additions & 2 deletions PrettyConsole/WriteExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public static void Write<T>(T item, OutputPipe pipe, ConsoleColor foreground,
/// <param name="span"></param>
/// <param name="pipe">The output pipe to use</param>
public static void Write(ReadOnlySpan<char> span, OutputPipe pipe) {
Write(span, pipe, ConsoleColor.DefaultForeground, ConsoleColor.DefaultBackground);
ConsoleContext.GetPipeTarget(pipe).Write(span);
}

/// <summary>
Expand All @@ -136,7 +136,7 @@ public static void Write(ReadOnlySpan<char> span, OutputPipe pipe, ConsoleColor
/// <param name="background">background color</param>
public static void Write(ReadOnlySpan<char> span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) {
Console.SetColors(foreground, background);
ConsoleContext.GetPipeTarget(pipe).Write(span);
Write(span, pipe);
Console.ResetColor();
}
}
Expand Down
7 changes: 4 additions & 3 deletions PrettyConsole/WriteLineExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public static void WriteLine<T>(T item, OutputPipe pipe, ConsoleColor foreground
/// <param name="span"></param>
/// <param name="pipe">The output pipe to use</param>
public static void WriteLine(ReadOnlySpan<char> span, OutputPipe pipe) {
Console.WriteLine(span, pipe, ConsoleColor.DefaultForeground, ConsoleColor.DefaultBackground);
ConsoleContext.GetPipeTarget(pipe).WriteLine(span);
}

/// <summary>
Expand All @@ -121,8 +121,9 @@ public static void WriteLine(ReadOnlySpan<char> span, OutputPipe pipe, ConsoleCo
/// <param name="foreground">foreground color</param>
/// <param name="background">background color</param>
public static void WriteLine(ReadOnlySpan<char> span, OutputPipe pipe, ConsoleColor foreground, ConsoleColor background) {
Console.Write(span, pipe, foreground, background);
Console.NewLine(pipe);
Console.SetColors(foreground, background);
WriteLine(span, pipe);
Console.ResetColor();
}
}
}
Loading
Loading