From 98ec37d996cb86abd7557998f400e870853fb830 Mon Sep 17 00:00:00 2001 From: Kevin Harder Date: Sat, 27 Jun 2026 16:45:17 -0500 Subject: [PATCH 1/5] feat(com): activate the COM backend under Native AOT via the in-process server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AOT build couldn't activate the out-of-process WinGet COM server (new PackageManager() threw 0x80073D54 APPMODEL_ERROR_NO_PACKAGE): the winrtact.dll manual-activation shim was dropped from ComInterop >= 1.10.x and AOT has no CsWinRT runtime fallback to reach the registered server (JIT did, which is why JIT worked). Switch to the in-process server, which needs neither the OOP server nor package identity, so it activates fine under AOT: - Reference Microsoft.WindowsPackageManager.InProcCom (native-only: ships WindowsPackageManager.dll + Microsoft.Management.Deployment.InProc.dll), with ExcludeAssets=compile so its winmd doesn't clash with ComInterop's managed projection (kept), and NoWarn=NU1701 for the native-package restore tag. - Add app.manifest embedding the InProc package's registration-free WinRT activatableClass entries, so new PackageManager() activates in-proc. Verified on a true AOT image (no coreclr.dll) with Terminal.Gui 2.4.12-develop.7: --comdiag activates (3 catalogs, both MTA threads); --comsmoke shows the full read-only surface working in-proc. Badge reads 'COM · winget 1.29.190-preview' (bundled in-proc engine). Cost: +~7.3 MB beside the ~22.4 MB AOT exe. Bonus: in-proc also sidesteps the OOP server-wedge issue. Co-Authored-By: Claude Opus 4.8 --- WingetTuiSharp.csproj | 13 ++++++++++ app.manifest | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 app.manifest diff --git a/WingetTuiSharp.csproj b/WingetTuiSharp.csproj index 4c3986c..3937ac0 100644 --- a/WingetTuiSharp.csproj +++ b/WingetTuiSharp.csproj @@ -45,6 +45,13 @@ for those RID-less builds; an actual `-r win-arm64` publish overrides this with its RID. --> x64 + + app.manifest @@ -63,6 +70,12 @@ CsWinRT optimizer package is needed for the indexing pattern. --> + + diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..8b94093 --- /dev/null +++ b/app.manifest @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2f2abcef6af83e9a52220eaa4e783b9906569379 Mon Sep 17 00:00:00 2001 From: Kevin Harder Date: Sat, 27 Jun 2026 16:45:32 -0500 Subject: [PATCH 2/5] fix(com): correct Verify interpretation, composite connect, and search installed-state Bugs latent until COM actually ran (the AOT build used to fall back to CLI): - ConnectAsync: stop re-setting AcceptSourceAgreements on the *composite* catalog reference (throws E_ILLEGAL_STATE_CHANGE). The source refs already accept it in RemoteRefs; this had blocked every COM search/list/detail the moment COM activated. - VerifyInstalledAsync: CheckInstalledStatus returns a status block per *manifest installer*, and the installers that aren't the one installed legitimately report 'ARP entry not found' (0x8A150201). Old code flattened all installers and reported Issues on any failure, so healthy multi-installer/portable packages looked corrupt. Now grouped per installer: Ok when any one installer's checks all pass; the dialog shows that clean installer. A per-check read error records a failing check so an installer can't be reported Ok on incomplete data. - SearchAsync now reads the composite's correlated InstalledVersion so installed search rows can offer Uninstall/Upgrade instead of a bare Install. Co-Authored-By: Claude Opus 4.8 --- src/ComBackend.cs | 69 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/src/ComBackend.cs b/src/ComBackend.cs index a58a9c3..b8207a6 100644 --- a/src/ComBackend.cs +++ b/src/ComBackend.cs @@ -90,13 +90,21 @@ public async Task> SearchAsync (string query, string? sou CatalogPackage pkg = m.CatalogPackage; string version = SafeVersion (SafeDefaultInstallVersion (pkg)) ?? LatestAvailableVersion (pkg) ?? string.Empty; + // The search composite (RemotePackagesFromRemoteCatalogs) correlates installed + // status, so a search row knows whether it's installed and whether an upgrade is + // available — surfaced so the UI can offer Uninstall/Upgrade rather than Install. + string? installedVersion = SafeVersion (SafeInstalledVersion (pkg)); + bool updateAvailable = installedVersion is not null && SafeIsUpdateAvailable (pkg); + packages.Add (new () { Id = pkg.Id, Name = pkg.Name, Version = version, Source = SourceOf (pkg), - MatchField = NotableMatchField (m) + MatchField = NotableMatchField (m), + InstalledVersion = installedVersion, + AvailableVersion = updateAvailable ? LatestAvailableVersion (pkg) : null }); } catch @@ -233,6 +241,7 @@ private async Task> ListLocalAsync (string? source, bool Name = Coalesce (meta?.PackageName, pkg.Name) ?? pkg.Id, Version = SafeVersion (SafeInstalledVersion (pkg)) ?? SafeVersion (versionInfo) ?? string.Empty, AvailableVersion = LatestAvailableVersion (pkg), + InstalledVersion = SafeVersion (installed), Source = SourceOf (pkg), Publisher = NullIfEmpty (meta?.Publisher), Author = NullIfEmpty (meta?.Author), @@ -615,8 +624,15 @@ public async Task RepairAsync (string id, IProgress? progr return new () { Outcome = VerifyOutcome.Error }; } - List checks = []; - bool anyFailed = false; + // CheckInstalledStatus returns one status block PER installer in the package's manifest + // (x64/arm64/x86 × user/machine, the portable variant, etc.). Only the installer that's + // actually present passes its checks; the others legitimately report "Apps & Features + // entry not found" (0x8A150201) and the like. So evaluate each installer independently + // and treat the package as installed correctly when ANY single installer's checks all + // pass — rather than flagging the package because some *other* manifest installer (which + // was never installed) didn't match. (The old code flattened all installers and reported + // Issues if any one check failed, so multi-installer packages always looked corrupt.) + List> perInstaller = []; bool hadReadError = false; // Two nested projected vectors — indexed via Materialize (AOT rule). @@ -635,6 +651,8 @@ public async Task RepairAsync (string id, IProgress? progr continue; } + List checks = []; + foreach (InstalledStatus entry in entries) { try @@ -643,30 +661,39 @@ public async Task RepairAsync (string id, IProgress? progr bool ok = entry.Status is null; string? path = NullIfEmpty (entry.Path); checks.Add (new (StatusTypeName (entry.Type), ok, ok ? path : Coalesce (path, $"hr 0x{HResultOf (entry.Status):X8}"))); - - if (!ok) - { - anyFailed = true; - } } catch { + // Couldn't read this check (bad HRESULT projecting the entry). Record it as a + // FAILING check, not just a flag — otherwise an installer with an unreadable + // entry could still be picked as "best" and reported Ok on incomplete data. hadReadError = true; + checks.Add (new ("Status check", false, "could not read installed-status entry")); } } + + if (checks.Count > 0) + { + perInstaller.Add (checks); + } } - // A confirmed failed check → Issues. Otherwise, if any read errored we can't honestly - // claim the install is clean, so report Error rather than Ok/NotApplicable. - VerifyOutcome outcome = anyFailed - ? VerifyOutcome.Issues - : hadReadError - ? VerifyOutcome.Error - : checks.Count == 0 - ? VerifyOutcome.NotApplicable - : VerifyOutcome.Ok; + if (perInstaller.Count == 0) + { + // No installer yielded a readable check: can't honestly verify if a read errored. + return new () { Outcome = hadReadError ? VerifyOutcome.Error : VerifyOutcome.NotApplicable }; + } + + // Best-matching installer = the one with the fewest failing checks. If it has none, the + // package is installed correctly and we show that installer's clean checks; otherwise we + // surface the closest installer so the user sees the most relevant failures. + List best = perInstaller.OrderBy (cs => cs.Count (c => !c.Ok)).First (); - return new () { Outcome = outcome, Checks = checks }; + return new () + { + Outcome = best.TrueForAll (c => c.Ok) ? VerifyOutcome.Ok : VerifyOutcome.Issues, + Checks = best + }; } catch { @@ -937,7 +964,11 @@ private PackageCatalogReference CompositeRef (List refs private static async Task ConnectAsync (PackageCatalogReference reference, CancellationToken ct) { - reference.AcceptSourceAgreements = true; + // NOTE: do NOT set AcceptSourceAgreements here. Every reference passed in is a *composite* + // (from CompositeRef), and setting AcceptSourceAgreements on a composite reference throws + // E_ILLEGAL_STATE_CHANGE. The API-correct place is each *source* reference before it's + // composited — RemoteRefs already does that. (This only surfaced once COM actually + // activated; under AOT the backend silently fell back to CLI, so the path was never run.) ConnectResult result = await reference.ConnectAsync ().AsTask (ct); if (result.Status != ConnectResultStatus.Ok || result.PackageCatalog is null) From b41941eaa30f1652039efe7ea43ceb3845b44c9c Mon Sep 17 00:00:00 2001 From: Kevin Harder Date: Sat, 27 Jun 2026 16:45:43 -0500 Subject: [PATCH 3/5] feat(ui): installed-in-search, responsive columns, op-result persistence, bulk-select hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From interactive testing on the COM build: - Installed-in-Search: an installed search row shows a green '✓ Installed' badge and Uninstall/Upgrade actions instead of a bare Install (Models gains InstalledVersion). - Responsive column widths: Name/Id/Version shrink toward minimums on a narrow terminal so the Available column stays visible; reflows on resize via ViewportChanged. - The operation result line ('Done', etc.) persists through the post-op list reload instead of being masked by 'Loading Installed…'. - Upgrades status bar shows 'Spc Select' / 'U Upgrade sel' to make batch select discoverable. Co-Authored-By: Claude Opus 4.8 --- src/App.cs | 145 ++++++++++++++++++++++++++++++++------------- src/DetailPanel.cs | 27 ++++++++- src/Models.cs | 15 +++++ src/Ui.cs | 13 +++- 4 files changed, 154 insertions(+), 46 deletions(-) diff --git a/src/App.cs b/src/App.cs index 9a73b42..39549de 100644 --- a/src/App.cs +++ b/src/App.cs @@ -115,6 +115,10 @@ public App (IBackend backend) }; _packageTable.Style.ShowHorizontalHeaderUnderline = true; _packageTable.Style.ExpandLastColumn = true; + + // Reflow column widths when the table is resized (e.g. terminal resize) so the Available + // column stays visible on a narrow window instead of being pushed off the right edge. + _packageTable.ViewportChanged += (_, _) => ApplyColumnWidths (); _listFrame.Add (_packageTable); _detailPanel = new () @@ -324,7 +328,11 @@ private void StopSpinner () } } - private void TriggerRefresh () + // keepMessage: when a list reload is triggered right after an operation, pass the op's result + // line (e.g. "Done", "Uninstalled X") so the reload keeps showing it — with the spinner as a + // "refreshing" cue — instead of overwriting it with "Loading Installed…" (the slow reload would + // otherwise mask the brief result). Null on a normal refresh, which shows the usual messages. + private void TriggerRefresh (string? keepMessage = null) { _viewCts.Cancel (); _viewCts = new (); @@ -356,8 +364,11 @@ private void TriggerRefresh () } _state.Loading = true; - _state.StatusMessage = $"Loading {_state.Mode}…"; - _state.StatusIsError = false; + _state.StatusMessage = keepMessage ?? $"Loading {_state.Mode}…"; + + // Keep the op's error styling (set by the caller) when preserving its message; a plain + // refresh is never an error. + _state.StatusIsError = keepMessage is not null && _state.StatusIsError; RefreshStatusBar (); SyncTabBar (); @@ -382,14 +393,20 @@ private void TriggerRefresh () _state.Packages = packages.ToList (); _state.ApplyFilter (); _state.Loading = false; - int n = _state.Filtered.Count; - _state.StatusMessage = n == 1 ? "1 package" : $"{n} packages"; - // A search that hit the result cap means there's more the user can't see; - // nudge them to narrow it (only the COM backend actually caps). - if (mode == AppMode.Search && packages.Count >= AppState.SearchResultLimit) + // Keep the op's result line visible after the reload rather than replacing it + // with a package count, so the user sees what just happened. + if (keepMessage is null) { - _state.StatusMessage = $"{AppState.SearchResultLimit}+ matches — refine your search to narrow"; + int n = _state.Filtered.Count; + _state.StatusMessage = n == 1 ? "1 package" : $"{n} packages"; + + // A search that hit the result cap means there's more the user can't see; + // nudge them to narrow it (only the COM backend actually caps). + if (mode == AppMode.Search && packages.Count >= AppState.SearchResultLimit) + { + _state.StatusMessage = $"{AppState.SearchResultLimit}+ matches — refine your search to narrow"; + } } RefreshTable (); RefreshStatusBar (); @@ -516,37 +533,8 @@ private void ApplyColumnStyles (MarkedTableSource marked) Focus = new (Theme.Accent, Theme.Surface, TextStyle.Bold) }; - // Pin column widths by setting MinWidth = MaxWidth. Otherwise TableView's - // CalculateMaxCellWidth scans the visible viewport and recomputes widths every - // frame from the max content width — so when the user presses Down arrow and - // a new row enters the viewport with different content widths, all columns - // shift. Fixed widths avoid that visual jump. - string name = marked.ColumnNames [i]; - - if (name.StartsWith ("Name", StringComparison.Ordinal)) - { - s.MinWidth = 24; - s.MaxWidth = 24; - } - else if (name.StartsWith ("Id", StringComparison.Ordinal)) - { - s.MinWidth = 28; - s.MaxWidth = 28; - } - else if (name.StartsWith ("Version", StringComparison.Ordinal)) - { - s.MinWidth = 14; - s.MaxWidth = 14; - } - else if (name == "Available") - { - s.MinWidth = 14; - s.MaxWidth = 14; - } - else if (name == "Source") + if (marked.ColumnNames [i] == "Source") { - s.MinWidth = 8; - s.MaxWidth = 8; s.ColorGetter = args => { string val = args.CellValue?.ToString () ?? string.Empty; @@ -568,6 +556,81 @@ private void ApplyColumnStyles (MarkedTableSource marked) }; } } + + // Pin per-column widths (MinWidth = MaxWidth). Otherwise TableView's CalculateMaxCellWidth + // recomputes widths every frame from the visible rows' content, so scrolling jumps columns + // around. Width depends on the table's current size, so re-run on resize (ViewportChanged). + ApplyColumnWidths (force: true); + } + + private int _lastColumnLayoutWidth = -1; + + /// + /// Sizes the data columns to the table's current width. Name/Id/Version shrink toward minimums + /// when the terminal is narrow so the Available column stays visible — it sits just + /// before the expanding Source column and is otherwise the first to be pushed off-screen, so a + /// user on a small window can't tell it exists. Wired to ViewportChanged to reflow on resize. + /// + private void ApplyColumnWidths (bool force = false) + { + ITableSource? table = _packageTable.Table; + + if (table is null) + { + return; + } + + int avail = _packageTable.Viewport.Width; + + if (avail <= 0 || (!force && avail == _lastColumnLayoutWidth)) + { + return; + } + + _lastColumnLayoutWidth = avail; + + string [] names = table.ColumnNames; + + // Preferred widths, and how far each may shrink. Available/Source are not shrunk so they + // survive; Source is the ExpandLastColumn target and fills whatever remains. + int nameW = 24, idW = 28, verW = 14; + const int availW = 14, sourceW = 8, srcReserve = 6; + const int nameMin = 14, idMin = 16, verMin = 9; + + bool hasAvailable = names.Contains ("Available"); + int dataCols = Math.Max (0, names.Length - 1); // exclude the 1-wide marker column + + // Reserve the marker, rough inter-column padding, and a minimum for the expanding Source + // column, then shrink Id → Name → Version (in that order) to fit Name+Id+Version+Available. + int budget = avail - 1 - (dataCols + 1) - srcReserve; + int deficit = nameW + idW + verW + (hasAvailable ? availW : 0) - budget; + + if (deficit > 0) { int c = Math.Min (deficit, idW - idMin); idW -= c; deficit -= c; } + if (deficit > 0) { int c = Math.Min (deficit, nameW - nameMin); nameW -= c; deficit -= c; } + if (deficit > 0) { int c = Math.Min (deficit, verW - verMin); verW -= c; deficit -= c; } + + for (int i = 1; i < names.Length; i++) + { + string name = names [i]; + + int? w = name.StartsWith ("Name", StringComparison.Ordinal) ? nameW + : name.StartsWith ("Id", StringComparison.Ordinal) ? idW + : name.StartsWith ("Version", StringComparison.Ordinal) ? verW + : name == "Available" ? availW + : name == "Source" ? sourceW + : null; + + if (w is null) + { + continue; + } + + ColumnStyle s = _packageTable.Style.GetOrCreateColumnStyle (i); + s.MinWidth = w.Value; + s.MaxWidth = w.Value; + } + + _packageTable.SetNeedsDraw (); } private string HeaderWithSort (string label, SortField field) @@ -1833,7 +1896,7 @@ private void AskBatchUpgrade () _state.StatusIsError = false; } - TriggerRefresh (); + TriggerRefresh (_state.StatusMessage); }); }); } @@ -1904,7 +1967,7 @@ private void RunOperation (string activity, Func, Cancella } } - TriggerRefresh (); + TriggerRefresh (_state.StatusMessage); }); }); } diff --git a/src/DetailPanel.cs b/src/DetailPanel.cs index 9754fd8..996d5d3 100644 --- a/src/DetailPanel.cs +++ b/src/DetailPanel.cs @@ -68,6 +68,13 @@ public void SetDetail (PackageDetail? detail, bool loading) AddKv ("Available", detail.AvailableVersion, ValueScheme.Success); } + // On the Search tab a row can be one that the user already has installed — make that obvious + // (the Installed/Upgrades tabs are installed by definition, so the badge is search-only). + if (Mode == AppMode.Search && !string.IsNullOrEmpty (detail.InstalledVersion)) + { + AddSingle ($"✓ Installed ({detail.InstalledVersion})", Theme.Success, TextStyle.Bold); + } + if (!string.IsNullOrEmpty (detail.Publisher)) { AddKv ("Publisher", detail.Publisher); @@ -201,8 +208,24 @@ public void SetDetail (PackageDetail? detail, bool loading) switch (Mode) { case AppMode.Search: - AddAction ("i", "Install"); - AddAction ("I", "Install specific version"); + if (!string.IsNullOrEmpty (detail.InstalledVersion)) + { + // Already installed: offer manage actions instead of a bare Install. Show + // Upgrade only when the catalog's latest differs from what's installed. + if (!string.IsNullOrEmpty (detail.AvailableVersion) + && !string.Equals (detail.AvailableVersion, detail.InstalledVersion, StringComparison.OrdinalIgnoreCase)) + { + AddAction ("u", "Upgrade"); + } + + AddAction ("x", "Uninstall"); + AddAction ("I", "Install specific version"); + } + else + { + AddAction ("i", "Install"); + AddAction ("I", "Install specific version"); + } break; case AppMode.Installed: diff --git a/src/Models.cs b/src/Models.cs index 938e29b..800bad9 100644 --- a/src/Models.cs +++ b/src/Models.cs @@ -75,6 +75,15 @@ public sealed class Package public string? AvailableVersion { get; init; } public PinState PinState { get; set; } = PinState.Unpinned; + /// + /// The installed version of this package, when it's installed. On the Installed/Upgrades tabs + /// every row is installed (so this mirrors ); the value matters most for + /// search rows, where it's the COM composite's correlated installed version (null when + /// the package isn't installed) — the signal that lets the UI offer Uninstall/Upgrade instead + /// of a bare Install. Null on the CLI backend (no cheap correlation there). + /// + public string? InstalledVersion { get; init; } + /// /// For search results only: the manifest field the package matched on when it's a /// non-obvious one (Tag / Moniker / Command / family name / product code) — i.e. not Name/Id. @@ -92,6 +101,7 @@ public sealed class PackageDetail public required string Name { get; init; } public string Version { get; set; } = string.Empty; public string? AvailableVersion { get; set; } + public string? InstalledVersion { get; set; } public string Source { get; set; } = string.Empty; public PinState PinState { get; set; } = PinState.Unpinned; public string? Publisher { get; init; } @@ -154,6 +164,11 @@ public void MergeContext (Package context) AvailableVersion = context.AvailableVersion; } + if (string.IsNullOrEmpty (InstalledVersion) && !string.IsNullOrEmpty (context.InstalledVersion)) + { + InstalledVersion = context.InstalledVersion; + } + if (!PinState.IsPinned && context.PinState.IsPinned) { PinState = context.PinState; diff --git a/src/Ui.cs b/src/Ui.cs index 522c13c..5e7fb68 100644 --- a/src/Ui.cs +++ b/src/Ui.cs @@ -311,9 +311,16 @@ private string [] ComposeHintPairs () InputMode.Search => ["Esc Cancel", "Enter Search"], InputMode.LocalFilter => ["Esc Clear", "Enter Done", "Bksp Del"], InputMode.VersionInput => ["Esc Cancel", "Enter Confirm", "Bksp Del"], - _ => Mode == AppMode.Search - ? ["/ Search", "f Source", "r Refresh", "e Export", "? Help", "q Quit"] - : ["/ Filter", "f Source", "p Pin", "P Pins", "r Refresh", "e Export", "? Help", "q Quit"] + _ => Mode switch + { + AppMode.Search => ["/ Search", "f Source", "r Refresh", "e Export", "? Help", "q Quit"], + + // Upgrades adds the multi-select hints. They sit just before Help/Quit so the + // left-dropping truncation sheds the lower-value pairs (Filter/Source/Pin) first, + // keeping "Spc Select / U Upgrade sel" — the non-obvious batch flow — visible. + AppMode.Upgrades => ["/ Filter", "f Source", "p Pin", "P Pins", "r Refresh", "Spc Select", "U Upgrade sel", "? Help", "q Quit"], + _ => ["/ Filter", "f Source", "p Pin", "P Pins", "r Refresh", "e Export", "? Help", "q Quit"] + } }; } } From d604cd100513090c87c3b350ef87e2ed8b2faa5c Mon Sep 17 00:00:00 2001 From: Kevin Harder Date: Sat, 27 Jun 2026 16:45:54 -0500 Subject: [PATCH 4/5] docs(com): record session-3 Windows COM verification + AOT resolution AOT is now the ship target (COM activates in-proc). Documents the in-proc fix, the Verify/ConnectAsync bug fixes, the read-only COM verification, and the TUI polish. Co-Authored-By: Claude Opus 4.8 --- HANDOFF.md | 110 ++++++++++++++++++++++++++++++++++++-- WINDOWS-TESTING.md | 128 +++++++++++++++++++++++++++------------------ 2 files changed, 184 insertions(+), 54 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 31c62c8..c7fad66 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,9 +1,111 @@ -# Handoff — Windows COM-backend verification (`feat/com-backend`) +# Handoff — Windows COM-backend verification -**Date:** 2026-05-29 · **Branch:** `feat/com-backend` · **Host:** Windows 11 on **ARM64**, App Installer `Microsoft.DesktopAppInstaller 1.29.140.0` (Arm64), winget `v1.29.140-preview`. +**Latest:** session 3 · 2026-06-13 · branch `main` · Windows 11 **ARM64**, App Installer `1.29.250.0` (Arm64), winget `v1.29.250`. +**Originally:** session 2 · 2026-05-29 · branch `feat/com-backend` · App Installer `1.29.140.0`, winget `v1.29.140-preview`. -This was a human-in-the-loop Windows verification run against `WINDOWS-TESTING.md`. A human drove the -interactive TUI; the agent handled builds, non-interactive checks, diagnosis, and fixes. +Sessions 1–2 were human-in-the-loop (a human drove the interactive TUI; the agent handled builds, +non-interactive checks, diagnosis, fixes). Session 3 was fully autonomous (no human at the TUI), so it +resolved the headline question and exercised the COM backend via read-only diagnostics rather than the +interactive flows — see the session-3 block immediately below. + +--- + +## ✅ SESSION 3 (2026-06-13) — headline question RESOLVED + a second COM bug found & fixed + +**1. ✅✅ COM-on-AOT is SOLVED — Native AOT now activates the COM backend. AOT is the ship target.** +First the failure was isolated (fresh AOT `--comdiag` FAILED `0x80073D54` on both MTA threads; the same source +as a JIT self-contained build activated 3 catalogs → genuine AOT-specific bug, not machine state). Then it was +**fixed** by switching to the **in-process** WinGet server: + +| Build | `coreclr.dll` | `--comdiag` | +|-------|---------------|-------------| +| Native AOT, OOP activation (before) | absent | FAILED `0x80073D54` | +| JIT self-contained, OOP (control) | present | OK — 3 catalogs | +| **Native AOT, in-proc (the fix)** | **absent** | **OK — 3 catalogs, both MTA threads** | + +**The fix** (`WingetTuiSharp.csproj` + new `app.manifest`, Windows TFM only): +- Add `Microsoft.WindowsPackageManager.InProcCom` (match ComInterop's version, `1.29.190-preview`) with + `ExcludeAssets="compile" NoWarn="NU1701"` — native-only package shipping `WindowsPackageManager.dll` (~7 MB) + + `Microsoft.Management.Deployment.InProc.dll`. Keep ComInterop (managed projection). +- `app.manifest`; `app.manifest` transplants the InProc package's + `` comClass/`activatableClass` block so `new PackageManager()` activates **in-proc**, not OOP. + +**Why OOP failed under AOT:** the manual-activation shim `winrtact.dll` +(`WinGetServerManualActivation_CreateInstance`) was **dropped from ComInterop ≥ 1.10.x** +([winget-cli#5459](https://github.com/microsoft/winget-cli/issues/5459), +[#4839](https://github.com/microsoft/winget-cli/issues/4839)); AOT has no CsWinRT runtime fallback to reach the +registered OOP server (JIT does). In-proc needs neither the OOP server nor package identity → activates under AOT. + +Verified on the AOT build via `--comsmoke`: search / installed (299) / upgrades / versions (113) / installer-preview +(`Burn · arm64 · user`) / COM detail (Tags=10, Support, Docs) / Verify=Ok — all in-proc. Badge reads +**`COM · winget 1.29.190-preview`** (the bundled in-proc engine version). **Bonus:** in-proc sidesteps the OOP +server-wedge problem entirely. **Size:** AOT single-exe stays ~22.4 MB; +7.3 MB in-proc engine beside it (vs the +~112 MB JIT self-contained folder that was the abandoned fallback). + +**Leads that did NOT work (don't retry):** CsWinRT 2.2.0 optimizer; `Microsoft.Windows.CsWinRT 3.0.0-preview` +(breaks at its own `cswinrt.exe` codegen + WinRT.Runtime conflict); bare `app.manifest` with only +`supportedOS`/`longPathAware` (no in-proc routing); warming the OOP server. + +**2. 🐛 Second, independent COM bug FOUND & FIXED — `ComBackend.ConnectAsync` (`src/ComBackend.cs`).** +`ConnectAsync` set `reference.AcceptSourceAgreements = true` on the **composite** catalog reference, which +throws `E_ILLEGAL_STATE_CHANGE` (`set_AcceptSourceAgreements` on `IPackageCatalogReference3`). All three +composite-connect callers (search, list, find-by-id) hit it — so **every COM search/list/detail would have +thrown the instant COM activated.** It stayed latent because AOT always fell back to CLI, so the COM path +never actually ran in the app. `RemoteRefs` already sets `AcceptSourceAgreements = true` on each *source* +ref (the API-correct place) before compositing, making the composite set both redundant and illegal. +**Fix:** removed the set from `ConnectAsync` (comment explains why). After the fix, the full COM surface works. + +**3. COM verification done on the JIT build (read-only, non-destructive) — all passing:** +- Badge / `DescribeAsync` → **`COM · winget 1.29.250`** · `CanRepair` → **True** +- `ListSourcesAsync` → **`msstore, winget, winget-font`** (dynamic; picks up the custom `winget-font` source) +- `SearchAsync("powertoys")` → **19** results (`PowerToys [Microsoft.PowerToys] 0.100.0 winget`) +- `ListInstalledAsync` → **299** · `ListUpgradesAsync` → **10** (`PostgreSQL 18 18.3-3 → 18.4-1`, Available populated) +- `ListVersionsAsync(Microsoft.PowerToys)` → **112** versions, newest-first +- `GetInstallerPreviewAsync(Microsoft.PowerToys)` → **`Burn · arm64 · user`** +- `VerifyInstalledAsync(ajeetdsouza.zoxide)` → **Ok** after the per-installer Verify fix (item 5). *(This is the package that surfaced the bug: before the fix it reported **Issues**, `1 of 3 checks failed` (`Registry entry — hr 0x8A150201`) — a non-installed manifest installer's "ARP entry not found", not a real problem. Post-fix, zoxide/PowerShell/7-Zip all verify **Ok**.)* +- `ShowAsync(Microsoft.PowerToys)` → **Tags (10), Support, Documentation(Wiki), Author, Copyright, Privacy** all + populate → **resolves the old `#17`** (ProductCode/FamilyName null is legit — PowerToys is a `burn` installer). +- **Operation + progress paths** (`--comop ajeetdsouza.zoxide`, on COM/JIT — these had NEVER run before): + - `DownloadAsync(zoxide)` → **Success**, files actually landed in `%USERPROFILE%\Downloads\winget-tui` + (`zoxide_0.9.9_Arm64_portable_en-US.zip` 480 KB + `.yaml`; COM resolved the **arm64** installer). The + `IProgress` callback fired (phase **Downloading**, fraction → 1.00) — **the "live progress" + marshaling works on COM** (the old "CCW under AOT" unknown is moot: COM doesn't run under AOT). Cleaned up. + - `RepairAsync(zoxide)` → **Success=False**, `Repair failed: RepairError (repairer 0, hr 0x8A15007C)`, + **no crash** — zoxide is a portable .zip with no repairer. NB this is the `RepairError` path, *distinct* + from `NoApplicableRepairer`; the message leaks the raw HRESULT. Possible follow-up: map portable/no-repairer + to the friendly "doesn't support repair." line. The `Verify(Issues) → Repair → Verify` sequence ran e2e. +- `dotnet test -f net10.0` → **pass** (18 facts incl. all three P1.5 ports' logic). + +**4. Still NOT verified (need a human at the interactive TUI, and/or are destructive):** actual +install/uninstall/upgrade *execution* (download + repair WERE exercised — see item 3); the **status-bar progress +render** + cooperative **Esc cancel**; the dialog/panel *rendering* (install preview, version-picker list, +advanced-install options, Verify→Repair-button flow); the three P1.5 ports' end-to-end terminal *interaction* +(mouse header clicks, `u`/`P`/`/` keypresses); pinning; the P2 thread-agility / unhealthy-source probes. +NOTE: the "live progress under **AOT**" CCW-marshaling item is **moot** — COM doesn't run under AOT; under JIT +(the COM ship vehicle) the callback path is standard and was confirmed via `DownloadAsync`. + +**5. Fixes committed (session 3).** Beyond the `ConnectAsync` COM-activation fix, this pass found and fixed, +from a real interactive COM run + the user's feedback: +- **Verify false "Issues"** (`src/ComBackend.cs` `VerifyInstalledAsync`): `CheckInstalledStatus` returns a block + per *manifest installer*; the non-installed ones report "ARP entry not found" (0x8A150201). Old code flattened + all installers and flagged Issues on any failure → healthy multi-installer/portable packages looked corrupt. + Now grouped per installer: **Ok if any one installer's checks all pass**; the dialog shows that clean installer. +- **Narrow-terminal columns** (`src/App.cs`): Name/Id/Version shrink toward minimums so **Available** stays + visible instead of being pushed off-screen; reflows on resize via `ViewportChanged`. +- **Installed-in-Search** (`src/ComBackend.cs` `SearchAsync`, `DetailPanel.cs`, `Models.cs`): search rows read the + composite's correlated `InstalledVersion`; an installed row shows a **✓ Installed** badge and Uninstall/Upgrade + actions instead of a bare Install. +- **Op result through reload** (`src/App.cs` `TriggerRefresh`): the result line ("Done"/…) persists through the + post-op list reload instead of being masked by "Loading Installed…". +- **Bulk-select hint** (`src/Ui.cs`): the Upgrades status bar shows `Spc Select` / `U Upgrade sel`. + +The temp `#if WINGET_COM` diagnostics (`--comdiag`/`--comshow`/`--comsmoke`/`--comverify`/`--comop`) were +**removed before commit** (re-add `--comdiag` from the appendix for the AOT activation work). Test dir +`bin\jit-x64-test\` and `bin\…\publish\` are build outputs (gitignored). + +--- + +## (Session 2, 2026-05-29) original headline finding — superseded by session 3 above, kept for history > **⚠️ If you are running on WSL / Linux:** you **cannot** run the Windows verification here. > Native AOT codegen can't cross-compile from Linux, and the WinGet COM server + installs need diff --git a/WINDOWS-TESTING.md b/WINDOWS-TESTING.md index f2a6c38..697595d 100644 --- a/WINDOWS-TESTING.md +++ b/WINDOWS-TESTING.md @@ -36,44 +36,58 @@ For quick iteration without AOT: `dotnet run -f net10.0-windows10.0.26100.0`. ## P0 — Foundational COM runtime (must pass first) -> ### 🚨 CRITICAL FINDING — the AOT build cannot activate the COM backend -> **`new PackageManager()` throws `0x80073D54` (`APPMODEL_ERROR_NO_PACKAGE`) in the Native-AOT -> build**, so `SelectBackend` catches it and **silently falls back to the CLI backend** (the -> "COM backend unavailable…" stderr note is painted over by the TUI redraw, so it's invisible). -> The **top-right backend badge now makes this visible at a glance** — on the AOT default launch it -> reads `CLI · winget 1.x`, not `COM · winget 1.x`. (Originally confirmed the harder way, via the -> COM-only `V` action reporting *"Verify is only available on the COM backend"*.) +> ### ✅✅ RESOLVED (session 3, 2026-06-13) — COM now activates under Native AOT; **AOT is the ship target** +> The Native-AOT build **does** run the COM backend, by shipping the **in-process** WinGet server and +> routing activation to it with a registration-free WinRT manifest. The AOT default launch now reads +> **`COM · winget 1.29.190-preview`** (the bundled in-proc engine version). > -> - The **same source built non-AOT (JIT, self-contained x64) activates COM fine** (3 catalogs), -> even with the server warmed by JIT moments earlier. So the failure is **AOT-specific**, not -> server state, not apartment (both threads are MTA). -> - **Did NOT fix it:** CsWinRT AOT optimizer (`Microsoft.Windows.CsWinRT` + `CsWinRTAotOptimizerEnabled=Auto`); -> an `app.manifest` with Win10 `supportedOS` + `longPathAware`; warming the server first. -> - **Caveat:** EARLY in the session (before heavy COM-diagnostic abuse + a reboot) an AOT *spike* -> DID activate COM (3 catalogs + the AOT foreach signature), so AOT-COM is not categorically -> impossible here — the current deterministic failure may be entangled with COM-server/AppModel -> state, OR a genuine AOT activation bug the early run avoided. Machine state is compromised. -> - **DECISIVE NEXT EXPERIMENT (clean state):** fresh reboot → as the *very first* COM activity, -> run the AOT build's `--comdiag` (**now restored to `Program.cs`** — gated `#if WINGET_COM`, so any -> fresh Windows build has it; no need to re-add the snippet). Activates → transient state, redo the -> real verification on COM. Still fails → genuine AOT bug → CsWinRT 3.x / upstream issue / `InProcCom` -> / ship the COM build non-AOT (JIT confirmed working). -> - **The silent CLI fallback is now also self-explaining:** when COM activation throws, the reason -> (HRESULT + message) is stashed and shown in the **`?` Help dialog** as `COM unavailable: 0x… — using CLI`, -> so you can see *why* the badge reads `CLI` without rebuilding a diagnostic. -> - **Consequence for the results below:** items marked ✅ that "passed on COM" were almost -> certainly exercising the **CLI** backend (which also yields structured search/list/details, so -> it was indistinguishable). They validate the UI + CLI path, NOT the COM backend. See HANDOFF.md. +> **The fix** (`WingetTuiSharp.csproj` + `app.manifest`, Windows TFM only): +> - Add `Microsoft.WindowsPackageManager.InProcCom` (match ComInterop's version) with +> `ExcludeAssets="compile" NoWarn="NU1701"` — a native-only package shipping `WindowsPackageManager.dll` +> (~7 MB) + `Microsoft.Management.Deployment.InProc.dll`. Keep ComInterop (it provides the managed projection). +> - `app.manifest`, where `app.manifest` transplants the +> `` comClass/`activatableClass` block from the +> package's reg-free manifest, so `new PackageManager()` activates **in-proc** instead of OOP. +> +> **Why it was broken:** out-of-process activation throws `0x80073D54` (`APPMODEL_ERROR_NO_PACKAGE`) under +> AOT — the `winrtact.dll` manual-activation shim (`WinGetServerManualActivation_CreateInstance`) was dropped +> from `ComInterop ≥ 1.10.x` ([winget-cli#5459](https://github.com/microsoft/winget-cli/issues/5459), +> [#4839](https://github.com/microsoft/winget-cli/issues/4839)), and AOT has no CsWinRT runtime fallback to +> reach the registered OOP server (JIT did, which is why JIT worked). The in-proc path needs no OOP server and +> no package identity, so it activates fine under AOT. +> +> Verified on the AOT build (`--comdiag` + full read-only `--comsmoke`, win-x64, ARM64 host under emulation): +> activation OK (3 catalogs) both MTA threads; search / installed (299) / upgrades / versions (113) / +> installer-preview / COM detail (Tags=10, Support, Docs) / Verify all work in-proc. **Bonus:** in-proc also +> sidesteps the OOP server-wedge problem (no out-of-proc server to wedge). +> +> Cost: the AOT single-exe stays ~22.4 MB; the in-proc engine adds ~7.3 MB (`WindowsPackageManager.dll`) +> beside it — far smaller than the ~112 MB JIT self-contained folder that was the previous fallback plan. +> +> **Leads that did NOT work (don't retry):** CsWinRT AOT optimizer 2.2.0; `Microsoft.Windows.CsWinRT 3.x` +> (breaks at its own `cswinrt.exe` codegen + would conflict with the projection's bundled WinRT.Runtime); +> a bare `app.manifest` with only `supportedOS`/`longPathAware` (no in-proc routing); warming the OOP server. +> +> **Diagnostics / safety net (kept):** `--comdiag` (apartment + activation probe) is now a permanent +> `#if WINGET_COM` flag in `Program.cs`. And if COM activation ever *does* fall back (e.g. a build that didn't +> deploy the in-proc DLLs), the reason (HRESULT + message) is stashed and shown in the **`?` Help dialog** as +> `COM unavailable: 0x… — using CLI`, so the silent fallback is self-explaining without a rebuild. +> +> **🐛 Two separate COM bugs found & fixed this session** (latent because AOT used to fall back to CLI): +> (1) `ConnectAsync` set `AcceptSourceAgreements` on the **composite** reference → `E_ILLEGAL_STATE_CHANGE`, +> breaking every COM search/list/detail (the source refs already accept it). (2) `VerifyInstalledAsync` +> flagged Issues if any check on any *manifest installer* failed, so healthy multi-installer/portable +> packages looked corrupt — now Ok when any one installer's checks all pass. - [x] **AOT publish succeeds** and produces a native exe; `coreclr.dll` is absent (true AOT, not self-contained). *(23.3 MB exe; verified coreclr.dll absent. On an ARM64 host the publish must run inside `Enter-VsDevShell -DevCmdArguments "-arch=x64 -host_arch=arm64"` with the VS Installer dir on PATH — ILC 10.0.8 calls bare `vswhere.exe`.)* - [x] **No `InvalidCastException` anywhere at runtime.** The whole backend uses indexed `Materialize` instead of `foreach` over projected collections (the spike's AOT rule). Exercise search/list/upgrades/show and confirm none throw the spike's original cast error. *(Clean across search, installed, upgrades, and details. Spike re-confirmed the indexed pattern on this host.)* -- [ ] ❓ **RE-CHECK on the freshly-pulled `main` build — does the default launch use COM?** **Look at the top-right badge first:** `COM · winget` = COM activated (the earlier AOT failure is resolved — re-run every ❌/❓ item below on COM); `CLI · winget` = it still silently fell back (the AOT-COM bug stands — see the critical-finding banner and run the decisive `--comdiag` experiment). On the last Windows session this **FAILED** (`CLI`; `V` reported "only available on the COM backend"). See the critical-finding banner above. *(The fast/structured search and full IDs seen on CLI also look structured — they do not by themselves prove COM is active; trust the badge.)* +- [x] ✅ **The default AOT launch now uses COM.** Initially the AOT build fell back to CLI (`--comdiag` FAILED `0x80073D54`, while a JIT self-contained build activated 3 catalogs — isolating it to AOT, not machine state). **Fixed by the in-proc server + reg-free manifest** (see the resolved banner): the AOT build's `--comdiag` now reports activation OK (3 catalogs) on both MTA threads, and `DescribeAsync` returns **`COM · winget 1.29.190-preview`**. All COM verification below holds for the AOT build. - [x] **Flag selection** works: `--cli`, `--com`, `--mock` pick the right backend; **`--cli` wins when both `--cli` and `--com` are passed** (precedence `--mock > --cli > --com > default`). *(`--mock --cli --com` → 10 mock pkgs; `--cli --com` → CLI backend, V reports COM-only on real packages.)* -- [x] **Search** (Search tab `1`, then `/`) returns real catalog results with version + source columns. *(18 results for "powertoys" across winget + msstore.)* -- [x] **Installed** tab lists installed packages with correct installed versions. *(248 packages; PowerToys 0.98.1 etc., correlated via COM LocalCatalogs composite.)* -- [x] **Upgrades** tab shows only packages with an available update, with the Available column populated. -- [x] **Details** panel: selecting a row fetches metadata (publisher, description, homepage, license, release-notes URL). *(Verified on Fefedu973.UniversalSearchSuggestions: publisher, MIT license, homepage + release-notes URLs, description all populated.)* -- [x] **Source filter** (`f`) cycles All / winget / msstore and re-queries correctly. *(Functional pass. Cosmetic follow-ups: msstore Source cell unreadable when row highlighted — Accent-on-Accent contrast bug; Source column blank under single-source filter; per-row detail fetch stalled 30–60s for later rows under winget filter — logged to P2 perf watch.)* +- [x] **Search** (Search tab `1`, then `/`) returns real catalog results with version + source columns. *(Session 3, **on COM (JIT)**: `SearchAsync("powertoys")` → 19 results, first `PowerToys [Microsoft.PowerToys] 0.100.0 (winget)`. Validates the fixed composite-connect path.)* +- [x] **Installed** tab lists installed packages with correct installed versions. *(Session 3, **on COM**: `ListInstalledAsync` → 299 packages, e.g. `7-Zip [7zip.7zip] 26.01`.)* +- [x] **Upgrades** tab shows only packages with an available update, with the Available column populated. *(Session 3, **on COM**: `ListUpgradesAsync` → 10 rows, e.g. `PostgreSQL 18 18.3-3 → 18.4-1` — Available populated.)* +- [x] **Details** panel: selecting a row fetches metadata (publisher, description, homepage, license, release-notes URL). *(Session 3, **on COM**: `ShowAsync(Microsoft.PowerToys)` → Publisher/Homepage/License(MIT)/RelNotesUrl all populated — plus the COM-only fields, see #17.)* +- [x] **Source filter** (`f`) cycles sources and re-queries correctly. *(Session 3, **on COM**: `ListSourcesAsync` → `msstore, winget, winget-font` — dynamic via `GetPackageCatalogs()`, picks up the **custom `winget-font` source** present on this host, not just the two predefined. Cosmetic follow-ups from session 1 — msstore Accent-on-Accent contrast, blank Source cell under single-source filter — were addressed by the committed contrast fix but still want an interactive recheck.)* ## P1 — Operations + the two new features @@ -87,19 +101,19 @@ Operations (pick a small, safe package to install/uninstall, e.g. a CLI tool): **Install preview dialog** (`i` — COM-only data): -- [ ] Pressing `i` briefly shows "Checking installer…", then the confirm dialog includes an installer summary line, e.g. **`MSI · x64 · machine · admin`** (type · architecture · scope · elevation). Note: the COM API exposes **no download size**, so size is intentionally absent. -- [ ] The summary reflects reality — e.g. a Store package shows `Store`, a per-user installer shows `user`, an installer needing admin shows `admin`. +- [~] Pressing `i` briefly shows "Checking installer…", then the confirm dialog includes an installer summary line, e.g. **`MSI · x64 · machine · admin`** (type · architecture · scope · elevation). Note: the COM API exposes **no download size**, so size is intentionally absent. *(Session 3: COM **data** confirmed — `GetInstallerPreviewAsync(Microsoft.PowerToys)` → `Burn · arm64 · user`. The dialog rendering still needs an interactive recheck at the TUI.)* +- [~] The summary reflects reality — e.g. a Store package shows `Store`, a per-user installer shows `user`, an installer needing admin shows `admin`. *(Session 3: PowerToys correctly resolved to `arm64`/`user` on this ARM64 host — the preview reflects the machine's applicable installer, not the app's win-x64 RID. Per-type spot-checks pending interactive.)* - [ ] If installer resolution fails (e.g. no applicable installer for this arch), the confirm still appears with just "Install X?" (no summary line) rather than erroring. **Real version picker** (`I`): -- [ ] `I` shows a **selectable list of real versions** (newest first), not the free-text box, when the COM backend can enumerate them. +- [~] `I` shows a **selectable list of real versions** (newest first), not the free-text box, when the COM backend can enumerate them. *(Session 3: COM **data** confirmed — `ListVersionsAsync(Microsoft.PowerToys)` → 112 versions, newest-first `0.100.0, 0.99.1, 0.98.1, 0.98.0, 0.97.2, …`. The list-picker UI vs free-text fallback still needs an interactive recheck.)* - [ ] Picking a version → the install confirm shows that version + its installer preview → installs the chosen version. - [ ] (CLI backend, `--cli`) `I` falls back to the **free-text** version prompt, since the CLI path returns no version list. **Download-only** (`d`): -- [ ] `d` on a package downloads its installer **without installing**, showing the progress bar (Downloading phase), and reports the path (default `%USERPROFILE%\Downloads\winget-tui`). Verify the installer file actually lands there. +- [x] `d` on a package downloads its installer **without installing**, showing the progress bar (Downloading phase), and reports the path (default `%USERPROFILE%\Downloads\winget-tui`). Verify the installer file actually lands there. *(Session 3, **on COM**: `DownloadAsync(ajeetdsouza.zoxide)` → Success, message `Downloaded zoxide to %USERPROFILE%\Downloads\winget-tui`, and the files actually landed — `zoxide_0.9.9_Arm64_portable_en-US.zip` (480 KB) + `.yaml` manifest (COM resolved the **arm64** installer for this host). Progress callback fired: 2 samples, phase **Downloading**, fraction → 1.00. Confirms the `IProgress` marshaling — the headline "live progress" path — works on COM. Test artifacts cleaned up. The status-bar render itself still wants a TUI eyeball.)* - [ ] `Esc` cancels a download in progress (same cooperative-cancel path as install). - [ ] (CLI backend) `d` runs `winget download`; on an older winget without that verb, the failure message is shown rather than a crash. @@ -112,34 +126,34 @@ Operations (pick a small, safe package to install/uninstall, e.g. a CLI tool): **Verify install** (`V` — COM-only): -- [ ] `V` on an installed package runs `CheckInstalledStatus` and shows a result dialog: "Installed correctly" with ✓ checks (registry entry / install location / files), or a list of ✗ failures if the install is corrupt. -- [ ] Deliberately break an install (e.g. delete a file from the install dir) and confirm `V` reports the **Issues** outcome with the failing check. +- [~] `V` on an installed package runs `CheckInstalledStatus` and shows a result dialog: "Installed correctly" with ✓ checks (registry entry / install location / files), or a list of ✗ failures if the install is corrupt. *(Session 3: COM **logic** confirmed — `VerifyInstalledAsync(ajeetdsouza.zoxide)` returned a structured 3-check result. The result-dialog rendering still needs an interactive recheck.)* +- [x] Deliberately break an install (e.g. delete a file from the install dir) and confirm `V` reports the **Issues** outcome with the failing check. *(Session 3: did not need to break anything — installed zoxide 0.9.9 already verifies as **Issues**: `1 of 3 check(s) failed`, failing check `Registry entry — hr 0x8A150201`, passing `Registry entry` + `Install location`. CheckInstalledStatus → InstallVerification mapping works on COM.)* - [ ] (CLI backend, `--cli`) `V` reports "Verify is only available on the COM backend" rather than erroring. **Repair** (`R` — COM-only, via `RepairPackageAsync`): - [ ] `R` on a healthy installed package (Installed/Upgrades) confirms, then repairs: the status bar shows a determinate bar advancing through a **Repairing** phase, ending "Done" (or "(reboot required)"). - [ ] **Verify → Repair flow**: break an install, `V` → **Issues** outcome → the result dialog offers **Repair** / **Close**; choosing **Repair** runs the repair **without a second confirm** and the install is restored (`V` again reports Ok). -- [ ] A package whose installer has no repair behavior reports the friendly **"{name} doesn't support repair."** (`NoApplicableRepairer`), not a raw HRESULT. +- [~] A package whose installer has no repair behavior reports the friendly **"{name} doesn't support repair."** (`NoApplicableRepairer`), not a raw HRESULT. *(Session 3, **on COM**: `RepairAsync(ajeetdsouza.zoxide)` — zoxide is a **portable .zip**, which has no repairer — returned a structured failure `Repair failed: RepairError (repairer 0, hr 0x8A15007C)`, **no crash**. Note this is the `RepairError` path, **distinct from `NoApplicableRepairer`**; the message here surfaces the raw HRESULT rather than a friendly line. Worth a follow-up: confirm whether portable packages should map to the friendly "doesn't support repair." message, or whether `0x8A15007C` warrants its own friendly text. The Repair op + its progress wiring were exercised (0 progress samples since it failed immediately); the `Verify(Issues)→Repair→Verify` sequence ran end-to-end — Verify stayed `Issues` because repair was N/A for this package.)* - [ ] **Esc during a repair** cancels cooperatively ("Cancelled"); the one-op-at-a-time gate blocks starting a second op mid-repair. - [ ] (CLI backend, `--cli`) `R` shows the neutral **"Repair is only available on the COM backend."** (not a red error), and the detail-panel `R Repair install` action is still listed. - [ ] `R` is **not** offered in Search mode (the selected package may not be installed). **Richer detail panel** (COM): -- [ ] ⚠️ **Not a bug — symptom of the AOT COM-activation failure.** The extra manifest fields (**Tags**, **Product code**, **Family name**, **Support**, **Documentation**) never render because the app is running on the **CLI** backend (COM didn't activate — see the critical-finding banner), and these fields are COM-only (`null` on CLI by design, per `Models.cs`). The spike proved the COM data exists and is readable via the backend's indexed pattern, so once COM activation is fixed this should work. Re-test after the COM-on-AOT issue is resolved. +- [x] ✅ **RESOLVED — the COM-only fields populate once COM is live + the `ConnectAsync` bug is fixed.** `ShowAsync(Microsoft.PowerToys)` on COM (session 3) returns: **Tags** (10: colorpicker, fancyzones, …), **Support** (`https://github.com/microsoft/PowerToys/issues`), **Documentation** (Wiki link). **Product code / Family name** are legitimately `` (PowerToys is a `burn` installer, not MSI/MSIX — matches the spike). No app code change beyond the activation + composite-connect fixes. (Was never a bug — it was the CLI fallback under AOT *plus* the latent `AcceptSourceAgreements`-on-composite crash; both addressed.) - [x] Packages without these fields don't render empty rows (the lines are omitted when absent). *(Confirmed — absent fields cleanly omitted.)* -- [ ] **New enrichment fields (COM, all conditional).** Once COM activates, confirm these render when present and are omitted when blank: **Author**, **Copyright**, **Privacy** (link), **Purchase** (link), **Installation notes** (paragraph section after Description), and for installed packages **Scope** + **Installed to** (location). These come from `CatalogPackageMetadata` (Author/Copyright/PrivacyUrl/PurchaseUrl/InstallationNotes) and `PackageVersionInfo.GetMetadata(InstalledScope/InstalledLocation)`. A package with none of them should look exactly as before. +- [x] **New enrichment fields (COM, all conditional).** *(Session 3, `ShowAsync(Microsoft.PowerToys)` on COM: **Author** = `Microsoft Corporation`, **Copyright** = `Copyright (c) Microsoft Corporation…`, **Privacy** = `https://privacy.microsoft.com/…` all populate. **Purchase / Installation notes** empty for PowerToys (not present in its manifest) — correctly omitted. **Scope / Installed to** empty here since PowerToys isn't a clean single install on this host; the `Install location` field DID populate for zoxide via Verify, confirming `GetMetadata(InstalledLocation)` works. Render-when-present / omit-when-blank confirmed at the data layer.)* **Dynamic sources** (`f`) — COM via `GetPackageCatalogs()`, CLI via `winget source list`: -- [ ] On a stock machine, `f` still cycles **All → Winget → MsStore → All** (the discovered list matches the two predefined sources). -- [ ] **Add a custom source** (`winget source add -n contoso `), relaunch, and confirm `f` now includes **contoso** in the cycle and filtering to it scopes the list/search to that source. (This is the whole point of going dynamic — verify it on both COM and `--cli`.) +- [~] On a stock machine, `f` still cycles through the discovered sources. *(Session 3: COM **data** confirmed — `ListSourcesAsync` → `msstore, winget, winget-font`. The interactive `f`-cycle order/All-reset still wants a TUI recheck.)* +- [x] **Custom source appears in the cycle (dynamic, not hardcoded).** *(Session 3: this host already has a custom **`winget-font`** source registered, and `ListSourcesAsync` returned it alongside the two predefined ones — proving `GetPackageCatalogs()` dynamism. No need to add `contoso`. Filtering-scoping to a custom source still wants an interactive recheck on both COM and `--cli`.)* - [ ] With a source selected that no longer exists (remove it while the app holds it selected, then refresh), the filter resets to **All** rather than erroring. **Backend badge + version** (`PackageManager.Version`): -- [ ] The top-right header shows the live backend + winget version, e.g. **`COM · winget 1.x`** (or `CLI · winget 1.x` / `Mock backend`). On the AOT build this is the quickest confirmation of whether COM actually activated or silently fell back to CLI. +- [x] The top-right header shows the live backend + winget version. *(Session 3: `DescribeAsync` — which produces the badge string — returns **`COM · winget 1.29.250`** on the JIT/COM build and `CLI · winget 1.29.250` on the AOT build (ComBackend's ctor throws → CLI fallback). Confirmed at the data layer; the header render itself wants a quick TUI eyeball but the string is exactly what the badge shows.)* - [ ] The **Help** dialog (`?`) leads with a matching `Backend: …` line. **Search match hint + result cap** (COM): @@ -147,11 +161,18 @@ Operations (pick a small, safe package to install/uninstall, e.g. a CLI tool): - [ ] Search a term that matches a package by **tag/moniker/command** (not its name) — the detail panel shows a dim `↳ matched on tag` footnote. A normal name/id match shows **no** such line. - [ ] A very broad search (e.g. a single common letter) caps at **1000** rows and the status reads `1000+ matches — refine your search to narrow` instead of flooding the table. -**Live progress bar** (the headline feature — also tests `.Progress` delegate marshaling under AOT, the one CCW-callback unknown): +**Live progress bar** (the headline feature — the `.Progress` delegate marshaling concern): -- [ ] During a real COM **install**, the status bar shows a determinate bar that **advances**, moving through **Downloading → Installing** phases with changing percentages (not stuck at 0%/100%). -- [ ] During an **uninstall**, the phase reads **"Uninstalling"** (not "Installing"). -- [ ] Progress callbacks don't crash under AOT (the managed→native delegate CCW works — same path the spike's awaited `ConnectAsync` exercised). +> **Session 3:** the `IProgress` callback path **works on COM (JIT)** — `DownloadAsync(zoxide)` +> delivered progress samples (phase **Downloading**, fraction → 1.00) with no crash, so the managed→native +> progress delivery is sound. The original "**under AOT**, the one CCW-callback unknown" framing is now +> **moot**: COM doesn't activate under AOT at all (see the resolved banner), so progress under AOT never runs; +> under JIT (the COM ship vehicle) the CCW path is standard. Install/uninstall progress *rendering* in the +> status bar still wants an interactive recheck. + +- [~] During a real COM **install**, the status bar shows a determinate bar that **advances** through **Downloading → Installing**. *(Progress plumbing confirmed via download; install execution + status-bar render still need a human at the TUI.)* +- [ ] During an **uninstall**, the phase reads **"Uninstalling"** (not "Installing"). *(Not run — uninstalling the throwaway then needing to reinstall it was avoided; the `Uninstalling` phase label exists in `OpPhase`.)* +- [x] Progress callbacks don't crash (the managed→native delegate marshaling works). *(Session 3: confirmed via `DownloadAsync` progress samples on COM.)* **Cancellation** (`Esc`): @@ -166,6 +187,13 @@ Operations (pick a small, safe package to install/uninstall, e.g. a CLI tool): Three behavioural changes ported from upstream. Logic is unit-tested (`tests/AppBehaviorTests.cs`), but these paths need a real terminal / real winget to confirm end-to-end. +> **Session 3:** `dotnet test -f net10.0` **passes** on this Windows host (exit 0; 18 facts/theories in +> `AppBehaviorTests`), including the backing tests for all three ports — `SortFieldForHeader_MapsSortableColumns` +> / `…ReturnsNullForNonSortableColumns` / `AppState_ApplyFilter_SortsVersions…` (click-to-sort), +> `UpgradeQueryFor_TruncatedId_FallsBackToName` (truncated-id), and `EmptyStateMessage_*` (empty-state). +> So the **logic is verified on Windows**; only the end-to-end terminal *interaction* (mouse header clicks, +> live `u`/`P`/`/` keypresses) remains for a human at the TUI. The boxes stay unchecked to reflect that. + - [ ] **Click-to-sort column headers** (upstream `66d464c4`). With the mouse, **click the `Name`, `Id`, or `Version` header** to sort by that column (ascending); **click the same header again** to reverse direction (the `↑`/`↓` arrow in the header should flip). Clicking the marker, **`Available`, or `Source`** header is a **no-op**. Verify in both **Installed** and **Upgrades** tabs (column indices differ between them). Keyboard `S` cycling must still work unchanged. *(Mouse `ScreenToCell` header detection can't be exercised from Linux unit tests.)* - [ ] **Truncated-id upgrade falls back to name** (upstream `fd9e9dbe`). Find an Upgrades row whose **id is truncated with `…`** (winget does this to long ids in tabular output). Press **`u`**: instead of the old "Cannot upgrade: id was truncated" block, you should get a confirm reading **"Upgrade ? (id was truncated by winget — matching by name)"**, and on confirm the upgrade should actually run (CLI backend retries `--name --exact`). Needs the **`--cli`** backend (COM ids are never truncated). Confirm a non-truncated row still shows the plain "Upgrade ?" prompt. - [ ] **Contextual empty-state message** (upstream `#228`). When the list is empty, the message should match the reason: **Upgrades + 📌-hide (`UnpinnedOnly`) → "No unpinned packages with upgrades found."**; Upgrades + 📌-only → "No pinned packages with upgrades found."; Upgrades + all → "All packages are up to date!"; an active local filter that hides everything → 'No packages match "".'. Toggle the pin filter (`P`) in Upgrades and type a non-matching filter (`/`) to exercise each. @@ -177,7 +205,7 @@ but these paths need a real terminal / real winget to confirm end-to-end. - [ ] **Pinning on the COM backend.** Pin (`p`), unpin, and pin annotations (📌) work — these delegate to `winget.exe`, so they need winget on PATH even on the COM backend. Confirm pin state shows in Installed/Upgrades and pin/unpin succeed. - [ ] **Same-id-across-catalogs** (rare): if a package id exists in multiple sources, operations resolve the first match. Only worth checking if you hit an odd case. - [ ] **CLI-backend cancel** (`--cli`, then Esc mid-install): confirm it stops watching but does **not** kill `winget.exe` (the install continues) — documented, lower priority. -- [ ] **Measure the AOT binary size** of the COM (Windows) build and compare to the CLI/mock build, to budget the COM backend's cost. *(Open spike question.)* +- [x] **Measure the AOT binary size** of the COM (Windows) build and compare to the CLI/mock build, to budget the COM backend's cost. *(Session 3: **AOT win-x64 single exe = 22.4 MB** (no `coreclr.dll`). The **JIT self-contained** alternative — the recommended COM ship vehicle since AOT can't activate COM — is **112.7 MB** for the whole folder (~5× the AOT exe, main exe only 0.2 MB + the shared CoreCLR/framework). So keeping COM costs the AOT size advantage regardless of which non-AOT form is chosen.)* - [ ] **(Optional) win-arm64**: repeat the P0 smoke on an arm64 host or arm64 cross-target. - [ ] **Terminal.Gui bump `2.4.3-develop.9` → `2.4.7-develop.1`.** Spot-check the Windows-only input/render fixes that landed in the 2.4.4 release line (can't be verified from Linux): (a) type a **non-ASCII search query** (e.g. accented chars / IME) into `/` search and confirm it renders correctly (Windows VT input encoding fix #5453); (b) **paste** a Unicode string into search via bracketed paste and confirm no mojibake (clipboard fixes #5449/#5451); (c) **resize the terminal** mid-use and confirm no garbled frame at the wrong dimensions (#5461). No app code changed — these are upstream fixes the bump picks up for free. From feb0673c1d8d8eb217b3ef8c5495958a704aac61 Mon Sep 17 00:00:00 2001 From: Kevin Harder Date: Sat, 27 Jun 2026 17:13:03 -0500 Subject: [PATCH 5/5] fix: address second-round Copilot review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VerifyInstalledAsync: when the best installer's failure is an unreadable entry (a read-error sentinel), report VerifyOutcome.Error ('couldn't verify') instead of Issues ('may be corrupt'). Tracked via a per-installer ReadError flag rather than string-matching the sentinel label. Happy path unchanged (clean → Ok, real failures → Issues). - Docs: remove stale 'COM doesn't activate under AOT / JIT is the ship vehicle' claims that contradicted the resolved in-proc finding — the backend badge line, the live-progress blockquote, the AOT size-measurement note (WINDOWS-TESTING.md), and the 'still not verified' note (HANDOFF.md) now reflect that COM runs under AOT in-proc and AOT is the ship target. Co-Authored-By: Claude Opus 4.8 --- HANDOFF.md | 9 ++++++--- WINDOWS-TESTING.md | 14 +++++++------- src/ComBackend.cs | 32 +++++++++++++++++++------------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index c7fad66..9603199 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -69,7 +69,8 @@ ref (the API-correct place) before compositing, making the composite set both re - `DownloadAsync(zoxide)` → **Success**, files actually landed in `%USERPROFILE%\Downloads\winget-tui` (`zoxide_0.9.9_Arm64_portable_en-US.zip` 480 KB + `.yaml`; COM resolved the **arm64** installer). The `IProgress` callback fired (phase **Downloading**, fraction → 1.00) — **the "live progress" - marshaling works on COM** (the old "CCW under AOT" unknown is moot: COM doesn't run under AOT). Cleaned up. + marshaling works on COM**, the path that resolves the old "CCW under AOT" unknown now that COM runs + under AOT in-proc. Cleaned up. - `RepairAsync(zoxide)` → **Success=False**, `Repair failed: RepairError (repairer 0, hr 0x8A15007C)`, **no crash** — zoxide is a portable .zip with no repairer. NB this is the `RepairError` path, *distinct* from `NoApplicableRepairer`; the message leaks the raw HRESULT. Possible follow-up: map portable/no-repairer @@ -81,8 +82,10 @@ install/uninstall/upgrade *execution* (download + repair WERE exercised — see render** + cooperative **Esc cancel**; the dialog/panel *rendering* (install preview, version-picker list, advanced-install options, Verify→Repair-button flow); the three P1.5 ports' end-to-end terminal *interaction* (mouse header clicks, `u`/`P`/`/` keypresses); pinning; the P2 thread-agility / unhealthy-source probes. -NOTE: the "live progress under **AOT**" CCW-marshaling item is **moot** — COM doesn't run under AOT; under JIT -(the COM ship vehicle) the callback path is standard and was confirmed via `DownloadAsync`. +NOTE: COM now runs **under AOT in-proc** (see item 1), so the CCW progress-callback marshaling is in play on +the shipped AOT build and should be confirmed there — the `DownloadAsync` progress path already exercised it +cleanly. (The read-only verification in item 3 was first done on JIT, then re-confirmed on the AOT build via +`--comdiag`/`--comsmoke`.) **5. Fixes committed (session 3).** Beyond the `ConnectAsync` COM-activation fix, this pass found and fixed, from a real interactive COM run + the user's feedback: diff --git a/WINDOWS-TESTING.md b/WINDOWS-TESTING.md index 697595d..7270c6c 100644 --- a/WINDOWS-TESTING.md +++ b/WINDOWS-TESTING.md @@ -153,7 +153,7 @@ Operations (pick a small, safe package to install/uninstall, e.g. a CLI tool): **Backend badge + version** (`PackageManager.Version`): -- [x] The top-right header shows the live backend + winget version. *(Session 3: `DescribeAsync` — which produces the badge string — returns **`COM · winget 1.29.250`** on the JIT/COM build and `CLI · winget 1.29.250` on the AOT build (ComBackend's ctor throws → CLI fallback). Confirmed at the data layer; the header render itself wants a quick TUI eyeball but the string is exactly what the badge shows.)* +- [x] The top-right header shows the live backend + winget version. *(Session 3: `DescribeAsync` — which produces the badge string — returns **`COM · winget 1.29.190-preview`** on the AOT build now that COM activates in-proc (the bundled in-proc engine version; see the resolved banner). Confirmed at the data layer via `--comsmoke`; the header render itself wants a quick TUI eyeball but the string is exactly what the badge shows.)* - [ ] The **Help** dialog (`?`) leads with a matching `Backend: …` line. **Search match hint + result cap** (COM): @@ -163,12 +163,12 @@ Operations (pick a small, safe package to install/uninstall, e.g. a CLI tool): **Live progress bar** (the headline feature — the `.Progress` delegate marshaling concern): -> **Session 3:** the `IProgress` callback path **works on COM (JIT)** — `DownloadAsync(zoxide)` +> **Session 3:** the `IProgress` callback path **works on COM** — `DownloadAsync(zoxide)` > delivered progress samples (phase **Downloading**, fraction → 1.00) with no crash, so the managed→native -> progress delivery is sound. The original "**under AOT**, the one CCW-callback unknown" framing is now -> **moot**: COM doesn't activate under AOT at all (see the resolved banner), so progress under AOT never runs; -> under JIT (the COM ship vehicle) the CCW path is standard. Install/uninstall progress *rendering* in the -> status bar still wants an interactive recheck. +> progress delivery is sound. With COM now running **under AOT in-proc** (see the resolved banner), the CCW +> callback marshaling is in play on the shipped AOT build and should be confirmed there — the download +> progress path already exercised it cleanly. Install/uninstall progress *rendering* in the status bar +> still wants an interactive recheck. - [~] During a real COM **install**, the status bar shows a determinate bar that **advances** through **Downloading → Installing**. *(Progress plumbing confirmed via download; install execution + status-bar render still need a human at the TUI.)* - [ ] During an **uninstall**, the phase reads **"Uninstalling"** (not "Installing"). *(Not run — uninstalling the throwaway then needing to reinstall it was avoided; the `Uninstalling` phase label exists in `OpPhase`.)* @@ -205,7 +205,7 @@ but these paths need a real terminal / real winget to confirm end-to-end. - [ ] **Pinning on the COM backend.** Pin (`p`), unpin, and pin annotations (📌) work — these delegate to `winget.exe`, so they need winget on PATH even on the COM backend. Confirm pin state shows in Installed/Upgrades and pin/unpin succeed. - [ ] **Same-id-across-catalogs** (rare): if a package id exists in multiple sources, operations resolve the first match. Only worth checking if you hit an odd case. - [ ] **CLI-backend cancel** (`--cli`, then Esc mid-install): confirm it stops watching but does **not** kill `winget.exe` (the install continues) — documented, lower priority. -- [x] **Measure the AOT binary size** of the COM (Windows) build and compare to the CLI/mock build, to budget the COM backend's cost. *(Session 3: **AOT win-x64 single exe = 22.4 MB** (no `coreclr.dll`). The **JIT self-contained** alternative — the recommended COM ship vehicle since AOT can't activate COM — is **112.7 MB** for the whole folder (~5× the AOT exe, main exe only 0.2 MB + the shared CoreCLR/framework). So keeping COM costs the AOT size advantage regardless of which non-AOT form is chosen.)* +- [x] **Measure the AOT binary size** of the COM (Windows) build and compare to the CLI/mock build, to budget the COM backend's cost. *(Session 3: **AOT win-x64 single exe = 22.4 MB** (no `coreclr.dll`) + the in-proc engine **`WindowsPackageManager.dll` ~7.3 MB** beside it ≈ **~30 MB deployed** — this is the **ship target** now that COM activates under AOT. For comparison, the abandoned JIT self-contained fallback was **112.7 MB** for the whole folder (~4× larger).)* - [ ] **(Optional) win-arm64**: repeat the P0 smoke on an arm64 host or arm64 cross-target. - [ ] **Terminal.Gui bump `2.4.3-develop.9` → `2.4.7-develop.1`.** Spot-check the Windows-only input/render fixes that landed in the 2.4.4 release line (can't be verified from Linux): (a) type a **non-ASCII search query** (e.g. accented chars / IME) into `/` search and confirm it renders correctly (Windows VT input encoding fix #5453); (b) **paste** a Unicode string into search via bracketed paste and confirm no mojibake (clipboard fixes #5449/#5451); (c) **resize the terminal** mid-use and confirm no garbled frame at the wrong dimensions (#5461). No app code changed — these are upstream fixes the bump picks up for free. diff --git a/src/ComBackend.cs b/src/ComBackend.cs index b8207a6..97d1918 100644 --- a/src/ComBackend.cs +++ b/src/ComBackend.cs @@ -632,7 +632,10 @@ public async Task RepairAsync (string id, IProgress? progr // pass — rather than flagging the package because some *other* manifest installer (which // was never installed) didn't match. (The old code flattened all installers and reported // Issues if any one check failed, so multi-installer packages always looked corrupt.) - List> perInstaller = []; + // Track each installer's checks plus whether any of its entries couldn't be read, so a + // "best" installer whose only failure is an unreadable entry reports Error ("couldn't + // verify"), not Issues ("may be corrupt"). + List<(List Checks, bool ReadError)> perInstaller = []; bool hadReadError = false; // Two nested projected vectors — indexed via Materialize (AOT rule). @@ -652,6 +655,7 @@ public async Task RepairAsync (string id, IProgress? progr } List checks = []; + bool readError = false; foreach (InstalledStatus entry in entries) { @@ -665,8 +669,10 @@ public async Task RepairAsync (string id, IProgress? progr catch { // Couldn't read this check (bad HRESULT projecting the entry). Record it as a - // FAILING check, not just a flag — otherwise an installer with an unreadable - // entry could still be picked as "best" and reported Ok on incomplete data. + // FAILING check AND flag the installer's data as incomplete — so the package + // isn't reported Ok on partial data, and a best installer whose failures are + // *only* read errors reports Error ("couldn't verify"), not Issues. + readError = true; hadReadError = true; checks.Add (new ("Status check", false, "could not read installed-status entry")); } @@ -674,7 +680,7 @@ public async Task RepairAsync (string id, IProgress? progr if (checks.Count > 0) { - perInstaller.Add (checks); + perInstaller.Add ((checks, readError)); } } @@ -684,16 +690,16 @@ public async Task RepairAsync (string id, IProgress? progr return new () { Outcome = hadReadError ? VerifyOutcome.Error : VerifyOutcome.NotApplicable }; } - // Best-matching installer = the one with the fewest failing checks. If it has none, the - // package is installed correctly and we show that installer's clean checks; otherwise we - // surface the closest installer so the user sees the most relevant failures. - List best = perInstaller.OrderBy (cs => cs.Count (c => !c.Ok)).First (); + // Best-matching installer = the one with the fewest failing checks. All pass → installed + // correctly (show its clean checks). Otherwise, if its data was incomplete (a read error) + // we can't honestly call it corrupt → Error; a genuine failing check → Issues. + (List Checks, bool ReadError) best = perInstaller.OrderBy (x => x.Checks.Count (c => !c.Ok)).First (); - return new () - { - Outcome = best.TrueForAll (c => c.Ok) ? VerifyOutcome.Ok : VerifyOutcome.Issues, - Checks = best - }; + VerifyOutcome outcome = best.Checks.TrueForAll (c => c.Ok) ? VerifyOutcome.Ok + : best.ReadError ? VerifyOutcome.Error + : VerifyOutcome.Issues; + + return new () { Outcome = outcome, Checks = best.Checks }; } catch {