From c73526292f2a16ca91e757c45f06b78cb459b642 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 11:49:07 -0700 Subject: [PATCH 01/15] Pin the VS extension's System.Text.Json to the VS SDK baseline Visual Studio ships System.Text.Json as a shared assembly and binds every extension to its own copy through a devenv.exe.config with a hard upper bound (VS 18: oldVersion="0.0.0.0-10.0.0.10"). Redirects unify upward only, so when the repo-wide Central Package Management pin rolled to 10.0.12 the extension began requesting AssemblyVersion 10.0.0.12 -- past that ceiling. No redirect applied; the VSIX carries no private copy because VS strips assemblies it provides, and the package registers no BindingPath, so the CLR probed devenv's base directory, found nothing, and threw: FileNotFoundException: Could not load file or assembly 'System.Text.Json, Version=10.0.0.12, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' That aborted every preview session at EmbedClient.StatusAsync, the first call a session makes. The build stayed green throughout -- nothing at compile time sees the ceiling. Pin System.Text.Json with VersionOverride="9.0.0" (the version Microsoft.VisualStudio.SDK 17.14 itself depends on) instead of following the repo-wide version. Every VS release that satisfies the SDK redirects at least that high, and building low is always safe because redirects unify upward. Add VsSharedAssemblyBindingTests, which reads the compiled assembly references and fails if any VS shared assembly exceeds the ceiling again. Verified that all 13 of the extension's references now fall inside VS 18's declared redirect ranges. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 17 ++++ .../Reactor.VsExtension.csproj | 13 ++- .../Reactor.VsExtension.Tests.csproj | 4 +- .../VsSharedAssemblyBindingTests.cs | 87 +++++++++++++++++++ 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fb270f41..4fd59b420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,23 @@ Conventions for contributors: ### Fixed +- **The Visual Studio preview failed every session with + `FileNotFoundException: Could not load file or assembly 'System.Text.Json, + Version=10.0.0.12'`.** Visual Studio ships `System.Text.Json` as a + *shared assembly* and binds extensions to its own copy through a + `devenv.exe.config` `` with a hard upper bound (VS 18: + `oldVersion="0.0.0.0-10.0.0.10"`). Redirects unify *upward* only, so when the + repo-wide Central Package Management pin rolled to `10.0.12` the extension began + requesting an assembly version *past* that ceiling: no redirect applied, the VSIX + carries no private copy (VS strips assemblies it provides) and the package + registers no `BindingPath`, so the CLR found nothing and threw on the first JSON + call — `EmbedClient.StatusAsync`, i.e. the very first thing every preview session + does. The extension now pins `System.Text.Json` to the `Microsoft.VisualStudio.SDK` + baseline (`9.0.0`) with a `VersionOverride` rather than following the repo-wide + version, and a new `VsSharedAssemblyBindingTests` gate reads the compiled + assembly references and fails the build if any VS shared assembly exceeds the + ceiling again. + - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested `ManipulationDelta` shape rather than Reactor's flat gesture structs, and the diff --git a/src/vs-reactor/Reactor.VsExtension/Reactor.VsExtension.csproj b/src/vs-reactor/Reactor.VsExtension/Reactor.VsExtension.csproj index b61c51085..eb23be428 100644 --- a/src/vs-reactor/Reactor.VsExtension/Reactor.VsExtension.csproj +++ b/src/vs-reactor/Reactor.VsExtension/Reactor.VsExtension.csproj @@ -53,7 +53,18 @@ - + + diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/Reactor.VsExtension.Tests.csproj b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/Reactor.VsExtension.Tests.csproj index ce1cd9dca..12e98cc56 100644 --- a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/Reactor.VsExtension.Tests.csproj +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/Reactor.VsExtension.Tests.csproj @@ -16,7 +16,9 @@ - + + diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs new file mode 100644 index 000000000..f5f32557d --- /dev/null +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs @@ -0,0 +1,87 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.UI.Reactor.VsExtension.Embed; +using Xunit; + +namespace Reactor.VsExtension.Tests +{ + /// + /// Guards the assembly-version ceiling on framework libraries that Visual Studio ships as + /// shared assemblies and binds on the extension's behalf. + /// + public sealed class VsSharedAssemblyBindingTests + { + // Visual Studio owns the identity of these assemblies inside devenv.exe: it ships one + // copy under Common7\IDE\SharedAssemblies and binds every extension to it through a + // devenv.exe.config . VS 18 declares: + // + // + // + // + // A redirect unifies UPWARD, and only within its oldVersion range. Compile the + // extension against a *higher* assembly version and the request falls outside the + // range, so no redirect applies; the VSIX carries no private copy (VS strips + // assemblies it provides) and the package registers no BindingPath, so the CLR probes + // devenv's base directory, finds nothing, and throws: + // + // FileNotFoundException: Could not load file or assembly + // 'System.Text.Json, Version=10.0.0.12, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' + // + // That is not hypothetical: the repo-wide CPM pin (System.Text.Json 10.0.12 -> + // AssemblyVersion 10.0.0.12) flowed into the extension and landed two servicing + // revisions past VS 18's ceiling, killing every preview session at the first JSON call. + // + // The safe ceiling is the version Microsoft.VisualStudio.SDK itself depends on: every + // VS release that satisfies the SDK redirects at least that high, and building lower is + // always safe because redirects unify upward. Bump these only together with the + // Microsoft.VisualStudio.SDK PackageVersion in Directory.Packages.props. + private static readonly IReadOnlyDictionary Ceilings = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["System.Text.Json"] = new Version(9, 0, 0, 0), + ["System.Text.Encodings.Web"] = new Version(9, 0, 0, 0), + }; + + private static AssemblyName[] ExtensionReferences() + { + return typeof(EmbedClient).Assembly.GetReferencedAssemblies(); + } + + /// + /// Positive control for : + /// a clean result there is only meaningful if the probe can actually see the reference + /// it gates. If the extension legitimately stops using System.Text.Json, delete this + /// whole class rather than relaxing this assertion. + /// + [Fact] + public void ExtensionReferences_IncludeAGatedSharedAssembly() + { + var names = ExtensionReferences().Select(r => r.Name).ToArray(); + + Assert.Contains("System.Text.Json", names); + } + + [Fact] + public void SharedAssemblyReferences_StayWithinVsBindingRedirectCeiling() + { + var violations = ExtensionReferences() + .Where(r => r.Name != null && r.Version != null) + .Where(r => Ceilings.TryGetValue(r.Name!, out var ceiling) && r.Version! > ceiling) + .Select(r => $"{r.Name} {r.Version} (ceiling {Ceilings[r.Name!]})") + .ToArray(); + + Assert.True( + violations.Length == 0, + "Reactor.VsExtension references a Visual Studio shared assembly at a higher version than " + + "VS's binding redirect covers. This does not fail the build — it fails at run time " + + "inside devenv with FileNotFoundException on the first call that touches the assembly. " + + "Pin it with a VersionOverride in Reactor.VsExtension.csproj instead of following the " + + "repo-wide Directory.Packages.props version. Offending references: " + + string.Join(", ", violations)); + } + } +} From 33db65187a9557bf8f2e08d6315bd01435c83ef7 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 12:29:34 -0700 Subject: [PATCH 02/15] Diagnose packaged (MSIX) projects in the VS preview instead of failing opaquely The preview starts the target with `dotnet watch run`, which launches the built .exe directly. That works for the unpackaged default but leaves a packaged project dead two different ways, and the extension reported neither. Package identity: a bare .exe launch has no package graph, so the Windows App SDK deployment auto-initializer cannot activate its WinRT types and the process dies in .cctor() with COMException (0x80040154) REGDB_E_CLASSNOTREG before any Reactor code runs. Microsoft.Windows.SDK.BuildTools.WinApp fixes this by overriding ComputeRunArguments so RunCommand points at a launcher that registers a debug identity. Inherited stdout: that launcher defaults to AUMID activation, which is brokered and has no stdout at all. The devtools handshake reports CAPTURE_PORT on stdout, so the app would run while the preview could never attach. Setting WinAppRunUseExecutionAlias=true makes the launcher use a generated execution alias, which inherits stdout. Verified end to end against the exact command the extension issues: the packaged app completes the full handshake, emitting CAPTURE_PORT / CAPTURE_TOKEN and devtools-ready, and both servers answer over HTTP. Either change alone leaves the preview broken, so the handshake-timeout path now recognises the stderr signature and answers with both, replacing a generic list of guesses. The matcher requires a CLASSNOTREG marker AND a Windows App SDK frame: REGDB_E_CLASSNOTREG on its own is a generic COM error, and matching it alone would mislabel unrelated failures. A test asserts exactly that, and mutation-checking it (dropping the second half of the signature) reddens it. The VS extension guide gains a "Packaged (MSIX) projects" section, edited in the pipeline template and recompiled. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 14 +++ docs/_pipeline/templates/vs-extension.md.dt | 31 +++++++ docs/guide/vs-extension.md | 31 +++++++ .../Session/ChildFailureDiagnostics.cs | 85 +++++++++++++++++++ .../Session/EmbedSession.cs | 28 ++++-- .../ChildFailureDiagnosticsTests.cs | 81 ++++++++++++++++++ 6 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 src/vs-reactor/Reactor.VsExtension/Session/ChildFailureDiagnostics.cs create mode 100644 src/vs-reactor/Tests/Reactor.VsExtension.Tests/ChildFailureDiagnosticsTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd59b420..52d1a7fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,20 @@ Conventions for contributors: assembly references and fails the build if any VS shared assembly exceeds the ceiling again. +- **The VS preview failed opaquely on packaged (MSIX) projects.** The preview starts the + target with `dotnet watch run`, which launches the built `.exe` directly. A packaged + project needs package identity for the Windows App SDK deployment initializer — without + it the app died at `.cctor()` with `COMException (0x80040154): Class not registered + (REGDB_E_CLASSNOTREG)` before any Reactor code ran — and it needs an inherited stdout, + because the devtools handshake reports `CAPTURE_PORT` there and the default AUMID + activation is brokered with no stdout at all. Either gap alone leaves the preview dead. + The extension now recognises that stderr signature and answers with the two project + changes that fix it (`Microsoft.Windows.SDK.BuildTools.WinApp` plus + `WinAppRunUseExecutionAlias`) instead of a generic list of guesses, and the + [VS extension guide](docs/guide/vs-extension.md) documents the packaged setup. The + matcher requires both halves of the signature, so an unrelated `REGDB_E_CLASSNOTREG` is + not mislabelled as a packaging problem. + - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested `ManipulationDelta` shape rather than Reactor's flat gesture structs, and the diff --git a/docs/_pipeline/templates/vs-extension.md.dt b/docs/_pipeline/templates/vs-extension.md.dt index d33b86393..f2a3adc73 100644 --- a/docs/_pipeline/templates/vs-extension.md.dt +++ b/docs/_pipeline/templates/vs-extension.md.dt @@ -35,6 +35,37 @@ The component picker is a ComboBox in the tool window chrome. By default it auto - **L1 hot reload** — edits inside a `Render` body usually apply in place. The embedded UI refreshes without recreating the placeholder HWND. - **L2 respawn** — rude edits such as record/type shape changes cause `dotnet watch` to rebuild and restart the child. The extension detects the new port/token pair and re-embeds the new child into the same placeholder, normally within about 10 seconds. +## Packaged (MSIX) projects + +The preview starts your app with `dotnet watch run`, which launches the built `.exe` +directly. That is fine for the unpackaged default, but a packaged project — one where +`WindowsPackageType` is `MSIX` rather than `None` — needs two extra things, because a +bare `.exe` launch provides neither. + +```xml + + true + + + + +``` + +**`Microsoft.Windows.SDK.BuildTools.WinApp` supplies package identity.** It overrides the +SDK's `ComputeRunArguments`, pointing `RunCommand` at a launcher that registers a debug +identity and activates the app as a package. Without it the app starts with no package +graph, so the Windows App SDK deployment initializer cannot resolve its WinRT types and +the process dies before any Reactor code runs, with +`COMException (0x80040154): Class not registered (REGDB_E_CLASSNOTREG)`. + +**`WinAppRunUseExecutionAlias` supplies stdout.** It makes that launcher activate the app +through a generated execution alias instead of its default AUMID activation. AUMID +activation is brokered and has no stdout at all, and the devtools handshake reports +`CAPTURE_PORT` on stdout — so without this the app runs but the preview can never attach. + +Either one alone leaves the preview broken. To preview unpackaged instead, set +`None`. + ## Known limitations - WinAppSDK 1.6 or newer is required by the target Reactor app. diff --git a/docs/guide/vs-extension.md b/docs/guide/vs-extension.md index 605d8c540..8dc0f5983 100644 --- a/docs/guide/vs-extension.md +++ b/docs/guide/vs-extension.md @@ -26,6 +26,37 @@ The component picker is a ComboBox in the tool window chrome. By default it auto - **L1 hot reload** — edits inside a `Render` body usually apply in place. The embedded UI refreshes without recreating the placeholder HWND. - **L2 respawn** — rude edits such as record/type shape changes cause `dotnet watch` to rebuild and restart the child. The extension detects the new port/token pair and re-embeds the new child into the same placeholder, normally within about 10 seconds. +## Packaged (MSIX) projects + +The preview starts your app with `dotnet watch run`, which launches the built `.exe` +directly. That is fine for the unpackaged default, but a packaged project — one where +`WindowsPackageType` is `MSIX` rather than `None` — needs two extra things, because a +bare `.exe` launch provides neither. + +```xml + + true + + + + +``` + +**`Microsoft.Windows.SDK.BuildTools.WinApp` supplies package identity.** It overrides the +SDK's `ComputeRunArguments`, pointing `RunCommand` at a launcher that registers a debug +identity and activates the app as a package. Without it the app starts with no package +graph, so the Windows App SDK deployment initializer cannot resolve its WinRT types and +the process dies before any Reactor code runs, with +`COMException (0x80040154): Class not registered (REGDB_E_CLASSNOTREG)`. + +**`WinAppRunUseExecutionAlias` supplies stdout.** It makes that launcher activate the app +through a generated execution alias instead of its default AUMID activation. AUMID +activation is brokered and has no stdout at all, and the devtools handshake reports +`CAPTURE_PORT` on stdout — so without this the app runs but the preview can never attach. + +Either one alone leaves the preview broken. To preview unpackaged instead, set +`None`. + ## Known limitations - WinAppSDK 1.6 or newer is required by the target Reactor app. diff --git a/src/vs-reactor/Reactor.VsExtension/Session/ChildFailureDiagnostics.cs b/src/vs-reactor/Reactor.VsExtension/Session/ChildFailureDiagnostics.cs new file mode 100644 index 000000000..576073dbf --- /dev/null +++ b/src/vs-reactor/Reactor.VsExtension/Session/ChildFailureDiagnostics.cs @@ -0,0 +1,85 @@ +#nullable enable + +using System; + +namespace Microsoft.UI.Reactor.VsExtension.Session +{ + /// + /// Translates known child-process failure signatures into actionable guidance. + /// Pure string analysis so it is unit-testable without a VS shell or a live child. + /// + internal static class ChildFailureDiagnostics + { + /// + /// Returns guidance for a recognised failure in , or + /// when nothing specific is recognised (the caller then falls + /// back to its generic cause list). + /// + public static string? Describe(string? stderr) + { + if (string.IsNullOrWhiteSpace(stderr)) + { + return null; + } + + return IsPackagedWithoutIdentity(stderr!) ? PackagedWithoutIdentityGuidance : null; + } + + /// + /// A packaged (MSIX) project launched by dotnet watch run starts the built + /// .exe directly, with no package identity. The Windows App SDK's deployment + /// auto-initializer then fails to activate its WinRT types, because those live in the + /// Windows App Runtime framework package and are resolved through the process's + /// package graph — which an identity-less process does not have. + /// + /// + /// Both halves of the signature are required. REGDB_E_CLASSNOTREG alone is a + /// generic COM error that any missing registration produces, so matching on it by + /// itself would mislabel unrelated failures. + /// + private static bool IsPackagedWithoutIdentity(string stderr) + { + var classNotRegistered = + Contains(stderr, "REGDB_E_CLASSNOTREG") || + Contains(stderr, "0x80040154"); + + if (!classNotRegistered) + { + return false; + } + + return Contains(stderr, "WindowsAppRuntime") || + Contains(stderr, "DeploymentManager") || + Contains(stderr, "Microsoft.Windows.ApplicationModel"); + } + + private static bool Contains(string haystack, string needle) + { + return haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; + } + + internal const string PackagedWithoutIdentityGuidance = + "This looks like a packaged (MSIX) project that was launched without package identity.\r\n" + + "\r\n" + + "The preview runs your app with `dotnet watch run`, which starts the built .exe directly.\r\n" + + "A packaged app needs two things that a bare .exe launch does not provide: MSIX identity,\r\n" + + "so the Windows App SDK deployment initializer can resolve its WinRT types, and an\r\n" + + "inherited stdout, which is how the devtools handshake reports CAPTURE_PORT.\r\n" + + "\r\n" + + "Add both of these to the project and preview again:\r\n" + + "\r\n" + + " \r\n" + + " true\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + "\r\n" + + "The package gives `dotnet watch run` a launcher that registers a debug identity.\r\n" + + "WinAppRunUseExecutionAlias makes that launcher use an execution alias, which inherits\r\n" + + "stdout; the default AUMID activation is brokered and has no stdout at all, so the\r\n" + + "handshake could never be read even though the app itself would run.\r\n" + + "\r\n" + + "To preview the app unpackaged instead, set None."; + } +} diff --git a/src/vs-reactor/Reactor.VsExtension/Session/EmbedSession.cs b/src/vs-reactor/Reactor.VsExtension/Session/EmbedSession.cs index a71b27db9..8e8c330c8 100644 --- a/src/vs-reactor/Reactor.VsExtension/Session/EmbedSession.cs +++ b/src/vs-reactor/Reactor.VsExtension/Session/EmbedSession.cs @@ -425,14 +425,26 @@ private async Task StartCoreAsync(string? componentName, CancellationToken ct, b detail.Append(((int)_firstSessionTimeout.TotalSeconds).ToString(System.Globalization.CultureInfo.InvariantCulture)); detail.AppendLine("s."); detail.AppendLine(); - detail.AppendLine("Common causes:"); - detail.AppendLine(" • First `dotnet watch` build is slow on a cold disk cache. The session will"); - detail.AppendLine(" automatically recover if the child eventually emits CAPTURE_PORT — watch"); - detail.AppendLine(" the Output pane. Set Reactor_VsExtension_HandshakeTimeoutSeconds=300"); - detail.AppendLine(" in the VS environment to extend this timeout."); - detail.AppendLine(" • Target project does not reference Microsoft.UI.Reactor.Devtools."); - detail.AppendLine(" • Target project is missing "); - detail.AppendLine(" • MSBuild really did fail — see the stderr below."); + + // A recognised failure signature is far more useful than the generic list, + // so lead with it and skip the guesses entirely. + var diagnosis = ChildFailureDiagnostics.Describe(stderr); + if (diagnosis != null) + { + detail.AppendLine(diagnosis); + } + else + { + detail.AppendLine("Common causes:"); + detail.AppendLine(" • First `dotnet watch` build is slow on a cold disk cache. The session will"); + detail.AppendLine(" automatically recover if the child eventually emits CAPTURE_PORT — watch"); + detail.AppendLine(" the Output pane. Set Reactor_VsExtension_HandshakeTimeoutSeconds=300"); + detail.AppendLine(" in the VS environment to extend this timeout."); + detail.AppendLine(" • Target project does not reference Microsoft.UI.Reactor.Devtools."); + detail.AppendLine(" • Target project is missing "); + detail.AppendLine(" • MSBuild really did fail — see the stderr below."); + } + if (!string.IsNullOrWhiteSpace(stderr)) { detail.AppendLine(); diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ChildFailureDiagnosticsTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ChildFailureDiagnosticsTests.cs new file mode 100644 index 000000000..64c4bd941 --- /dev/null +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ChildFailureDiagnosticsTests.cs @@ -0,0 +1,81 @@ +#nullable enable + +using Microsoft.UI.Reactor.VsExtension.Session; +using Xunit; + +namespace Reactor.VsExtension.Tests +{ + public sealed class ChildFailureDiagnosticsTests + { + /// + /// Verbatim stderr from a packaged (MSIX) Reactor app launched by `dotnet watch run` + /// without Microsoft.Windows.SDK.BuildTools.WinApp, captured while reproducing the + /// failure. Kept real rather than synthesised so the matcher is exercised against the + /// shape it actually has to recognise. + /// + private const string PackagedWithoutIdentityStderr = + "Unhandled exception. System.Runtime.InteropServices.COMException (0x80040154): Class not registered (0x80040154 (REGDB_E_CLASSNOTREG))\n" + + " at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode)\n" + + " at WinRT.ActivationFactory.Get(String typeName)\n" + + " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentInitializeOptions..ctor()\n" + + " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentManagerCS.AutoInitialize.get_Options()\n" + + " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Common.AutoInitialize.InitializeWindowsAppSDK()\n" + + " at .cctor()"; + + [Fact] + public void Describe_RecognisesPackagedAppLaunchedWithoutIdentity() + { + var result = ChildFailureDiagnostics.Describe(PackagedWithoutIdentityStderr); + + Assert.NotNull(result); + Assert.Equal(ChildFailureDiagnostics.PackagedWithoutIdentityGuidance, result); + } + + [Fact] + public void Guidance_NamesBothRequiredProjectChanges() + { + var result = ChildFailureDiagnostics.Describe(PackagedWithoutIdentityStderr)!; + + // Either one alone leaves the preview broken: the package supplies identity, the + // property supplies the inherited stdout the handshake is read from. + Assert.Contains("Microsoft.Windows.SDK.BuildTools.WinApp", result); + Assert.Contains("WinAppRunUseExecutionAlias", result); + Assert.Contains("WindowsPackageType", result); + } + + [Theory] + // A bare CLASSNOTREG with no Windows App SDK frames is some other missing + // registration; claiming it is a packaging problem would send the user the wrong way. + [InlineData("System.Runtime.InteropServices.COMException (0x80040154): Class not registered (REGDB_E_CLASSNOTREG)\n at Contoso.Widget.Activate()")] + // Windows App SDK frames without CLASSNOTREG are a different failure entirely. + [InlineData("Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentManager threw InvalidOperationException")] + [InlineData("MSBUILD : error MSB1009: Project file does not exist.")] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Describe_ReturnsNullForUnrelatedFailures(string? stderr) + { + Assert.Null(ChildFailureDiagnostics.Describe(stderr)); + } + + [Fact] + public void Describe_MatchesOnSymbolicNameWhenHresultIsAbsent() + { + var stderr = + "COMException: Class not registered (REGDB_E_CLASSNOTREG)\n" + + " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentInitializeOptions..ctor()"; + + Assert.NotNull(ChildFailureDiagnostics.Describe(stderr)); + } + + [Fact] + public void Describe_MatchesOnHresultWhenSymbolicNameIsAbsent() + { + var stderr = + "COMException (0x80040154): Class not registered\n" + + " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentManagerCS.AutoInitialize.get_Options()"; + + Assert.NotNull(ChildFailureDiagnostics.Describe(stderr)); + } + } +} From e3edde1e0ec025ded28abd912a19bebead8874e3 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 13:03:09 -0700 Subject: [PATCH 03/15] Make packaged (MSIX) apps work in the VS preview The Reactor templates generate a packaged app by default, and previewing one did not work. The preview starts the target with `dotnet watch run`. For a packaged project that goes through the launcher Microsoft.Windows.SDK.BuildTools.WinApp installs, which the templates already reference. That launcher activates the app by AUMID, which is brokered and has no stdout, and stdout is how the child reports CAPTURE_PORT / CAPTURE_TOKEN. The app started and rendered while the preview waited out its handshake timeout with nothing in stderr to explain it. The launcher now sets WinAppRunUseExecutionAlias=true in the child environment, which switches that launch to an execution alias that inherits stdout. A freshly generated packaged app now previews with no project changes. Set through the environment rather than -p: for two reasons. `dotnet watch` rejects -p alongside --project as ambiguous, and an environment property is the lowest-precedence MSBuild property, so a project that sets the value explicitly still wins. Verified both: env-only evaluates to true, and an explicit project value of false overrides it. Set unconditionally rather than behind a packaged/unpackaged probe. The WinApp targets only consume it when their run support is active, so it is inert for an unpackaged project, and probing would cost an MSBuild evaluation per launch. Verified against two apps generated from the current template, driven with the exact command the extension issues. Unpackaged control (WindowsPackageType=None, the only edit): full handshake, byte-identical with and without the variable. Packaged default template, unchanged: no handshake before, full handshake after. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 16 +--- docs/_pipeline/templates/vs-extension.md.dt | 32 +------ docs/guide/vs-extension.md | 32 +------ .../Embed/ReactorChildLauncher.cs | 41 +++++++++ .../Session/ChildFailureDiagnostics.cs | 85 ------------------- .../Session/EmbedSession.cs | 28 ++---- .../ChildFailureDiagnosticsTests.cs | 81 ------------------ .../ReactorChildLauncherEnvironmentTests.cs | 71 ++++++++++++++++ 8 files changed, 131 insertions(+), 255 deletions(-) delete mode 100644 src/vs-reactor/Reactor.VsExtension/Session/ChildFailureDiagnostics.cs delete mode 100644 src/vs-reactor/Tests/Reactor.VsExtension.Tests/ChildFailureDiagnosticsTests.cs create mode 100644 src/vs-reactor/Tests/Reactor.VsExtension.Tests/ReactorChildLauncherEnvironmentTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 52d1a7fd5..93f6bc361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,19 +86,9 @@ Conventions for contributors: assembly references and fails the build if any VS shared assembly exceeds the ceiling again. -- **The VS preview failed opaquely on packaged (MSIX) projects.** The preview starts the - target with `dotnet watch run`, which launches the built `.exe` directly. A packaged - project needs package identity for the Windows App SDK deployment initializer — without - it the app died at `.cctor()` with `COMException (0x80040154): Class not registered - (REGDB_E_CLASSNOTREG)` before any Reactor code ran — and it needs an inherited stdout, - because the devtools handshake reports `CAPTURE_PORT` there and the default AUMID - activation is brokered with no stdout at all. Either gap alone leaves the preview dead. - The extension now recognises that stderr signature and answers with the two project - changes that fix it (`Microsoft.Windows.SDK.BuildTools.WinApp` plus - `WinAppRunUseExecutionAlias`) instead of a generic list of guesses, and the - [VS extension guide](docs/guide/vs-extension.md) documents the packaged setup. The - matcher requires both halves of the signature, so an unrelated `REGDB_E_CLASSNOTREG` is - not mislabelled as a packaging problem. +- **Packaged (MSIX) apps now work in the Visual Studio preview.** The Reactor templates + generate a packaged app by default, and previewing one previously failed. It now works + with no project changes. - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested diff --git a/docs/_pipeline/templates/vs-extension.md.dt b/docs/_pipeline/templates/vs-extension.md.dt index f2a3adc73..7777f1b64 100644 --- a/docs/_pipeline/templates/vs-extension.md.dt +++ b/docs/_pipeline/templates/vs-extension.md.dt @@ -37,34 +37,10 @@ The component picker is a ComboBox in the tool window chrome. By default it auto ## Packaged (MSIX) projects -The preview starts your app with `dotnet watch run`, which launches the built `.exe` -directly. That is fine for the unpackaged default, but a packaged project — one where -`WindowsPackageType` is `MSIX` rather than `None` — needs two extra things, because a -bare `.exe` launch provides neither. - -```xml - - true - - - - -``` - -**`Microsoft.Windows.SDK.BuildTools.WinApp` supplies package identity.** It overrides the -SDK's `ComputeRunArguments`, pointing `RunCommand` at a launcher that registers a debug -identity and activates the app as a package. Without it the app starts with no package -graph, so the Windows App SDK deployment initializer cannot resolve its WinRT types and -the process dies before any Reactor code runs, with -`COMException (0x80040154): Class not registered (REGDB_E_CLASSNOTREG)`. - -**`WinAppRunUseExecutionAlias` supplies stdout.** It makes that launcher activate the app -through a generated execution alias instead of its default AUMID activation. AUMID -activation is brokered and has no stdout at all, and the devtools handshake reports -`CAPTURE_PORT` on stdout — so without this the app runs but the preview can never attach. - -Either one alone leaves the preview broken. To preview unpackaged instead, set -`None`. +Packaged projects work with no project changes. The Reactor templates generate a packaged +app by default, and the preview launches it with package identity for you. + +To preview the app unpackaged instead, set `None`. ## Known limitations diff --git a/docs/guide/vs-extension.md b/docs/guide/vs-extension.md index 8dc0f5983..ed85424e4 100644 --- a/docs/guide/vs-extension.md +++ b/docs/guide/vs-extension.md @@ -28,34 +28,10 @@ The component picker is a ComboBox in the tool window chrome. By default it auto ## Packaged (MSIX) projects -The preview starts your app with `dotnet watch run`, which launches the built `.exe` -directly. That is fine for the unpackaged default, but a packaged project — one where -`WindowsPackageType` is `MSIX` rather than `None` — needs two extra things, because a -bare `.exe` launch provides neither. - -```xml - - true - - - - -``` - -**`Microsoft.Windows.SDK.BuildTools.WinApp` supplies package identity.** It overrides the -SDK's `ComputeRunArguments`, pointing `RunCommand` at a launcher that registers a debug -identity and activates the app as a package. Without it the app starts with no package -graph, so the Windows App SDK deployment initializer cannot resolve its WinRT types and -the process dies before any Reactor code runs, with -`COMException (0x80040154): Class not registered (REGDB_E_CLASSNOTREG)`. - -**`WinAppRunUseExecutionAlias` supplies stdout.** It makes that launcher activate the app -through a generated execution alias instead of its default AUMID activation. AUMID -activation is brokered and has no stdout at all, and the devtools handshake reports -`CAPTURE_PORT` on stdout — so without this the app runs but the preview can never attach. - -Either one alone leaves the preview broken. To preview unpackaged instead, set -`None`. +Packaged projects work with no project changes. The Reactor templates generate a packaged +app by default, and the preview launches it with package identity for you. + +To preview the app unpackaged instead, set `None`. ## Known limitations diff --git a/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs b/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs index 4b405a68f..7d7679b05 100644 --- a/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs +++ b/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs @@ -146,9 +146,50 @@ private static ProcessStartInfo CreateStartInfo(StartOptions options) info.EnvironmentVariables["DOTNET_WATCH_RESTART_ON_RUDE_EDIT"] = "1"; info.EnvironmentVariables["DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER"] = "1"; info.EnvironmentVariables["NoDefaultCurrentDirectoryInExePath"] = "1"; + ApplyPackagedRunDefaults(info); return info; } + /// + /// Makes the preview work on packaged (MSIX) projects without any project edit. + /// + /// + /// + /// A packaged project launched by dotnet watch run goes through the launcher + /// that Microsoft.Windows.SDK.BuildTools.WinApp installs (the Reactor project + /// templates reference it by default). That launcher activates the app by AUMID, + /// which is brokered and therefore has no stdout at all — and stdout is how + /// the child reports CAPTURE_PORT / CAPTURE_TOKEN. The app would start + /// and render while the preview silently waited out its handshake timeout, with no + /// error anywhere to explain it. Setting this property switches the launcher to an + /// execution alias, which inherits stdout. + /// + /// + /// Set unconditionally rather than behind a packaged/unpackaged probe: the WinApp + /// targets only consume it when their run support is active, so it is inert for an + /// unpackaged project (verified — an unpackaged launch is byte-identical with and + /// without it), and probing would cost an MSBuild evaluation on every launch. + /// + /// + /// It is passed through the environment, not -p:, for two reasons. + /// dotnet watch rejects -p alongside --project as ambiguous, and + /// an environment property is the lowest-precedence MSBuild property, so a project + /// that explicitly sets <WinAppRunUseExecutionAlias>false</...> + /// still wins and keeps control. + /// + /// + private static void ApplyPackagedRunDefaults(ProcessStartInfo info) + { + info.EnvironmentVariables[WinAppRunUseExecutionAliasVariable] = "true"; + } + + internal const string WinAppRunUseExecutionAliasVariable = "WinAppRunUseExecutionAlias"; + + internal static ProcessStartInfo CreateStartInfoForTest(StartOptions options) + { + return CreateStartInfo(options); + } + private static string BuildArguments(StartOptions options) { var args = new List diff --git a/src/vs-reactor/Reactor.VsExtension/Session/ChildFailureDiagnostics.cs b/src/vs-reactor/Reactor.VsExtension/Session/ChildFailureDiagnostics.cs deleted file mode 100644 index 576073dbf..000000000 --- a/src/vs-reactor/Reactor.VsExtension/Session/ChildFailureDiagnostics.cs +++ /dev/null @@ -1,85 +0,0 @@ -#nullable enable - -using System; - -namespace Microsoft.UI.Reactor.VsExtension.Session -{ - /// - /// Translates known child-process failure signatures into actionable guidance. - /// Pure string analysis so it is unit-testable without a VS shell or a live child. - /// - internal static class ChildFailureDiagnostics - { - /// - /// Returns guidance for a recognised failure in , or - /// when nothing specific is recognised (the caller then falls - /// back to its generic cause list). - /// - public static string? Describe(string? stderr) - { - if (string.IsNullOrWhiteSpace(stderr)) - { - return null; - } - - return IsPackagedWithoutIdentity(stderr!) ? PackagedWithoutIdentityGuidance : null; - } - - /// - /// A packaged (MSIX) project launched by dotnet watch run starts the built - /// .exe directly, with no package identity. The Windows App SDK's deployment - /// auto-initializer then fails to activate its WinRT types, because those live in the - /// Windows App Runtime framework package and are resolved through the process's - /// package graph — which an identity-less process does not have. - /// - /// - /// Both halves of the signature are required. REGDB_E_CLASSNOTREG alone is a - /// generic COM error that any missing registration produces, so matching on it by - /// itself would mislabel unrelated failures. - /// - private static bool IsPackagedWithoutIdentity(string stderr) - { - var classNotRegistered = - Contains(stderr, "REGDB_E_CLASSNOTREG") || - Contains(stderr, "0x80040154"); - - if (!classNotRegistered) - { - return false; - } - - return Contains(stderr, "WindowsAppRuntime") || - Contains(stderr, "DeploymentManager") || - Contains(stderr, "Microsoft.Windows.ApplicationModel"); - } - - private static bool Contains(string haystack, string needle) - { - return haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; - } - - internal const string PackagedWithoutIdentityGuidance = - "This looks like a packaged (MSIX) project that was launched without package identity.\r\n" + - "\r\n" + - "The preview runs your app with `dotnet watch run`, which starts the built .exe directly.\r\n" + - "A packaged app needs two things that a bare .exe launch does not provide: MSIX identity,\r\n" + - "so the Windows App SDK deployment initializer can resolve its WinRT types, and an\r\n" + - "inherited stdout, which is how the devtools handshake reports CAPTURE_PORT.\r\n" + - "\r\n" + - "Add both of these to the project and preview again:\r\n" + - "\r\n" + - " \r\n" + - " true\r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - "\r\n" + - "The package gives `dotnet watch run` a launcher that registers a debug identity.\r\n" + - "WinAppRunUseExecutionAlias makes that launcher use an execution alias, which inherits\r\n" + - "stdout; the default AUMID activation is brokered and has no stdout at all, so the\r\n" + - "handshake could never be read even though the app itself would run.\r\n" + - "\r\n" + - "To preview the app unpackaged instead, set None."; - } -} diff --git a/src/vs-reactor/Reactor.VsExtension/Session/EmbedSession.cs b/src/vs-reactor/Reactor.VsExtension/Session/EmbedSession.cs index 8e8c330c8..a71b27db9 100644 --- a/src/vs-reactor/Reactor.VsExtension/Session/EmbedSession.cs +++ b/src/vs-reactor/Reactor.VsExtension/Session/EmbedSession.cs @@ -425,26 +425,14 @@ private async Task StartCoreAsync(string? componentName, CancellationToken ct, b detail.Append(((int)_firstSessionTimeout.TotalSeconds).ToString(System.Globalization.CultureInfo.InvariantCulture)); detail.AppendLine("s."); detail.AppendLine(); - - // A recognised failure signature is far more useful than the generic list, - // so lead with it and skip the guesses entirely. - var diagnosis = ChildFailureDiagnostics.Describe(stderr); - if (diagnosis != null) - { - detail.AppendLine(diagnosis); - } - else - { - detail.AppendLine("Common causes:"); - detail.AppendLine(" • First `dotnet watch` build is slow on a cold disk cache. The session will"); - detail.AppendLine(" automatically recover if the child eventually emits CAPTURE_PORT — watch"); - detail.AppendLine(" the Output pane. Set Reactor_VsExtension_HandshakeTimeoutSeconds=300"); - detail.AppendLine(" in the VS environment to extend this timeout."); - detail.AppendLine(" • Target project does not reference Microsoft.UI.Reactor.Devtools."); - detail.AppendLine(" • Target project is missing "); - detail.AppendLine(" • MSBuild really did fail — see the stderr below."); - } - + detail.AppendLine("Common causes:"); + detail.AppendLine(" • First `dotnet watch` build is slow on a cold disk cache. The session will"); + detail.AppendLine(" automatically recover if the child eventually emits CAPTURE_PORT — watch"); + detail.AppendLine(" the Output pane. Set Reactor_VsExtension_HandshakeTimeoutSeconds=300"); + detail.AppendLine(" in the VS environment to extend this timeout."); + detail.AppendLine(" • Target project does not reference Microsoft.UI.Reactor.Devtools."); + detail.AppendLine(" • Target project is missing "); + detail.AppendLine(" • MSBuild really did fail — see the stderr below."); if (!string.IsNullOrWhiteSpace(stderr)) { detail.AppendLine(); diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ChildFailureDiagnosticsTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ChildFailureDiagnosticsTests.cs deleted file mode 100644 index 64c4bd941..000000000 --- a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ChildFailureDiagnosticsTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -#nullable enable - -using Microsoft.UI.Reactor.VsExtension.Session; -using Xunit; - -namespace Reactor.VsExtension.Tests -{ - public sealed class ChildFailureDiagnosticsTests - { - /// - /// Verbatim stderr from a packaged (MSIX) Reactor app launched by `dotnet watch run` - /// without Microsoft.Windows.SDK.BuildTools.WinApp, captured while reproducing the - /// failure. Kept real rather than synthesised so the matcher is exercised against the - /// shape it actually has to recognise. - /// - private const string PackagedWithoutIdentityStderr = - "Unhandled exception. System.Runtime.InteropServices.COMException (0x80040154): Class not registered (0x80040154 (REGDB_E_CLASSNOTREG))\n" + - " at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode)\n" + - " at WinRT.ActivationFactory.Get(String typeName)\n" + - " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentInitializeOptions..ctor()\n" + - " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentManagerCS.AutoInitialize.get_Options()\n" + - " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Common.AutoInitialize.InitializeWindowsAppSDK()\n" + - " at .cctor()"; - - [Fact] - public void Describe_RecognisesPackagedAppLaunchedWithoutIdentity() - { - var result = ChildFailureDiagnostics.Describe(PackagedWithoutIdentityStderr); - - Assert.NotNull(result); - Assert.Equal(ChildFailureDiagnostics.PackagedWithoutIdentityGuidance, result); - } - - [Fact] - public void Guidance_NamesBothRequiredProjectChanges() - { - var result = ChildFailureDiagnostics.Describe(PackagedWithoutIdentityStderr)!; - - // Either one alone leaves the preview broken: the package supplies identity, the - // property supplies the inherited stdout the handshake is read from. - Assert.Contains("Microsoft.Windows.SDK.BuildTools.WinApp", result); - Assert.Contains("WinAppRunUseExecutionAlias", result); - Assert.Contains("WindowsPackageType", result); - } - - [Theory] - // A bare CLASSNOTREG with no Windows App SDK frames is some other missing - // registration; claiming it is a packaging problem would send the user the wrong way. - [InlineData("System.Runtime.InteropServices.COMException (0x80040154): Class not registered (REGDB_E_CLASSNOTREG)\n at Contoso.Widget.Activate()")] - // Windows App SDK frames without CLASSNOTREG are a different failure entirely. - [InlineData("Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentManager threw InvalidOperationException")] - [InlineData("MSBUILD : error MSB1009: Project file does not exist.")] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] - public void Describe_ReturnsNullForUnrelatedFailures(string? stderr) - { - Assert.Null(ChildFailureDiagnostics.Describe(stderr)); - } - - [Fact] - public void Describe_MatchesOnSymbolicNameWhenHresultIsAbsent() - { - var stderr = - "COMException: Class not registered (REGDB_E_CLASSNOTREG)\n" + - " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentInitializeOptions..ctor()"; - - Assert.NotNull(ChildFailureDiagnostics.Describe(stderr)); - } - - [Fact] - public void Describe_MatchesOnHresultWhenSymbolicNameIsAbsent() - { - var stderr = - "COMException (0x80040154): Class not registered\n" + - " at Microsoft.Windows.ApplicationModel.WindowsAppRuntime.DeploymentManagerCS.AutoInitialize.get_Options()"; - - Assert.NotNull(ChildFailureDiagnostics.Describe(stderr)); - } - } -} diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ReactorChildLauncherEnvironmentTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ReactorChildLauncherEnvironmentTests.cs new file mode 100644 index 000000000..04caef0c0 --- /dev/null +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/ReactorChildLauncherEnvironmentTests.cs @@ -0,0 +1,71 @@ +#nullable enable + +using Microsoft.UI.Reactor.VsExtension.Embed; +using Xunit; + +namespace Reactor.VsExtension.Tests +{ + /// + /// Covers the environment the preview child is launched with. These are the knobs that + /// decide whether a packaged (MSIX) project can complete the devtools handshake at all. + /// + public sealed class ReactorChildLauncherEnvironmentTests + { + private static ReactorChildLauncher.StartOptions Options() => new ReactorChildLauncher.StartOptions + { + CsprojPath = @"C:\src\App\App.csproj", + DotnetPath = "dotnet", + HostPid = 1234, + }; + + /// + /// Without this the WinApp launcher activates a packaged app by AUMID, which is + /// brokered and has no stdout, so CAPTURE_PORT is never observable and the preview + /// times out with nothing in stderr to explain why. + /// + [Fact] + public void StartInfo_RequestsExecutionAliasLaunchForPackagedProjects() + { + var info = ReactorChildLauncher.CreateStartInfoForTest(Options()); + + Assert.Equal( + "true", + info.EnvironmentVariables[ReactorChildLauncher.WinAppRunUseExecutionAliasVariable]); + } + + /// + /// The property has to travel in the environment. `dotnet watch` rejects `-p` when + /// `--project` is also present ("Cannot specify both '--project' and '-p' options"), + /// and an environment property is the lowest-precedence MSBuild property, so a + /// project that sets it explicitly still wins. + /// + [Fact] + public void StartInfo_DoesNotPassTheAliasPropertyOnTheCommandLine() + { + var info = ReactorChildLauncher.CreateStartInfoForTest(Options()); + + Assert.DoesNotContain(ReactorChildLauncher.WinAppRunUseExecutionAliasVariable, info.Arguments); + Assert.DoesNotContain(" -p:", info.Arguments); + Assert.DoesNotContain(" --property", info.Arguments); + } + + [Fact] + public void StartInfo_StillCarriesTheExistingWatchEnvironment() + { + var info = ReactorChildLauncher.CreateStartInfoForTest(Options()); + + Assert.Equal("1", info.EnvironmentVariables["DOTNET_WATCH_RESTART_ON_RUDE_EDIT"]); + Assert.Equal("1", info.EnvironmentVariables["DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER"]); + Assert.Equal("1", info.EnvironmentVariables["NoDefaultCurrentDirectoryInExePath"]); + } + + [Fact] + public void StartInfo_RedirectsStdoutSoTheHandshakeCanBeRead() + { + var info = ReactorChildLauncher.CreateStartInfoForTest(Options()); + + Assert.True(info.RedirectStandardOutput); + Assert.False(info.UseShellExecute); + } + } +} From 5dc846a84f738049e4dbf1ca12be3e2ad3634150 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 13:04:22 -0700 Subject: [PATCH 04/15] Trim the System.Text.Json changelog entry to the outcome Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f6bc361..0861a7038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,22 +69,10 @@ Conventions for contributors: ### Fixed -- **The Visual Studio preview failed every session with - `FileNotFoundException: Could not load file or assembly 'System.Text.Json, - Version=10.0.0.12'`.** Visual Studio ships `System.Text.Json` as a - *shared assembly* and binds extensions to its own copy through a - `devenv.exe.config` `` with a hard upper bound (VS 18: - `oldVersion="0.0.0.0-10.0.0.10"`). Redirects unify *upward* only, so when the - repo-wide Central Package Management pin rolled to `10.0.12` the extension began - requesting an assembly version *past* that ceiling: no redirect applied, the VSIX - carries no private copy (VS strips assemblies it provides) and the package - registers no `BindingPath`, so the CLR found nothing and threw on the first JSON - call — `EmbedClient.StatusAsync`, i.e. the very first thing every preview session - does. The extension now pins `System.Text.Json` to the `Microsoft.VisualStudio.SDK` - baseline (`9.0.0`) with a `VersionOverride` rather than following the repo-wide - version, and a new `VsSharedAssemblyBindingTests` gate reads the compiled - assembly references and fails the build if any VS shared assembly exceeds the - ceiling again. +- **The Visual Studio preview failed to start every session.** The extension was built + against a newer `System.Text.Json` than Visual Studio binds extensions to, so it failed + to load at runtime. It now tracks the `Microsoft.VisualStudio.SDK` baseline, with a test + to keep it there. - **Packaged (MSIX) apps now work in the Visual Studio preview.** The Reactor templates generate a packaged app by default, and previewing one previously failed. It now works From ce3bd2ff2d80b2db135d996f9f98a1fa590dcc91 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 13:28:38 -0700 Subject: [PATCH 05/15] Address Copilot review: raise the VSIX minimum host to 17.14 The extension compiles against Microsoft.VisualStudio.SDK 17.14, which baselines System.Text.Json 9.0.0, but source.extension.vsixmanifest still advertised Visual Studio 17.8. Measured against the SDK's own dependency graph, 17.8 baselines System.Text.Json 7.0.3 and 17.9 baselines 8.0.0, so a 9.0.0.0 reference falls outside those hosts' binding redirect range and the extension would fail to load there with exactly the FileNotFoundException this PR fixes for VS 18. Advertising 17.8 support was therefore unsound before this PR too. Raise all 13 InstallationTarget ranges and the CoreEditor prerequisite to [17.14,19.0), and say 17.14 in the guide and the VSIX README. Add VsixManifest_DoesNotAdvertiseHostsOlderThanThePinnedBaseline so the pin and the manifest cannot drift apart again: it parses every advertised range out of the manifest and fails on any below the baseline, with a positive control so an unparsed manifest cannot read as "no violations" and a hard failure if the file cannot be located rather than a vacuous pass. Mutation-checked by restoring 17.8, which reddens it. Also cite the originating PR on both changelog entries, per the cross-reference convention in CHANGELOG.md. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 5 +- docs/_pipeline/templates/vs-extension.md.dt | 2 +- docs/guide/vs-extension.md | 2 +- src/vs-reactor/README.md | 4 +- .../source.extension.vsixmanifest | 26 +++---- .../VsSharedAssemblyBindingTests.cs | 67 +++++++++++++++++++ 6 files changed, 87 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0861a7038..b18033f02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,11 +72,12 @@ Conventions for contributors: - **The Visual Studio preview failed to start every session.** The extension was built against a newer `System.Text.Json` than Visual Studio binds extensions to, so it failed to load at runtime. It now tracks the `Microsoft.VisualStudio.SDK` baseline, with a test - to keep it there. + to keep it there. The VSIX's advertised minimum host moves to Visual Studio 17.14 to + match that baseline. (PR #1282) - **Packaged (MSIX) apps now work in the Visual Studio preview.** The Reactor templates generate a packaged app by default, and previewing one previously failed. It now works - with no project changes. + with no project changes. (PR #1282) - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested diff --git a/docs/_pipeline/templates/vs-extension.md.dt b/docs/_pipeline/templates/vs-extension.md.dt index 7777f1b64..dc4cd5fef 100644 --- a/docs/_pipeline/templates/vs-extension.md.dt +++ b/docs/_pipeline/templates/vs-extension.md.dt @@ -12,7 +12,7 @@ tier: stub > ⚠️ **Very rough / experimental.** This is the roughest surface in an already experimental Reactor repository. It is meant for early feedback on the embedded-preview direction, not for reliable day-to-day development yet. Expect install friction, solution-load races, HWND/DPI edge cases, blank-window bugs, and breaking changes while this area is hardened. -The Visual Studio Embedded Preview hosts a live Microsoft.UI.Reactor (Reactor) component inside a Visual Studio tool window — Visual Studio 2022 (17.8+) or Visual Studio 2026 (18.x), with the **Visual Studio extension development** workload. Unlike the VS Code preview, which streams screenshots, the Visual Studio extension embeds the real WinUI surface so mouse, keyboard, focus, popups, and accessibility tree inspection work against the running app. +The Visual Studio Embedded Preview hosts a live Microsoft.UI.Reactor (Reactor) component inside a Visual Studio tool window — Visual Studio 2022 (17.14+) or Visual Studio 2026 (18.x), with the **Visual Studio extension development** workload. Unlike the VS Code preview, which streams screenshots, the Visual Studio extension embeds the real WinUI surface so mouse, keyboard, focus, popups, and accessibility tree inspection work against the running app. ## When to use it diff --git a/docs/guide/vs-extension.md b/docs/guide/vs-extension.md index ed85424e4..e8b8f35f8 100644 --- a/docs/guide/vs-extension.md +++ b/docs/guide/vs-extension.md @@ -3,7 +3,7 @@ > ⚠️ **Very rough / experimental.** This is the roughest surface in an already experimental Reactor repository. It is meant for early feedback on the embedded-preview direction, not for reliable day-to-day development yet. Expect install friction, solution-load races, HWND/DPI edge cases, blank-window bugs, and breaking changes while this area is hardened. -The Visual Studio Embedded Preview hosts a live Microsoft.UI.Reactor (Reactor) component inside a Visual Studio tool window — Visual Studio 2022 (17.8+) or Visual Studio 2026 (18.x), with the **Visual Studio extension development** workload. Unlike the VS Code preview, which streams screenshots, the Visual Studio extension embeds the real WinUI surface so mouse, keyboard, focus, popups, and accessibility tree inspection work against the running app. +The Visual Studio Embedded Preview hosts a live Microsoft.UI.Reactor (Reactor) component inside a Visual Studio tool window — Visual Studio 2022 (17.14+) or Visual Studio 2026 (18.x), with the **Visual Studio extension development** workload. Unlike the VS Code preview, which streams screenshots, the Visual Studio extension embeds the real WinUI surface so mouse, keyboard, focus, popups, and accessibility tree inspection work against the running app. ## When to use it diff --git a/src/vs-reactor/README.md b/src/vs-reactor/README.md index 7141a063d..f17dc4a0d 100644 --- a/src/vs-reactor/README.md +++ b/src/vs-reactor/README.md @@ -1,6 +1,6 @@ # Visual Studio Reactor Preview -Live embedded preview pane for Microsoft.UI.Reactor apps in Visual Studio 2022 (17.8+) and Visual Studio 2026 (18.x). +Live embedded preview pane for Microsoft.UI.Reactor apps in Visual Studio 2022 (17.14+) and Visual Studio 2026 (18.x). > ⚠️ **Very rough / experimental.** This VSIX is the roughest surface in an already experimental repository. It exists to validate the embedded-preview direction and collect feedback; expect install friction, solution-load races, HWND/DPI quirks, blank-window bugs, and breaking changes while it is hardened. @@ -12,7 +12,7 @@ Build the full VSIX, double-click it, install into the target Visual Studio hive pwsh -File src\vs-reactor\Build-Vsix.ps1 ``` -This requires Visual Studio 2022 (17.8+) or Visual Studio 2026 (18.x) with the **Visual Studio extension development** workload. Phase 1 may ship unsigned for dev distribution, so Visual Studio may show an unsigned-extension warning. Treat installs as developer smoke builds, not stable product drops. +This requires Visual Studio 2022 (17.14+) or Visual Studio 2026 (18.x) with the **Visual Studio extension development** workload. Phase 1 may ship unsigned for dev distribution, so Visual Studio may show an unsigned-extension warning. Treat installs as developer smoke builds, not stable product drops. ## Debug diff --git a/src/vs-reactor/Reactor.VsExtension/source.extension.vsixmanifest b/src/vs-reactor/Reactor.VsExtension/source.extension.vsixmanifest index c44f220a0..090641684 100644 --- a/src/vs-reactor/Reactor.VsExtension/source.extension.vsixmanifest +++ b/src/vs-reactor/Reactor.VsExtension/source.extension.vsixmanifest @@ -7,45 +7,45 @@ https://github.com/microsoft/microsoft-ui-reactor - + amd64 - + arm64 - + amd64 - + arm64 - + amd64 - + arm64 - + amd64 - + arm64 - + amd64 - + arm64 - + amd64 - + arm64 - + diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs index f5f32557d..0055cf1b3 100644 --- a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using Microsoft.UI.Reactor.VsExtension.Embed; using Xunit; @@ -51,6 +53,71 @@ private static AssemblyName[] ExtensionReferences() return typeof(EmbedClient).Assembly.GetReferencedAssemblies(); } + /// + /// The ceiling above is only half the contract. A shared-assembly reference also has + /// to be covered by the oldest Visual Studio the VSIX claims to install into, + /// and that claim lives in source.extension.vsixmanifest — a different file that + /// nothing otherwise ties to this pin. + /// + /// + /// Microsoft.VisualStudio.SDK baselines System.Text.Json per VS version: 17.8 -> 7.0.3, + /// 17.9 -> 8.0.0, 17.14 -> 9.0.0. Referencing 9.0.0.0 while advertising 17.8 support + /// would leave the extension dead on every host below 17.14 in exactly the way this + /// PR fixes for VS 18, so the manifest minimum must not fall below the SDK whose + /// baseline we pin to. + /// + private static readonly Version MinimumSupportedVsHost = new Version(17, 14); + + [Fact] + public void VsixManifest_DoesNotAdvertiseHostsOlderThanThePinnedBaseline() + { + var manifest = FindRepoFile(Path.Combine( + "src", "vs-reactor", "Reactor.VsExtension", "source.extension.vsixmanifest")); + + var text = File.ReadAllText(manifest); + var ranges = Regex.Matches(text, @"Version=""\[(?\d+\.\d+)\s*,") + .Cast() + .Select(m => m.Groups["min"].Value) + .ToArray(); + + // Positive control: a manifest we failed to parse must not read as "no violations". + Assert.NotEmpty(ranges); + + var tooOld = ranges + .Select(v => new Version(v)) + .Where(v => v < MinimumSupportedVsHost) + .Select(v => v.ToString()) + .Distinct() + .ToArray(); + + Assert.True( + tooOld.Length == 0, + $"source.extension.vsixmanifest advertises Visual Studio {string.Join(", ", tooOld)}, " + + $"older than the {MinimumSupportedVsHost} baseline the extension's System.Text.Json pin " + + "requires. Those hosts redirect System.Text.Json below the pinned version, so the " + + "extension would fail to load there with FileNotFoundException. Raise the manifest " + + "minimum, or lower the pin and this constant together."); + } + + private static string FindRepoFile(string relativePath) + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + dir = dir.Parent; + } + + throw new FileNotFoundException( + $"Could not locate '{relativePath}' walking up from {AppContext.BaseDirectory}. " + + "The test cannot pass vacuously — fix the lookup rather than deleting the assertion."); + } + /// /// Positive control for : /// a clean result there is only meaningful if the probe can actually see the reference From c59534da04146493f2a03015315edfadc5268428 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 13:37:00 -0700 Subject: [PATCH 06/15] Parse the VSIX manifest as XML so the host gate cannot skip a range The regex form matched only two-component minimums, so a range carrying a patch component -- [17.8.1,19.0) -- matched nothing and was dropped silently, while Assert.NotEmpty stayed satisfied by the remaining ranges. A skipped range read exactly like a compliant one, which is the failure mode the gate exists to prevent. Read the manifest with XDocument and scope to InstallationTarget and Prerequisite elements instead. Every Version attribute on those elements is now parsed, and an unrecognised range shape throws rather than being skipped, so the gate cannot silently narrow again. Error text names the offending element and Id. Mutation-checked with the exact reported case: rewriting one range to [17.8.1,19.0) reddens the test and names it. The previous regex passed it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- .../VsSharedAssemblyBindingTests.cs | 77 ++++++++++++++----- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs index 0055cf1b3..475ea9879 100644 --- a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs @@ -5,7 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; +using System.Xml.Linq; using Microsoft.UI.Reactor.VsExtension.Embed; using Xunit; @@ -74,29 +74,68 @@ public void VsixManifest_DoesNotAdvertiseHostsOlderThanThePinnedBaseline() var manifest = FindRepoFile(Path.Combine( "src", "vs-reactor", "Reactor.VsExtension", "source.extension.vsixmanifest")); - var text = File.ReadAllText(manifest); - var ranges = Regex.Matches(text, @"Version=""\[(?\d+\.\d+)\s*,") - .Cast() - .Select(m => m.Groups["min"].Value) + // Parsed as XML and scoped to the host elements rather than pattern-matched out of + // the raw text: a regex over version literals silently skips any shape it did not + // anticipate (a patch component such as [17.8.1,19.0) being the obvious one), and a + // skipped range reads exactly like a compliant one. + var doc = XDocument.Load(manifest); + var declarations = doc.Descendants() + .Where(e => e.Name.LocalName == "InstallationTarget" || e.Name.LocalName == "Prerequisite") + .Select(e => new + { + Element = e.Name.LocalName, + Id = (string?)e.Attribute("Id"), + Version = (string?)e.Attribute("Version"), + }) + .Where(d => !string.IsNullOrWhiteSpace(d.Version)) .ToArray(); - // Positive control: a manifest we failed to parse must not read as "no violations". - Assert.NotEmpty(ranges); + // Positive control: a manifest we failed to read must not report "no violations". + Assert.NotEmpty(declarations); - var tooOld = ranges - .Select(v => new Version(v)) - .Where(v => v < MinimumSupportedVsHost) - .Select(v => v.ToString()) - .Distinct() - .ToArray(); + var tooOld = new List(); + foreach (var declaration in declarations) + { + // Unparseable is a failure, never a skip — that is the hole this replaced. + var minimum = ParseRangeMinimum(declaration.Version!); + if (minimum < MinimumSupportedVsHost) + { + tooOld.Add($"{declaration.Element} {declaration.Id} {declaration.Version}"); + } + } Assert.True( - tooOld.Length == 0, - $"source.extension.vsixmanifest advertises Visual Studio {string.Join(", ", tooOld)}, " - + $"older than the {MinimumSupportedVsHost} baseline the extension's System.Text.Json pin " - + "requires. Those hosts redirect System.Text.Json below the pinned version, so the " - + "extension would fail to load there with FileNotFoundException. Raise the manifest " - + "minimum, or lower the pin and this constant together."); + tooOld.Count == 0, + $"source.extension.vsixmanifest advertises Visual Studio hosts older than {MinimumSupportedVsHost}, " + + "which redirect System.Text.Json below the version the extension is pinned to, so the extension " + + "would fail to load there with FileNotFoundException. Raise the manifest minimum, or lower the " + + "pin and this constant together. Offending declarations: " + string.Join("; ", tooOld)); + } + + /// + /// Reads the lower bound out of a VSIX version range such as [17.14,19.0), + /// [17.8.1,19.0) or a bare 17.14. Throws rather than returning a + /// sentinel, so an unrecognised shape fails the gate instead of slipping past it. + /// + private static Version ParseRangeMinimum(string range) + { + var text = range.Trim().TrimStart('[', '('); + var comma = text.IndexOf(','); + if (comma >= 0) + { + text = text.Substring(0, comma); + } + + text = text.TrimEnd(']', ')').Trim(); + + if (!Version.TryParse(text, out var version)) + { + throw new FormatException( + $"Could not read a minimum version out of the manifest range '{range}'. " + + "Teach this parser the new shape — do not let it be skipped."); + } + + return version; } private static string FindRepoFile(string relativePath) From ac75bb2bf1a57339db1eed9689cb901192c343a1 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 13:46:17 -0700 Subject: [PATCH 07/15] Correct compatibility and packaging claims in the docs and changelog Three follow-ups from review, all factual accuracy rather than behaviour. The claim that the Reactor templates generate a packaged app by default is not true of this repository's template: tools/Templates/templates/WinUIApp-CSharp sets WindowsPackageType=None. The packaged-by-default behaviour belongs to the separately distributed Microsoft.WindowsAppSDK.WinUI.CSharp.Templates pack, which is what was used while reproducing the bug. Rather than disambiguate two templates in a changelog line, describe the fix by what it does: previewing a packaged project now works with no project changes. The guide index still advertised the VS extension page as Visual Studio 2022 17.8+, contradicting the raised minimum. Update index.md.dt and regenerate, so index.md and README.md agree with the extension page and the manifest. Regenerating the index also rewrote four unrelated readme screenshots; those are reverted. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 5 ++--- docs/_pipeline/templates/index.md.dt | 2 +- docs/_pipeline/templates/vs-extension.md.dt | 4 ++-- docs/guide/README.md | 2 +- docs/guide/index.md | 2 +- docs/guide/vs-extension.md | 4 ++-- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b18033f02..07c5dd861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,9 +75,8 @@ Conventions for contributors: to keep it there. The VSIX's advertised minimum host moves to Visual Studio 17.14 to match that baseline. (PR #1282) -- **Packaged (MSIX) apps now work in the Visual Studio preview.** The Reactor templates - generate a packaged app by default, and previewing one previously failed. It now works - with no project changes. (PR #1282) +- **Packaged (MSIX) apps now work in the Visual Studio preview.** Previewing a packaged + project previously failed; it now works with no project changes. (PR #1282) - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested diff --git a/docs/_pipeline/templates/index.md.dt b/docs/_pipeline/templates/index.md.dt index 711ea1243..facca1e94 100644 --- a/docs/_pipeline/templates/index.md.dt +++ b/docs/_pipeline/templates/index.md.dt @@ -139,7 +139,7 @@ first; everyone else can follow the order. ### 7. Tooling & process - **[Dev Tooling](dev-tooling.md)** — `mur` CLI, MCP server, VS Code panel, dotnet watch, in-app dev menu -- **[VS Extension](vs-extension.md)** — Rough experimental embedded preview for Visual Studio 2022 (17.8+) and 2026 (18.x) +- **[VS Extension](vs-extension.md)** — Rough experimental embedded preview for Visual Studio 2022 (17.14+) and 2026 (18.x) - **[Testing](testing.md)** — Headless renderer, structural assertions, async test patterns - **[Performance](performance.md)** — ETW, EventDispatch, flame graphs - **[Packaging](packaging.md)** — MSIX, single-file, ARM64, AOT considerations diff --git a/docs/_pipeline/templates/vs-extension.md.dt b/docs/_pipeline/templates/vs-extension.md.dt index dc4cd5fef..bbb822aa2 100644 --- a/docs/_pipeline/templates/vs-extension.md.dt +++ b/docs/_pipeline/templates/vs-extension.md.dt @@ -37,8 +37,8 @@ The component picker is a ComboBox in the tool window chrome. By default it auto ## Packaged (MSIX) projects -Packaged projects work with no project changes. The Reactor templates generate a packaged -app by default, and the preview launches it with package identity for you. +Packaged projects work with no project changes. The preview launches them with package +identity for you. To preview the app unpackaged instead, set `None`. diff --git a/docs/guide/README.md b/docs/guide/README.md index db51faf5a..eb1f1022d 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -165,7 +165,7 @@ first; everyone else can follow the order. ### 7. Tooling & process - **[Dev Tooling](dev-tooling.md)** — `mur` CLI, MCP server, VS Code panel, dotnet watch, in-app dev menu -- **[VS Extension](vs-extension.md)** — Rough experimental embedded preview for Visual Studio 2022 (17.8+) and 2026 (18.x) +- **[VS Extension](vs-extension.md)** — Rough experimental embedded preview for Visual Studio 2022 (17.14+) and 2026 (18.x) - **[Testing](testing.md)** — Headless renderer, structural assertions, async test patterns - **[Performance](performance.md)** — ETW, EventDispatch, flame graphs - **[Packaging](packaging.md)** — MSIX, single-file, ARM64, AOT considerations diff --git a/docs/guide/index.md b/docs/guide/index.md index db51faf5a..eb1f1022d 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -165,7 +165,7 @@ first; everyone else can follow the order. ### 7. Tooling & process - **[Dev Tooling](dev-tooling.md)** — `mur` CLI, MCP server, VS Code panel, dotnet watch, in-app dev menu -- **[VS Extension](vs-extension.md)** — Rough experimental embedded preview for Visual Studio 2022 (17.8+) and 2026 (18.x) +- **[VS Extension](vs-extension.md)** — Rough experimental embedded preview for Visual Studio 2022 (17.14+) and 2026 (18.x) - **[Testing](testing.md)** — Headless renderer, structural assertions, async test patterns - **[Performance](performance.md)** — ETW, EventDispatch, flame graphs - **[Packaging](packaging.md)** — MSIX, single-file, ARM64, AOT considerations diff --git a/docs/guide/vs-extension.md b/docs/guide/vs-extension.md index e8b8f35f8..31eefcd4d 100644 --- a/docs/guide/vs-extension.md +++ b/docs/guide/vs-extension.md @@ -28,8 +28,8 @@ The component picker is a ComboBox in the tool window chrome. By default it auto ## Packaged (MSIX) projects -Packaged projects work with no project changes. The Reactor templates generate a packaged -app by default, and the preview launches it with package identity for you. +Packaged projects work with no project changes. The preview launches them with package +identity for you. To preview the app unpackaged instead, set `None`. From 8cb79aff8f6360763994a97d7957ce4c8a2254a8 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 13:55:24 -0700 Subject: [PATCH 08/15] Derive the VSIX host minimum from the central VS SDK version The host minimum was a second hard-coded literal, independent of the dependency it gates. Bumping Microsoft.VisualStudio.SDK in Directory.Packages.props while updating the System.Text.Json pin and the ceiling would leave that literal behind, keeping all three tests green while the VSIX still advertised hosts the new pin cannot load on -- the same class of silent breakage this PR started from. Read the Microsoft.VisualStudio.SDK PackageVersion instead and take its major.minor as the required host floor. The extension cannot support a host older than the SDK it compiles against, and that SDK is what fixes the System.Text.Json baseline, so the two now move together by construction. A missing or unparseable PackageVersion throws rather than degrading to a default. Mutation-checked with the reported scenario: bumping the SDK to 18.0 reddens the manifest gate and names all thirteen declarations. It stayed green before. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- .../VsSharedAssemblyBindingTests.cs | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs index 475ea9879..b04723f54 100644 --- a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs @@ -60,13 +60,41 @@ private static AssemblyName[] ExtensionReferences() /// nothing otherwise ties to this pin. /// /// - /// Microsoft.VisualStudio.SDK baselines System.Text.Json per VS version: 17.8 -> 7.0.3, - /// 17.9 -> 8.0.0, 17.14 -> 9.0.0. Referencing 9.0.0.0 while advertising 17.8 support - /// would leave the extension dead on every host below 17.14 in exactly the way this - /// PR fixes for VS 18, so the manifest minimum must not fall below the SDK whose - /// baseline we pin to. + /// Derived from the central Microsoft.VisualStudio.SDK version rather than + /// written down again here. The extension cannot support a host older than the SDK it + /// compiles against, and that SDK is what fixes the System.Text.Json baseline: the SDK + /// baselines it per VS version (17.8 -> 7.0.3, 17.9 -> 8.0.0, 17.14 -> 9.0.0). A second + /// hard-coded literal would let an SDK bump update the pin and the ceiling while + /// silently leaving the host minimum behind, keeping every test green while the VSIX + /// advertised hosts the new pin cannot load on. One source of truth means that bump + /// fails this gate until the manifest is raised deliberately. /// - private static readonly Version MinimumSupportedVsHost = new Version(17, 14); + private static Version MinimumSupportedVsHost => VisualStudioSdkVersion(); + + private static Version VisualStudioSdkVersion() + { + var props = FindRepoFile("Directory.Packages.props"); + + var raw = XDocument.Load(props) + .Descendants() + .Where(e => e.Name.LocalName == "PackageVersion") + .Where(e => string.Equals( + (string?)e.Attribute("Include"), + "Microsoft.VisualStudio.SDK", + StringComparison.OrdinalIgnoreCase)) + .Select(e => (string?)e.Attribute("Version")) + .FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)); + + if (raw == null || !Version.TryParse(raw, out var version)) + { + throw new InvalidOperationException( + "Could not read the Microsoft.VisualStudio.SDK PackageVersion from " + + $"'{props}'. The host-minimum gate has no source of truth without it — fix " + + "the lookup rather than reintroducing a hard-coded version."); + } + + return new Version(version.Major, version.Minor); + } [Fact] public void VsixManifest_DoesNotAdvertiseHostsOlderThanThePinnedBaseline() @@ -107,9 +135,11 @@ public void VsixManifest_DoesNotAdvertiseHostsOlderThanThePinnedBaseline() Assert.True( tooOld.Count == 0, $"source.extension.vsixmanifest advertises Visual Studio hosts older than {MinimumSupportedVsHost}, " - + "which redirect System.Text.Json below the version the extension is pinned to, so the extension " - + "would fail to load there with FileNotFoundException. Raise the manifest minimum, or lower the " - + "pin and this constant together. Offending declarations: " + string.Join("; ", tooOld)); + + "the Microsoft.VisualStudio.SDK version the extension compiles against. Those hosts redirect " + + "System.Text.Json below the version the extension is pinned to, so the extension would fail to " + + "load there with FileNotFoundException. Raise the manifest minimum to match the SDK, or move the " + + "SDK, the System.Text.Json pin and the manifest together. Offending declarations: " + + string.Join("; ", tooOld)); } /// From e152457321d9396c3e4ce569e9009e2cbcbaf584 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 14:04:56 -0700 Subject: [PATCH 09/15] Scope the packaged-preview claim to projects with WinApp run support WinAppRunUseExecutionAlias is consumed only by the targets that Microsoft.Windows.SDK.BuildTools.WinApp installs, so "works with no project changes" was true only for project shapes that already reference it -- which is what the reproduction used. It is not true of this repository's own shapes: tools/Templates/templates/WinUIApp-CSharp references only Reactor and Devtools, and the MSIX conversion documented in packaging.md.dt adds only MSIX properties and a manifest. A project following either route would still launch without the alias, and the environment property would have no consumer. Narrow both claims rather than overstate the fix. The changelog now says the preview supports packaged apps, without the no-edit promise, and the guide states the run-support prerequisite and that a hand-converted MSIX project needs that package added. The launcher behaviour is unchanged and remains correct: setting the property is the whole of what the extension can do from its side. It cannot make a target project import MSBuild targets, which is why the launcher test asserts the property is set rather than asserting a project shape. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 4 ++-- docs/_pipeline/templates/vs-extension.md.dt | 6 ++++-- docs/guide/vs-extension.md | 6 ++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c5dd861..fd0915e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,8 +75,8 @@ Conventions for contributors: to keep it there. The VSIX's advertised minimum host moves to Visual Studio 17.14 to match that baseline. (PR #1282) -- **Packaged (MSIX) apps now work in the Visual Studio preview.** Previewing a packaged - project previously failed; it now works with no project changes. (PR #1282) +- **The Visual Studio preview now supports packaged (MSIX) apps.** Previewing a packaged + project previously failed. (PR #1282) - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested diff --git a/docs/_pipeline/templates/vs-extension.md.dt b/docs/_pipeline/templates/vs-extension.md.dt index bbb822aa2..d37f09b73 100644 --- a/docs/_pipeline/templates/vs-extension.md.dt +++ b/docs/_pipeline/templates/vs-extension.md.dt @@ -37,8 +37,10 @@ The component picker is a ComboBox in the tool window chrome. By default it auto ## Packaged (MSIX) projects -Packaged projects work with no project changes. The preview launches them with package -identity for you. +Packaged projects preview as-is when the project uses the Windows App SDK run support +(`Microsoft.Windows.SDK.BuildTools.WinApp`) — the preview supplies the execution-alias +setting that run support needs in order to inherit stdout. A project converted to MSIX by +hand needs that package added before the preview can attach. To preview the app unpackaged instead, set `None`. diff --git a/docs/guide/vs-extension.md b/docs/guide/vs-extension.md index 31eefcd4d..624c7ed95 100644 --- a/docs/guide/vs-extension.md +++ b/docs/guide/vs-extension.md @@ -28,8 +28,10 @@ The component picker is a ComboBox in the tool window chrome. By default it auto ## Packaged (MSIX) projects -Packaged projects work with no project changes. The preview launches them with package -identity for you. +Packaged projects preview as-is when the project uses the Windows App SDK run support +(`Microsoft.Windows.SDK.BuildTools.WinApp`) — the preview supplies the execution-alias +setting that run support needs in order to inherit stdout. A project converted to MSIX by +hand needs that package added before the preview can attach. To preview the app unpackaged instead, set `None`. From 092f4c2727acf7347396d19a929f9e12c0f5e580 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 14:17:42 -0700 Subject: [PATCH 10/15] Qualify the packaged-preview summary on the launcher helper The XML doc still claimed the fix works "without any project edit" and that the Reactor templates reference the WinApp run-support package by default. Neither holds for this repository: the in-repo template is WindowsPackageType=None and references only Reactor and Devtools. The packaged-by-default shape belongs to the separately distributed WinUI template pack. Restate the summary as what it does -- lets a packaged project complete the handshake when the project uses the Windows App SDK run support -- and say plainly that a project without that package must add it first. Matches the guide and the PR scope; no behaviour change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- .../Embed/ReactorChildLauncher.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs b/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs index 7d7679b05..5bfaa3981 100644 --- a/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs +++ b/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs @@ -151,18 +151,25 @@ private static ProcessStartInfo CreateStartInfo(StartOptions options) } /// - /// Makes the preview work on packaged (MSIX) projects without any project edit. + /// Lets a packaged (MSIX) project complete the devtools handshake, for projects that + /// use the Windows App SDK run support. /// /// /// /// A packaged project launched by dotnet watch run goes through the launcher - /// that Microsoft.Windows.SDK.BuildTools.WinApp installs (the Reactor project - /// templates reference it by default). That launcher activates the app by AUMID, - /// which is brokered and therefore has no stdout at all — and stdout is how - /// the child reports CAPTURE_PORT / CAPTURE_TOKEN. The app would start - /// and render while the preview silently waited out its handshake timeout, with no - /// error anywhere to explain it. Setting this property switches the launcher to an - /// execution alias, which inherits stdout. + /// that Microsoft.Windows.SDK.BuildTools.WinApp installs. That launcher + /// activates the app by AUMID, which is brokered and therefore has no stdout at + /// all — and stdout is how the child reports CAPTURE_PORT / + /// CAPTURE_TOKEN. The app would start and render while the preview silently + /// waited out its handshake timeout, with no error anywhere to explain it. Setting + /// this property switches the launcher to an execution alias, which inherits stdout. + /// + /// + /// This does not make every packaged project previewable on its own: a project that + /// does not reference that package has nothing to consume the property and must add + /// it first. Notably the in-repo Reactor template is unpackaged + /// (WindowsPackageType=None) and does not carry the package. See the VS + /// extension guide. /// /// /// Set unconditionally rather than behind a packaged/unpackaged probe: the WinApp From ec6b759aa0d736f63a3d6cbb9e041775a79d389a Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 14:19:10 -0700 Subject: [PATCH 11/15] Harden the repo-file lookup against a rooted path (code-quality bot) Path.Combine silently discards earlier arguments when a later one is rooted, so a rooted relativePath would make every iteration of the walk probe the same absolute path -- the search would still "run" while testing nothing, which is the vacuous-pass failure this helper exists to prevent. Guard the parameter and throw, and pass the manifest path as a single repo-relative literal instead of assembling it with Path.Combine. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- .../VsSharedAssemblyBindingTests.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs index b04723f54..9beb32bca 100644 --- a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs @@ -99,8 +99,7 @@ private static Version VisualStudioSdkVersion() [Fact] public void VsixManifest_DoesNotAdvertiseHostsOlderThanThePinnedBaseline() { - var manifest = FindRepoFile(Path.Combine( - "src", "vs-reactor", "Reactor.VsExtension", "source.extension.vsixmanifest")); + var manifest = FindRepoFile("src/vs-reactor/Reactor.VsExtension/source.extension.vsixmanifest"); // Parsed as XML and scoped to the host elements rather than pattern-matched out of // the raw text: a regex over version literals silently skips any shape it did not @@ -170,6 +169,16 @@ private static Version ParseRangeMinimum(string range) private static string FindRepoFile(string relativePath) { + // A rooted argument would make Path.Combine below discard dir.FullName, so every + // iteration would probe the same absolute path and the walk would be meaningless. + // Fail loudly rather than let the lookup quietly stop searching. + if (Path.IsPathRooted(relativePath)) + { + throw new ArgumentException( + $"Expected a repo-relative path, got the rooted path '{relativePath}'.", + nameof(relativePath)); + } + var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null) { From 1fb97caee49ca7ff242a040587661d97b8fbefc4 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 14:28:32 -0700 Subject: [PATCH 12/15] Drop the dormant ceiling entry and make dormant entries impossible Ceilings carried System.Text.Encodings.Web, but the gate reads this assembly's direct manifest references and that dependency is transitive through System.Text.Json -- EmbedClient never touches its types. The entry was therefore never evaluated: any version could have resolved and the gate would have stayed green while appearing to cover it. Remove it, and turn the positive control into a stronger invariant. Instead of asserting one known reference is visible, assert every key in Ceilings is a direct reference. A ceiling nothing checks is false confidence, and this makes adding one fail immediately rather than sit dormant. Mutation-checked: re-adding System.Text.Encodings.Web reddens the control and names it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- .../VsSharedAssemblyBindingTests.cs | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs index 9beb32bca..84712f711 100644 --- a/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs +++ b/src/vs-reactor/Tests/Reactor.VsExtension.Tests/VsSharedAssemblyBindingTests.cs @@ -41,11 +41,17 @@ public sealed class VsSharedAssemblyBindingTests // VS release that satisfies the SDK redirects at least that high, and building lower is // always safe because redirects unify upward. Bump these only together with the // Microsoft.VisualStudio.SDK PackageVersion in Directory.Packages.props. + // + // Only *direct* assembly references are gated, because that is what the CLR resolves + // against this assembly's manifest. An entry for something the extension does not + // reference directly would never be exercised and would be pure false confidence, so + // EveryGatedAssemblyIsActuallyReferenced fails on any such entry rather than letting it + // sit dormant. System.Text.Encodings.Web was exactly that: transitive through + // System.Text.Json, with no direct reference for this probe to ever see. private static readonly IReadOnlyDictionary Ceilings = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["System.Text.Json"] = new Version(9, 0, 0, 0), - ["System.Text.Encodings.Web"] = new Version(9, 0, 0, 0), }; private static AssemblyName[] ExtensionReferences() @@ -198,16 +204,33 @@ private static string FindRepoFile(string relativePath) /// /// Positive control for : - /// a clean result there is only meaningful if the probe can actually see the reference - /// it gates. If the extension legitimately stops using System.Text.Json, delete this - /// whole class rather than relaxing this assertion. + /// a clean result there is only meaningful if the probe can actually see the references + /// it gates. Because that probe reads direct assembly references, a ceiling entry for + /// something referenced only transitively would never be evaluated and the gate would + /// report success having tested nothing. Asserting every entry is visible makes a + /// dormant entry impossible: add one the extension does not reference directly and this + /// fails, forcing it to be made real or dropped. /// [Fact] - public void ExtensionReferences_IncludeAGatedSharedAssembly() + public void EveryGatedAssemblyIsActuallyReferenced() { - var names = ExtensionReferences().Select(r => r.Name).ToArray(); + var names = ExtensionReferences() + .Select(r => r.Name) + .Where(n => n != null) + .ToArray(); - Assert.Contains("System.Text.Json", names); + var dormant = Ceilings.Keys + .Where(k => !names.Contains(k, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + Assert.True( + dormant.Length == 0, + "These entries in Ceilings are not direct assembly references of Reactor.VsExtension, " + + "so the ceiling gate never evaluates them and would stay green no matter what version " + + "resolved: " + string.Join(", ", dormant) + ". Either the extension should reference them " + + "directly, or they should be removed — a ceiling nothing checks is false confidence. " + + "(Transitive dependencies are not covered by this gate; the CLR resolves this assembly's " + + "own manifest references, which is what the binding failure this guards against involves.)"); } [Fact] From 47135bd70412b3fcf4fa43859ba20e75219da4e1 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 14:33:34 -0700 Subject: [PATCH 13/15] Qualify the changelog claim to WinApp-enabled packaged projects The note promised packaged-app support generally, but the fix only reaches projects that use the Windows App SDK run support; a hand-converted MSIX project without that package still cannot attach. Name the prerequisite inline so the release note matches the guide and the PR scope. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0915e68..fe63218dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,8 +75,9 @@ Conventions for contributors: to keep it there. The VSIX's advertised minimum host moves to Visual Studio 17.14 to match that baseline. (PR #1282) -- **The Visual Studio preview now supports packaged (MSIX) apps.** Previewing a packaged - project previously failed. (PR #1282) +- **The Visual Studio preview now supports packaged (MSIX) apps** that use the Windows App + SDK run support (`Microsoft.Windows.SDK.BuildTools.WinApp`). Previewing one previously + failed. (PR #1282) - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested From 80d73e5de84f693ac278549cf00b6f46f587877b Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 14:39:30 -0700 Subject: [PATCH 14/15] Update spec 056 to the 17.14 host baseline The minimum-host change left the design and implementation specs specifying Microsoft.VisualStudio.SDK / VSSDK.BuildTools 17.8+ and a VSIX declaring VS 2022 17.8+, which would invite future work to restore the unsupported range. Seven references across both documents now read 17.14+. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- docs/specs/056-visual-studio-embedded-preview.md | 4 ++-- ...56-visual-studio-embedded-preview-implementation.md | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/specs/056-visual-studio-embedded-preview.md b/docs/specs/056-visual-studio-embedded-preview.md index 8ae00fed3..f08391dcc 100644 --- a/docs/specs/056-visual-studio-embedded-preview.md +++ b/docs/specs/056-visual-studio-embedded-preview.md @@ -640,8 +640,8 @@ The csproj is the legacy SDK style (`Microsoft.NET.Sdk` + `true Key NuGet packages: -- `Microsoft.VisualStudio.SDK` 17.8+ -- `Microsoft.VSSDK.BuildTools` 17.8+ +- `Microsoft.VisualStudio.SDK` 17.14+ +- `Microsoft.VSSDK.BuildTools` 17.14+ - `Newtonsoft.Json` (VS already ships it; pin to the version VS 2022 carries to avoid binding redirect headaches) — *or* `System.Text.Json` via the JSON helpers already in the SDK. ### §6.2 Package + tool window diff --git a/docs/specs/tasks/056-visual-studio-embedded-preview-implementation.md b/docs/specs/tasks/056-visual-studio-embedded-preview-implementation.md index 0017146db..969a109fa 100644 --- a/docs/specs/tasks/056-visual-studio-embedded-preview-implementation.md +++ b/docs/specs/tasks/056-visual-studio-embedded-preview-implementation.md @@ -178,8 +178,8 @@ real extension from a clean slate. - [ ] Create `src/vs-reactor-spike/` (gitignored, **not** part of `Reactor.slnx`). Hand-roll a minimal VSIX project on .NET - Framework 4.7.2 with `Microsoft.VisualStudio.SDK` 17.8+ and - `Microsoft.VSSDK.BuildTools` 17.8+. Single `ToolWindowPane` + Framework 4.7.2 with `Microsoft.VisualStudio.SDK` 17.14+ and + `Microsoft.VSSDK.BuildTools` 17.14+. Single `ToolWindowPane` whose content is a `Grid` containing only a `HwndHostPlaceholder`. - [ ] Hand-roll a minimal Reactor target app under `samples/apps/embed-spike/` (also gitignored or removed before Phase 1 PR) whose `Program.cs` @@ -464,12 +464,12 @@ green) - [ ] Create `src/vs-reactor/Reactor.VsExtension/` with: - `Reactor.VsExtension.csproj` (.NET Framework 4.7.2, SDK style, - `true`, `Microsoft.VisualStudio.SDK` 17.8+ - PackageReference, `Microsoft.VSSDK.BuildTools` 17.8+, + `true`, `Microsoft.VisualStudio.SDK` 17.14+ + PackageReference, `Microsoft.VSSDK.BuildTools` 17.14+, `true`, VSIX properties). - `source.extension.vsixmanifest` declaring the Reactor publisher, - name, version, dependencies (VS 2022 17.8+), and the + name, version, dependencies (VS 2022 17.14+), and the `extension.vsixmanifest` Product/Asset entries. - Standard VS SDK template files: `Properties/AssemblyInfo.cs`, `Resources.resx`, `VSPackage.resx`, `index.html` if needed. From b23e4137e672a88944f3e8990da488cd722acfcb Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 25 Sep 2026 14:54:57 -0700 Subject: [PATCH 15/15] Restore the packaged claim: the Reactor templates are packaged by default Merge origin/main, which removed the in-repo template pack (#1277) in favour of the Windows App SDK dotnet new templates. WinAppSdkTemplates.PackageId is now Microsoft.WindowsAppSDK.WinUI.CSharp.Templates -- the same pack `dotnet new reactor` resolves to, and the same pack the packaged validation in this PR was run against. That pack's Reactor template declares no WindowsPackageType (so, packaged) and already references Microsoft.Windows.SDK.BuildTools.WinApp. Earlier commits in this PR hedged the claim down to "projects that use the Windows App SDK run support" on the strength of tools/Templates/.../Company.ReactorApp1.csproj setting WindowsPackageType=None -- a file this branch was still carrying from a merge base that predates its deletion, and which no longer exists on main. Restore the accurate statement: `dotnet new reactor` generates a packaged app and previewing it needs no project changes. Keep the one caveat that remains true, since packaging.md still documents a hand MSIX conversion that adds only MSIX properties: such a project must add the run-support package itself. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3c522596-4cdf-4ae1-981e-1fb5af52d4a2 --- CHANGELOG.md | 6 ++--- docs/_pipeline/templates/vs-extension.md.dt | 8 +++---- docs/guide/vs-extension.md | 8 +++---- .../Embed/ReactorChildLauncher.cs | 24 +++++++++---------- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6cbc90a..1b5843cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,9 +120,9 @@ Conventions for contributors: to keep it there. The VSIX's advertised minimum host moves to Visual Studio 17.14 to match that baseline. (PR #1282) -- **The Visual Studio preview now supports packaged (MSIX) apps** that use the Windows App - SDK run support (`Microsoft.Windows.SDK.BuildTools.WinApp`). Previewing one previously - failed. (PR #1282) +- **The Visual Studio preview now supports packaged (MSIX) apps.** `dotnet new reactor` + generates a packaged app, and previewing one previously failed. It now works with no + project changes. (PR #1282) - **Wrong code and guidance in the shipped agent-kit skills (spec 064 §4, issue #1275).** Three gesture snippets in `reactor-input` used WinUI's nested diff --git a/docs/_pipeline/templates/vs-extension.md.dt b/docs/_pipeline/templates/vs-extension.md.dt index d37f09b73..7a09b1e12 100644 --- a/docs/_pipeline/templates/vs-extension.md.dt +++ b/docs/_pipeline/templates/vs-extension.md.dt @@ -37,10 +37,10 @@ The component picker is a ComboBox in the tool window chrome. By default it auto ## Packaged (MSIX) projects -Packaged projects preview as-is when the project uses the Windows App SDK run support -(`Microsoft.Windows.SDK.BuildTools.WinApp`) — the preview supplies the execution-alias -setting that run support needs in order to inherit stdout. A project converted to MSIX by -hand needs that package added before the preview can attach. +Packaged projects work with no project changes: `dotnet new reactor` generates a packaged +app that carries the Windows App SDK run support, and the preview supplies the rest. A +project converted to MSIX by hand needs that run support +(`Microsoft.Windows.SDK.BuildTools.WinApp`) added before the preview can attach. To preview the app unpackaged instead, set `None`. diff --git a/docs/guide/vs-extension.md b/docs/guide/vs-extension.md index 624c7ed95..76e924175 100644 --- a/docs/guide/vs-extension.md +++ b/docs/guide/vs-extension.md @@ -28,10 +28,10 @@ The component picker is a ComboBox in the tool window chrome. By default it auto ## Packaged (MSIX) projects -Packaged projects preview as-is when the project uses the Windows App SDK run support -(`Microsoft.Windows.SDK.BuildTools.WinApp`) — the preview supplies the execution-alias -setting that run support needs in order to inherit stdout. A project converted to MSIX by -hand needs that package added before the preview can attach. +Packaged projects work with no project changes: `dotnet new reactor` generates a packaged +app that carries the Windows App SDK run support, and the preview supplies the rest. A +project converted to MSIX by hand needs that run support +(`Microsoft.Windows.SDK.BuildTools.WinApp`) added before the preview can attach. To preview the app unpackaged instead, set `None`. diff --git a/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs b/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs index 5bfaa3981..66e3d96d8 100644 --- a/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs +++ b/src/vs-reactor/Reactor.VsExtension/Embed/ReactorChildLauncher.cs @@ -151,25 +151,23 @@ private static ProcessStartInfo CreateStartInfo(StartOptions options) } /// - /// Lets a packaged (MSIX) project complete the devtools handshake, for projects that - /// use the Windows App SDK run support. + /// Lets a packaged (MSIX) project complete the devtools handshake. /// /// /// /// A packaged project launched by dotnet watch run goes through the launcher - /// that Microsoft.Windows.SDK.BuildTools.WinApp installs. That launcher - /// activates the app by AUMID, which is brokered and therefore has no stdout at - /// all — and stdout is how the child reports CAPTURE_PORT / - /// CAPTURE_TOKEN. The app would start and render while the preview silently - /// waited out its handshake timeout, with no error anywhere to explain it. Setting - /// this property switches the launcher to an execution alias, which inherits stdout. + /// that Microsoft.Windows.SDK.BuildTools.WinApp installs, which the Reactor + /// templates reference. That launcher activates the app by AUMID, which is brokered + /// and therefore has no stdout at all — and stdout is how the child reports + /// CAPTURE_PORT / CAPTURE_TOKEN. The app would start and render while + /// the preview silently waited out its handshake timeout, with no error anywhere to + /// explain it. Setting this property switches the launcher to an execution alias, + /// which inherits stdout. /// /// - /// This does not make every packaged project previewable on its own: a project that - /// does not reference that package has nothing to consume the property and must add - /// it first. Notably the in-repo Reactor template is unpackaged - /// (WindowsPackageType=None) and does not carry the package. See the VS - /// extension guide. + /// A project that does not carry that run support has nothing to consume this + /// property and must add the package first — a project converted to MSIX by hand is + /// the case in point. See the VS extension guide. /// /// /// Set unconditionally rather than behind a packaged/unpackaged probe: the WinApp