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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Named workspaces hold **up to four independent agents each**. Keyboard shortcuts

## A new way to work in the terminal — v0.16.0

**Patch v0.16.1:** fixes a startup race around prompt activation, keeps prompt state updates on the UI dispatcher, and includes clipboard and component lifecycle improvements. See the [release notes](docs/CHANGELOG.md#0161---2026-10-21).

Built on **RazorConsole 0.6.0**, this release brings multiple workspaces, up to four independent agents per workspace, agent collaboration, portable context snapshots, and saved-agent history into one terminal interface. Explore your files, manage MCP servers and Skills, and keep working while other agents handle their tasks.

The conversation scrolls above a persistent multiline prompt, with searchable pickers and collapsible tool output. See the feature demos below or read the [changelog](docs/CHANGELOG.md) for the full release details.
Expand Down
6 changes: 6 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to MandoCode will be documented in this file.

## [Unreleased]

## [0.16.1] - 2026-10-21

**Startup and component stability.** The chat prompt now waits for model validation and MCP initialization to finish. Prompt state changes run together on the UI dispatcher, and agents closed during startup cannot start an input loop afterward. RazorConsole remains at 0.6.0.

### Changed

- Improve readability of integration menu navigation, delegation completion, component wiring, and Razor parameter lists; retain intent-focused comments and update outdated engine explanations.
Expand All @@ -20,6 +24,8 @@ All notable changes to MandoCode will be documented in this file.

### Fixed

- Keep the chat input loop closed until model validation and MCP startup finish, even when connection-status updates trigger another render. Apply prompt state changes on the renderer dispatcher and avoid starting input after an agent closes during the startup delay.

- Make clipboard-reader overrides work across platforms while retaining native terminal paste on Linux when only the Windows clipboard reader is available.
- Isolate clipboard-command test output from process stdout and verify scoped terminal-control sinks across concurrent agents.
- Stabilize CI layout checks under forced ANSI output, use native paths in checkpoint tests, and restore the process working directory after testing bare `cd` commands.
Expand Down
36 changes: 36 additions & 0 deletions docs/releases/0.16.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# MandoCode CLI 0.16.1 release preparation

Prepared October 21, 2026. This is a CLI patch release; Desktop versioning and RazorConsole.Core 0.6.0 are unchanged. No tag, GitHub release, or NuGet publication has been created.

## Release summary

The chat input loop now waits for model validation and MCP initialization to finish, including when startup status updates trigger another render. Prompt state changes run together on the UI dispatcher. Closing an agent during the startup delay prevents its input loop from starting afterward.

This release also includes the clipboard fixes, component lifecycle refactoring, and test infrastructure improvements listed in the [changelog](../CHANGELOG.md#0161---2026-10-21).

The startup race is covered by a regression that fails with the previous gate. The exact intermittent repeated-border screen has not been reproduced automatically; successful interactive retesting is encouraging but does not establish that every rendering failure is resolved.

## Completed validation

- Release builds and complete test suites on Windows: 1,247 passing tests on net8.0 and 1,247 on net10.0.
- Startup lifecycle checks: 25 fresh test processes per framework, three tests per process, all passing.
- Broader component checks: three fresh test processes per framework, 234 tests per process, all passing.
- Local `MandoCode.0.16.1.nupkg` created and checked for version 0.16.1 and both runtime payloads.
- Package installed as a .NET tool into an isolated directory using only the local package source. The installed executable's `--doctor` command passed against the available local Ollama server.
- Whitespace validation passed. Local debugging settings are excluded from the release changes.

Local artifacts are under `artifacts/packages/`, `artifacts/release-validation/`, `artifacts/startup-stability/`, and `artifacts/component-stability/`. These ignored files are not committed.

## Remaining release checks

- Run the existing GitHub Actions matrix on Windows and Linux for both frameworks after pushing the release branch. Local Windows results do not establish Linux compatibility.
- Complete the [interactive terminal checklist](../testing.md#interactive-terminal-smoke-checklist), including 10–20 launches each from Visual Studio and Windows Terminal. Type immediately, use Alt+N/Alt+W, resize, and exercise model selection with slow/unavailable Ollama and MCP startup. Record the terminal, framework, model, and outcome.
- Review and merge the release changes, then create the v0.16.1 tag/release and publish the package as a separate release action.

To retest the isolated installed package from the repository root:

```powershell
./artifacts/tool-smoke/v0.16.1/mandocode.exe
```

Repeatable startup and component commands are documented in [testing.md](../testing.md#stability-and-targeted-mutation-checks).
9 changes: 9 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ Run repeated component checks locally:

Each iteration runs in a new test process and preserves a separate TRX result under ignored `artifacts/`. A failure stops the script; hang diagnostics bound stalled runs. Passing repeated runs reduces uncertainty but does not prove the absence of intermittent failures.

For startup release checks, focus repetitions on the startup lifecycle regressions:

```powershell
./scripts/verify-component-stability.ps1 -Framework net10.0 -Iterations 25 -TestFilter "FullyQualifiedName~StartupRenderingTests" -ResultsDirectory artifacts/startup-stability/net10.0
./scripts/verify-component-stability.ps1 -Framework net8.0 -Iterations 25 -TestFilter "FullyQualifiedName~StartupRenderingTests" -ResultsDirectory artifacts/startup-stability/net8.0
```

These tests cover renders during incomplete initialization, closing an agent during the startup delay, and retaining one prompt/input owner across refreshes. They do not reproduce every Windows Terminal or Visual Studio launch condition. Before release, also perform 10–20 cold launches from each, type immediately, and create/close agents with Alt+N/Alt+W. Include a slow or unavailable Ollama endpoint and enabled MCP servers, and check narrow/wide windows. Record any duplicated borders, missing focus, or failed submission rather than treating the new-agent workaround as a pass.

The focused mutation probe requires Python and .NET:

```powershell
Expand Down
5 changes: 3 additions & 2 deletions scripts/verify-component-stability.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
param(
[ValidateRange(1, 100)] [int] $Iterations = 5,
[ValidateSet("net10.0", "net8.0")] [string] $Framework = "net10.0",
[string] $ResultsDirectory = ""
[string] $ResultsDirectory = "",
[string] $TestFilter = "Category=Component"
)
$ErrorActionPreference = "Stop"
$repository = Split-Path $PSScriptRoot -Parent
Expand All @@ -11,7 +12,7 @@ if (!$ResultsDirectory) { $ResultsDirectory = Join-Path $repository "artifacts/c
if ($LASTEXITCODE -ne 0) { throw "Test build failed ($LASTEXITCODE)." }
for ($iteration = 1; $iteration -le $Iterations; $iteration++) {
Write-Host "Component stability: $Framework run $iteration of $Iterations"
& dotnet test $project -c Release -f $Framework --no-build --filter "Category=Component" `
& dotnet test $project -c Release -f $Framework --no-build --filter $TestFilter `
--results-directory $ResultsDirectory --logger "trx;LogFileName=component-$iteration.trx" `
--blame-hang-timeout 2m --verbosity quiet
if ($LASTEXITCODE -ne 0) { throw "Component run $iteration failed ($LASTEXITCODE). See $ResultsDirectory." }
Expand Down
10 changes: 8 additions & 2 deletions src/MandoCode/Components/App.Lifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ await InvokeAsync(() => conversation = _messages
return conversation;
}

private bool _startupComplete;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
using var output = Pane is null ? null : TuiConsole.Enter(Pane.Session);
Expand All @@ -208,9 +210,12 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
if (firstRender && !_connectionChecked)
{
await InitializeConnectionAsync();
_startupComplete = true;
}

if (_connectionChecked && !_hasRendered)
// Connection status becomes available before model validation and MCP finish.
// Those steps can render again; only the completed startup may claim input.
if (_startupComplete && _connectionChecked && !_hasRendered && !_disposed && !_paneStopped)
{
_hasRendered = true;
RestoreAgentHistory();
Expand All @@ -232,6 +237,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
// Component input owns the keyboard; the imperative adapter is compatibility-only.
// Small delay to ensure UI is fully rendered before accepting input
await Task.Delay(100);
if (_disposed || _paneStopped) return;
_ = Task.Run(async () => await RunInteractiveLoopAsync());
}
}
Expand Down Expand Up @@ -288,4 +294,4 @@ public async ValueTask DisposeAsync()
Dispose();
await Pane.DisposeAsync();
}
}
}
25 changes: 16 additions & 9 deletions src/MandoCode/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -1875,18 +1875,25 @@
// of the interactive loop — blocking Spectre prompts, guarded renders, Spinner.Stop's
// task.Wait) would resume inline ON the dispatcher and freeze the UI. Mirrors
// InstructionPromptCoordinator, which already uses this for the same reason.
_inputTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
_promptInitialValue = "";
_showPrompt = true;
_isProcessing = false;
await InvokeAsync(StateHasChanged);
var inputCompletion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
await InvokeAsync(() =>
{
_inputTcs = inputCompletion;
_promptInitialValue = "";
_showPrompt = true;
_isProcessing = false;
StateHasChanged();
});

var input = await _inputTcs.Task;
var input = await inputCompletion.Task;
if (_paneStopped) return string.Empty;

_showPrompt = true;
_isProcessing = true;
await InvokeAsync(StateHasChanged);
await InvokeAsync(() =>
{
_showPrompt = true;
_isProcessing = true;
StateHasChanged();
});

return input;
}
Expand Down
4 changes: 2 additions & 2 deletions src/MandoCode/MandoCode.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
<PackageId>MandoCode</PackageId>

<!-- NuGet metadata -->
<Version>0.16.0</Version>
<Version>0.16.1</Version>
<Authors>Armando Fernandez (DevMando)</Authors>
<Description>An AI coding assistant built for the developer workflow: independent agents, workspaces, portable context, and local or cloud models through Ollama.</Description>
<PackageProjectUrl>https://github.com/DevMando/MandoCode</PackageProjectUrl>
<RepositoryUrl>https://github.com/DevMando/MandoCode</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>ai;cli;coding-assistant;ollama;agent-framework;razorconsole;multi-agent;local-llm;dotnet10;dotnet8</PackageTags>
<PackageReleaseNotes>v0.16.0 introduces a RazorConsole 0.6.0 terminal layout, multiple agents and workspaces, agent collaboration, context snapshots, history, MCP and Skills managers, vision attachments, and HTML transcript export. Full release notes: https://github.com/DevMando/MandoCode/blob/v0.16.0/docs/CHANGELOG.md</PackageReleaseNotes>
<PackageReleaseNotes>v0.16.1 fixes a startup race that could activate the chat prompt before model and MCP initialization finished, keeps prompt updates on the UI dispatcher, and prevents closed agents from starting input. Includes clipboard fixes, component lifecycle refactoring, and stronger regression testing. Full release notes: https://github.com/DevMando/MandoCode/blob/v0.16.1/docs/CHANGELOG.md</PackageReleaseNotes>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>MC.png</PackageIcon>
<ApplicationIcon>MC.ico</ApplicationIcon>
Expand Down
126 changes: 126 additions & 0 deletions tests/MandoCode.Tests/StartupRenderingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using System.Reflection;
using MandoCode.Components;
using MandoCode.Models;
using MandoCode.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RazorConsole.Core;
using RazorConsole.Core.Rendering;
using Xunit;

namespace MandoCode.Tests;

[Collection("TUI console routing")]
[Trait("Category", "Component")]
public sealed class StartupRenderingTests
{
[Fact]
public async Task ConnectionStatusRender_DoesNotStartInputBeforeStartupFinishes()
{
await using var services = Services();
StartupApp app = null!;
await using var renderer = await Mount(services, value => app = value);
await renderer.Dispatcher.InvokeAsync(async () =>
{
Set(app, "_connectionChecked", true);
await app.AfterRender();
Assert.False(Get<bool>(app, "_hasRendered"));
Assert.Null(Get<object?>(app, "_inputTcs"));
Assert.False(Get<bool>(app, "_showPrompt"));
});
}

[Fact]
public async Task ClosingDuringStartupDelay_DoesNotLaunchInputOnDisposedAgent()
{
await using var services = Services();
StartupApp app = null!;
await using var renderer = await Mount(services, value => app = value);
await renderer.Dispatcher.InvokeAsync(async () =>
{
Set(app, "_connectionChecked", true);
Set(app, "_startupComplete", true);
var startup = app.AfterRender();
app.Dispose();
await startup;
Assert.Null(Get<object?>(app, "_inputTcs"));
});
}

[Fact]
public async Task StartupAndRefreshes_KeepOnePromptAndInputOwner()
{
await using var services = Services();
StartupApp app = null!;
await using var renderer = await Mount(services, value => app = value);
await renderer.Dispatcher.InvokeAsync(async () =>
{
Set(app, "_connectionChecked", true);
Set(app, "_startupComplete", true);
await app.AfterRender();
});
await renderer.WaitUntilAsync(() => Get<object?>(app, "_inputTcs") is not null);
var input = Get<object>(app, "_inputTcs");
for (var i = 0; i < 12; i++)
{
await renderer.Dispatcher.InvokeAsync(app.AfterRender);
var snapshot = await renderer.Dispatcher.InvokeAsync(renderer.Snapshot);
var nodes = WidgetRendererHarness.Flatten(WidgetRendererHarness.Root(snapshot));
Assert.Single(nodes, node => node.Attributes.TryGetValue("data-focus-key", out var focusKey)
&& focusKey?.StartsWith("prompt-", StringComparison.Ordinal) == true);
Assert.Same(input, Get<object>(app, "_inputTcs"));
}
}

private static ServiceProvider Services()
{
var registrations = new ServiceCollection().AddLogging();
registrations.AddRazorConsoleServices();
registrations.AddSingleton<ITerminalViewport>(new Viewport());
registrations.AddSingleton<IHostApplicationLifetime, Lifetime>();
Program.RegisterAgentServices(registrations, new MandoCodeConfig
{
EnableThemeCustomization = false, AllowPersistence = false, UseAgentNames = false
}, Path.GetTempPath());
return registrations.BuildServiceProvider();
}

private static Task<WidgetRendererHarness> Mount(ServiceProvider services, Action<StartupApp> ready)
{
services.GetRequiredService<AgentWorkspace>().Add();
RenderFragment<AgentPane> body = _ => builder =>
{
builder.OpenComponent<StartupApp>(0);
builder.AddAttribute(1, nameof(StartupApp.Ready), ready);
builder.CloseComponent();
};
return WidgetRendererHarness.MountAsync<AgentWorkspaceView>(services, new() { ["AgentBody"] = body });
}

private static T Get<T>(App app, string name) => (T)Field(name).GetValue(app)!;
private static void Set(App app, string name, object value) => Field(name).SetValue(app, value);
private static FieldInfo Field(string name) => typeof(App).GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)!;

public sealed class StartupApp : App
{
[Parameter] public Action<StartupApp> Ready { get; set; } = default!;
protected override void OnInitialized() { base.OnInitialized(); Ready(this); }
// Drive startup render reentry deterministically without probing a live model server.
protected override Task OnAfterRenderAsync(bool firstRender) => Task.CompletedTask;
public Task AfterRender() => base.OnAfterRenderAsync(false);
}
private sealed class Viewport : ITerminalViewport
{
public int Width => 120;
public int Height => 40;
public event Action? OnResized { add { } remove { } }
}
private sealed class Lifetime : IHostApplicationLifetime
{
public CancellationToken ApplicationStarted => CancellationToken.None;
public CancellationToken ApplicationStopping => CancellationToken.None;
public CancellationToken ApplicationStopped => CancellationToken.None;
public void StopApplication() { }
}
}
Loading