Skip to content

fix: design polish — Bulk Installer, Context Menu, elevation badges#500

Merged
laurentiu021 merged 1 commit into
mainfrom
fix/design-polish-batch
May 22, 2026
Merged

fix: design polish — Bulk Installer, Context Menu, elevation badges#500
laurentiu021 merged 1 commit into
mainfrom
fix/design-polish-batch

Conversation

@laurentiu021
Copy link
Copy Markdown
Owner

@laurentiu021 laurentiu021 commented May 22, 2026

Summary

UI/UX polish for 4 pages based on user feedback.

Bulk Installer

  • Expanded from 24 to 47 curated apps across 8 categories
  • New categories: Office & Productivity, Creativity, Networking & VPN, Runtimes
  • Added app descriptions (one-line under each name)
  • Replaced DataGrid with grouped ItemsControl (category headers with dividers)
  • Winget ID moved to tooltip (not cluttering the view)
  • Custom winget search: users can search any package and add it to install list

Context Menu Manager

  • Friendly names for cryptic entries (.SpotlightLearnMore → "Spotlight — Learn More")
  • Dictionary of known entries + command-based inference + CamelCase splitting
  • Rich tooltip showing raw registry name, full command, path
  • "Show system entries" toggle (defaults OFF — hides internal/cryptic entries)
  • Explanatory header text for non-power-users

Privacy Toggles + Windows Features

  • Replaced alarming red/orange elevation badges with subtle info strips
  • Consistent Surface2 card with shield icon + calm text
  • Added "Run as administrator" button inline
  • Hidden when already elevated

Test plan

  • CI build passes
  • Bulk Installer shows categories, descriptions, grouping
  • Custom search finds winget packages and adds to list
  • Context Menu shows friendly names, hides system entries by default
  • Privacy/Features pages show subtle elevation banner (not alarming)
  • "Run as administrator" button works
  • App launches without crash

Summary by CodeRabbit

  • New Features

    • Context menu entries show friendly names (plus stored raw name and system-entry flag)
    • Custom winget search in Bulk Installer with add-to-install list
    • Grouped view in Bulk Installer; per-item descriptions added
    • Toggle to show/hide system context-menu entries; tooltips reveal raw/registry details
    • Run-as-administrator (relaunch elevated) commands in Privacy and Windows Features views
  • Tests

    • Updated BulkInstallerViewModel tests to reflect expanded curated app set and categories

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

This PR extends the application with three independent feature enhancements: context menu entries now resolve and display friendly names with optional system-entry filtering; the bulk installer adds grouped view layout and integrated winget package search with descriptions; and both Privacy and Windows Features views gain admin elevation relaunch commands with corresponding UI strips.

Changes

Context Menu Friendly-Name Resolution and System Entry Filtering

Layer / File(s) Summary
ContextMenuEntry metadata properties
SysManager/SysManager/Models/ContextMenuEntry.cs
ContextMenuEntry adds RawName and IsSystemEntry init-only properties to expose the original registry key name and classify system/internal entries.
ContextMenuService friendly-name resolution
SysManager/SysManager/Services/ContextMenuService.cs
New private helper methods and lookup dictionaries (KnownNames, KnownResourceStrings, KnownExeNames) resolve registry entry names into user-friendly labels via known aliases, resource-string parsing, dot-prefix cleanup, and CamelCase splitting; ScanEntries() populates RawName and IsSystemEntry.
ContextMenuViewModel system-entry filtering
SysManager/SysManager/ViewModels/ContextMenuViewModel.cs
ShowSystemEntries observable flag gates visibility of system entries in filtered Entries; filtering logic and text search extended to match Source and RawName fields.
ContextMenuView UI updates
SysManager/SysManager/Views/ContextMenuView.xaml
Header description updated; "Show system entries" checkbox added to toolbar; Name column replaced with template displaying RawName/Command/RegistryPath in tooltip alongside friendly name and command preview.

Bulk Installer Grouping and Winget Search

Layer / File(s) Summary
InstallableApp description property
SysManager/SysManager/Models/InstallableApp.cs
InstallableApp adds Description init-only property for curated winget applications.
BulkInstallerViewModel grouping and search
SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs
GroupedView property groups FilteredApps by category; SearchQuery, IsSearching, and SearchResults observables manage custom winget search; SearchWingetPackages() parses winget's tabular output into InstallableApp entries; AddToInstallList() merges search results while preventing duplicates by WingetId.
Curated apps with descriptions
SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs
BuildCuratedApps() expanded to include Description for each predefined app.
BulkInstallerView redesign with search and grouping
SysManager/SysManager/Views/BulkInstallerView.xaml
DataGrid replaced with grouped ItemsControl; new "Custom winget search" section with input, progress bar, and results list with per-item Add buttons; main list renders category headers and rows with checkbox, name/description, and status badge; status bar shifted down.
BulkInstallerViewModel tests
SysManager/SysManager.Tests/BulkInstallerViewModelTests.cs
Tests updated to expect the expanded curated apps set (constructor count, FilterByCategory data, and category-count including "Custom").

Privacy View Admin Elevation

Layer / File(s) Summary
PrivacyViewModel and PrivacyView elevation features
SysManager/SysManager/ViewModels/PrivacyViewModel.cs, SysManager/SysManager/Views/PrivacyView.xaml
RelaunchElevated relay command added to trigger admin relaunch and shutdown; view adds grid row and elevation info strip with "Run as administrator" button and text.

Windows Features View Admin Elevation

Layer / File(s) Summary
WindowsFeaturesViewModel and WindowsFeaturesView elevation features
SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs, SysManager/SysManager/Views/WindowsFeaturesView.xaml
RelaunchElevated relay command added to trigger admin relaunch and shutdown; view adds grid row and elevation info strip replacing previous toolbar warning, with subsequent sections shifted down.

Possibly Related PRs

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 The menu entries now have names so nice,
Search for apps in bulk—twice the spice!
Admin mode awaits with buttons so grand,
For Privacy and Features across the land!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: design polish — Bulk Installer, Context Menu, elevation badges' accurately describes the main changes: UI/UX polish to three key areas (Bulk Installer, Context Menu, and elevation components) across multiple pages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/design-polish-batch

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs Dismissed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SysManager/SysManager/Services/ContextMenuService.cs (1)

62-64: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the registry subkey name as RawName and for IsSystemEntry.

RawName is intended to be the original registry key name, but at Line 80 it is populated from displayName (which can come from the default value). This also affects IsSystemEntry at Line 86 and can hide/show the wrong entries.

Suggested fix
-                        // Read display name: prefer (Default) value, fall back to key name
-                        var displayName = entryKey.GetValue("")?.ToString();
-                        if (string.IsNullOrWhiteSpace(displayName))
-                            displayName = entryName;
+                        // Keep raw key name for filtering/tooltips; use default value only for display seed
+                        var rawName = entryName;
+                        var defaultDisplayName = entryKey.GetValue("")?.ToString();
+                        var displaySeed = string.IsNullOrWhiteSpace(defaultDisplayName)
+                            ? rawName
+                            : defaultDisplayName;
@@
-                        var friendlyName = GetFriendlyName(displayName, command);
+                        var friendlyName = GetFriendlyName(displaySeed, command);
@@
-                            RawName = displayName,
+                            RawName = rawName,
@@
-                            IsSystemEntry = IsSystemEntry(displayName)
+                            IsSystemEntry = IsSystemEntry(rawName)

Also applies to: 79-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/Services/ContextMenuService.cs` around lines 62 - 64,
The code currently assigns displayName from entryKey.GetValue("") and then uses
that value to populate RawName and to decide IsSystemEntry, which can be wrong;
instead keep displayName as the friendly name (from entryKey.GetValue("")
falling back to entryName), but set RawName to the registry subkey name
(entryName) and compute IsSystemEntry based on entryName (the original key name)
rather than displayName so system entries are detected and toggled correctly
(update references where RawName and IsSystemEntry are set to use entryName
instead of displayName).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@SysManager/SysManager/Services/ContextMenuService.cs`:
- Around line 388-395: The current parsing trims quotes then cuts at the first
space which can turn quoted paths like "C:\Program Files\app.exe" into
"C:\Program"; update the parsing of command -> path to honor quoted paths: if
command starts with a quote, extract the content between the opening and
matching closing quote (use that as path) before any space-splitting; otherwise
keep the existing percent/space trimming but only split on the first space for
unquoted commands and still verify File.Exists(path). Refer to the local
variables 'command' and 'path' and the File.Exists check when making this
change.

In `@SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs`:
- Around line 205-206: The current code in BulkInstallerViewModel uses
proc.StandardOutput.ReadToEnd which blocks and makes the subsequent
proc.WaitForExit(30_000) ineffective; change this to non-blocking async I/O and
enforce a real timeout: use proc.StandardOutput.ReadToEndAsync() or
BeginOutputReadLine/OutputDataReceived with Task-based completion, then use a
Task.WhenAny with proc.WaitForExitAsync() (or a Task.Delay(30_000)) to detect
timeout, and if the timeout elapses call proc.Kill()/Dispose() and fail the
search task so the UI can leave the searching state; replace references to
StandardOutput.ReadToEnd and WaitForExit(30_000) with this async timeout+kill
flow and ensure proper cleanup and error logging.
- Around line 226-231: The code in BulkInstallerViewModel that determines column
offsets by inspecting the header string (variables header, idStart, versionStart
and the header.IndexOf("Id"/"Version") logic) is fragile and locale-dependent;
replace this table-header parsing with a locale-invariant, structured WinGet
interface (for example use the WinGet COM API or the official PowerShell
cmdlets) to request search results as structured objects and read the package
Id/Version fields directly instead of parsing the human-readable table; update
the search/execute path in the BulkInstallerViewModel to call the COM/Powershell
API and map the returned object's Id and Version properties rather than relying
on idStart/versionStart string offsets.

In `@SysManager/SysManager/ViewModels/PrivacyViewModel.cs`:
- Around line 92-96: RelaunchElevated currently silently does nothing when
AdminHelper.RelaunchAsAdmin() returns false; update RelaunchElevated to detect
the false return and surface a failure message (e.g., set a ViewModel status
property, call the app logger, or display a MessageBox) so users know the
elevation was cancelled/blocked; modify the method around the
AdminHelper.RelaunchAsAdmin() call to log/assign a clear status like "Elevation
cancelled or blocked (UAC declined)" when false, while keeping the existing
Shutdown() path when true.

---

Outside diff comments:
In `@SysManager/SysManager/Services/ContextMenuService.cs`:
- Around line 62-64: The code currently assigns displayName from
entryKey.GetValue("") and then uses that value to populate RawName and to decide
IsSystemEntry, which can be wrong; instead keep displayName as the friendly name
(from entryKey.GetValue("") falling back to entryName), but set RawName to the
registry subkey name (entryName) and compute IsSystemEntry based on entryName
(the original key name) rather than displayName so system entries are detected
and toggled correctly (update references where RawName and IsSystemEntry are set
to use entryName instead of displayName).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fe8d6e10-4468-445f-9e5f-8ef1cf971766

📥 Commits

Reviewing files that changed from the base of the PR and between ea4e6da and fb6ecf2.

📒 Files selected for processing (11)
  • SysManager/SysManager/Models/ContextMenuEntry.cs
  • SysManager/SysManager/Models/InstallableApp.cs
  • SysManager/SysManager/Services/ContextMenuService.cs
  • SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs
  • SysManager/SysManager/ViewModels/ContextMenuViewModel.cs
  • SysManager/SysManager/ViewModels/PrivacyViewModel.cs
  • SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs
  • SysManager/SysManager/Views/BulkInstallerView.xaml
  • SysManager/SysManager/Views/ContextMenuView.xaml
  • SysManager/SysManager/Views/PrivacyView.xaml
  • SysManager/SysManager/Views/WindowsFeaturesView.xaml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build & unit tests
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (7)
SysManager/SysManager/Views/PrivacyView.xaml (1)

14-19: LGTM!

Also applies to: 51-65, 68-68, 119-119

SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs (1)

43-48: LGTM!

SysManager/SysManager/Views/WindowsFeaturesView.xaml (1)

17-17: LGTM!

Also applies to: 57-71, 74-74, 79-79, 185-185

SysManager/SysManager/Models/ContextMenuEntry.cs (1)

34-38: LGTM!

SysManager/SysManager/Services/ContextMenuService.cs (1)

8-8: LGTM!

Also applies to: 246-378, 411-435

SysManager/SysManager/ViewModels/ContextMenuViewModel.cs (1)

28-28: LGTM!

Also applies to: 47-47, 120-136

SysManager/SysManager/Views/ContextMenuView.xaml (1)

23-24: LGTM!

Also applies to: 50-53, 87-108

Comment on lines +388 to +395
var path = command.Trim('"', ' ');
var percentIdx = path.IndexOf('%');
if (percentIdx > 0) path = path[..percentIdx].Trim();

var spaceIdx = path.IndexOf(' ');
if (spaceIdx > 0 && !File.Exists(path))
path = path[..spaceIdx].Trim('"');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quoted command parsing can truncate paths like C:\Program Files\....

At Line 393/394, splitting on first space after a broad trim can reduce quoted paths to C:\Program, which breaks executable inference for common commands.

Suggested fix
-            // Try to extract executable name
-            var path = command.Trim('"', ' ');
-            var percentIdx = path.IndexOf('%');
-            if (percentIdx > 0) path = path[..percentIdx].Trim();
-
-            var spaceIdx = path.IndexOf(' ');
-            if (spaceIdx > 0 && !File.Exists(path))
-                path = path[..spaceIdx].Trim('"');
+            // Extract executable token safely (quoted or unquoted)
+            var m = Regex.Match(
+                command,
+                "^\\s*\"([^\"]+\\.exe)\"|^\\s*([^\\s]+\\.exe)",
+                RegexOptions.IgnoreCase);
+            if (!m.Success) return "";
+            var path = m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/Services/ContextMenuService.cs` around lines 388 - 395,
The current parsing trims quotes then cuts at the first space which can turn
quoted paths like "C:\Program Files\app.exe" into "C:\Program"; update the
parsing of command -> path to honor quoted paths: if command starts with a
quote, extract the content between the opening and matching closing quote (use
that as path) before any space-splitting; otherwise keep the existing
percent/space trimming but only split on the first space for unquoted commands
and still verify File.Exists(path). Refer to the local variables 'command' and
'path' and the File.Exists check when making this change.

Comment on lines +205 to +206
var output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit(30_000);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the winget timeout real.

Line 205 blocks until winget exits, so Line 206 never meaningfully enforces the 30s limit. If the process stalls, the search task never completes and the UI stays in the searching state indefinitely. Switch this path to async process I/O with an actual timeout/kill branch before awaiting the full output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs` around lines 205
- 206, The current code in BulkInstallerViewModel uses
proc.StandardOutput.ReadToEnd which blocks and makes the subsequent
proc.WaitForExit(30_000) ineffective; change this to non-blocking async I/O and
enforce a real timeout: use proc.StandardOutput.ReadToEndAsync() or
BeginOutputReadLine/OutputDataReceived with Task-based completion, then use a
Task.WhenAny with proc.WaitForExitAsync() (or a Task.Delay(30_000)) to detect
timeout, and if the timeout elapses call proc.Kill()/Dispose() and fail the
search task so the UI can leave the searching state; replace references to
StandardOutput.ReadToEnd and WaitForExit(30_000) with this async timeout+kill
flow and ensure proper cleanup and error logging.

Comment on lines +226 to +231
// Determine column positions from the header line
var header = lines[separatorIndex - 1];
var idStart = header.IndexOf("Id", StringComparison.Ordinal);
var versionStart = header.IndexOf("Version", StringComparison.Ordinal);

if (idStart < 0 || versionStart < 0) return results;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

In current winget documentation, does winget search expose a machine-readable or locale-invariant output format, and are the default table headers/localized display columns considered stable for parsing?

💡 Result:

In current WinGet documentation, winget search is presented as a human-oriented table output with localized UI text; the docs do not describe any machine-readable, locale-invariant output mode for the winget search command itself (e.g., no documented --json/--csv-style output flag for winget search). The official winget search command reference focuses on query/filter options (like --id, --name, --moniker, --tag, --command, --source, --count, --exact) rather than output formatting. [1] Because there is no documented machine-readable output mode, you should assume winget search’s default table output is not stable for parsing across locales/terminal environments: even WinGet’s table headers/columns are user-visible strings and WinGet’s CLI UI text is designed to be localized. A WinGet CLI issue discussion highlights parsing difficulties attributed to formatting/localization concerns, and suggests using COM APIs / PowerShell cmdlets for structured output rather than parsing native CLI text. [2] Additionally, WinGet CLI discussions/issues explicitly call out that native CLI output is not intended to be machine-consumed and that adding a machine-parseable format (CSV/JSON/XML) has been requested for search/show/list. [3] Bottom line: 1) Machine-readable, locale-invariant output for winget search (as a documented option) is not provided in the current winget search docs. [1] 2) The default table headers / localized display columns should not be considered stable for parsing, because the CLI output is table-oriented and is subject to localization/formatting changes, and maintainers/users recommend structured alternatives (PowerShell module / COM APIs) for automation rather than parsing the table output. [2][3]

Citations:


Avoid parsing winget search display table headers; use structured output.

In SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs (lines 226-231), the code locates column offsets by searching the human-readable "Id"/"Version" header text (header.IndexOf("Id"), header.IndexOf("Version")). WinGet’s winget search documentation does not provide a documented machine-readable/locale-invariant output mode, and WinGet discussions/issues note the native table output is localized and not intended for automation—so this parsing can fail silently across locales/format changes. Switch to WinGet’s structured interfaces (e.g., COM API / PowerShell cmdlets) or another locale-invariant contract before relying on parsed IDs for search/install results.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs` around lines 226
- 231, The code in BulkInstallerViewModel that determines column offsets by
inspecting the header string (variables header, idStart, versionStart and the
header.IndexOf("Id"/"Version") logic) is fragile and locale-dependent; replace
this table-header parsing with a locale-invariant, structured WinGet interface
(for example use the WinGet COM API or the official PowerShell cmdlets) to
request search results as structured objects and read the package Id/Version
fields directly instead of parsing the human-readable table; update the
search/execute path in the BulkInstallerViewModel to call the COM/Powershell API
and map the returned object's Id and Version properties rather than relying on
idStart/versionStart string offsets.

Comment on lines +92 to +96
private void RelaunchElevated()
{
if (AdminHelper.RelaunchAsAdmin())
System.Windows.Application.Current?.Shutdown();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add failure feedback when elevation relaunch does not start.

If AdminHelper.RelaunchAsAdmin() returns false, the click is silent. Add a status/log message so users know the relaunch was blocked/cancelled (e.g., UAC declined).

Suggested patch
 [RelayCommand]
 private void RelaunchElevated()
 {
-    if (AdminHelper.RelaunchAsAdmin())
-        System.Windows.Application.Current?.Shutdown();
+    if (AdminHelper.RelaunchAsAdmin())
+    {
+        System.Windows.Application.Current?.Shutdown();
+        return;
+    }
+
+    StatusMessage = "Could not relaunch as administrator.";
+    Log.Warning("Privacy: relaunch as administrator was not started");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/ViewModels/PrivacyViewModel.cs` around lines 92 - 96,
RelaunchElevated currently silently does nothing when
AdminHelper.RelaunchAsAdmin() returns false; update RelaunchElevated to detect
the false return and surface a failure message (e.g., set a ViewModel status
property, call the app logger, or display a MessageBox) so users know the
elevation was cancelled/blocked; modify the method around the
AdminHelper.RelaunchAsAdmin() call to log/assign a clear status like "Elevation
cancelled or blocked (UAC declined)" when false, while keeping the existing
Shutdown() path when true.

@laurentiu021 laurentiu021 force-pushed the fix/design-polish-batch branch from fb6ecf2 to a6abd42 Compare May 22, 2026 08:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
SysManager/SysManager.Tests/BulkInstallerViewModelTests.cs (1)

20-100: ⚡ Quick win

Add focused tests for new search/add behaviors.

Please add coverage for AddToInstallList duplicate prevention and SearchWingetAsync state transitions (IsSearching, result replacement, and failure status path). Those are key new paths and currently unverified here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager.Tests/BulkInstallerViewModelTests.cs` around lines 20 -
100, Add unit tests to cover duplicate prevention in AddToInstallList and
SearchWingetAsync state transitions: add tests that call AddToInstallList twice
with the same AppViewModel and assert the InstallList (or whichever collection
is used by the VM) contains only one entry (no duplicates) and that
IsSelected/IsInInstallList flags are set appropriately; and add tests for
SearchWingetAsync that assert IsSearching is true while the async search runs,
that a successful search replaces the previous FilteredApps (or search result
collection) with the new results, and that a failing search sets the
SearchFailed/IsSearchFailed flag and clears or leaves results as intended.
Target the methods/properties AddToInstallList, SearchWingetAsync, IsSearching,
FilteredApps (or SearchResults), InstallList (or the VM's install collection),
and SearchFailed so the tests exercise duplicate prevention, in-progress state,
success replacement, and failure paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs`:
- Around line 272-345: The BuildCuratedApps method returns 46 InstallableApp
entries but the PR objective/tests expect 47; update BuildCuratedApps to either
add the missing InstallableApp entry (create a new new() { Name = "...",
WingetId = "...", Category = "...", Description = "..." } in the returned list)
so the list contains 47 items, or adjust the PR objective/tests to expect 46;
locate the list inside the BuildCuratedApps function and modify it accordingly,
ensuring the InstallableApp objects remain correctly formed.
- Around line 163-165: The early return in BulkInstallerViewModel (the block
using SearchQuery, IsSearching, and SearchResults) skips clearing previous
results, so move or add SearchResults.Clear() (and set IsSearching = false if
appropriate) before the return when SearchQuery is null/whitespace or shorter
than 2; ensure the logic in the search-start guard always clears stale
SearchResults and leaves view state consistent.

In `@SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs`:
- Around line 43-48: The RelaunchElevated method only shuts down the app on
success and provides no user feedback when AdminHelper.RelaunchAsAdmin() returns
false; update RelaunchElevated to detect the false return and display a clear
user-facing notification (e.g., MessageBox/notification via the VM's messaging
service) indicating that elevation was cancelled or blocked and any recommended
next steps; keep the existing shutdown behavior when RelaunchAsAdmin() returns
true and reference the RelaunchElevated method and AdminHelper.RelaunchAsAdmin()
when making the change.

---

Nitpick comments:
In `@SysManager/SysManager.Tests/BulkInstallerViewModelTests.cs`:
- Around line 20-100: Add unit tests to cover duplicate prevention in
AddToInstallList and SearchWingetAsync state transitions: add tests that call
AddToInstallList twice with the same AppViewModel and assert the InstallList (or
whichever collection is used by the VM) contains only one entry (no duplicates)
and that IsSelected/IsInInstallList flags are set appropriately; and add tests
for SearchWingetAsync that assert IsSearching is true while the async search
runs, that a successful search replaces the previous FilteredApps (or search
result collection) with the new results, and that a failing search sets the
SearchFailed/IsSearchFailed flag and clears or leaves results as intended.
Target the methods/properties AddToInstallList, SearchWingetAsync, IsSearching,
FilteredApps (or SearchResults), InstallList (or the VM's install collection),
and SearchFailed so the tests exercise duplicate prevention, in-progress state,
success replacement, and failure paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 97229025-c4a5-4cfa-a820-aab6c8f98adb

📥 Commits

Reviewing files that changed from the base of the PR and between fb6ecf2 and a6abd42.

📒 Files selected for processing (12)
  • SysManager/SysManager.Tests/BulkInstallerViewModelTests.cs
  • SysManager/SysManager/Models/ContextMenuEntry.cs
  • SysManager/SysManager/Models/InstallableApp.cs
  • SysManager/SysManager/Services/ContextMenuService.cs
  • SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs
  • SysManager/SysManager/ViewModels/ContextMenuViewModel.cs
  • SysManager/SysManager/ViewModels/PrivacyViewModel.cs
  • SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs
  • SysManager/SysManager/Views/BulkInstallerView.xaml
  • SysManager/SysManager/Views/ContextMenuView.xaml
  • SysManager/SysManager/Views/PrivacyView.xaml
  • SysManager/SysManager/Views/WindowsFeaturesView.xaml
✅ Files skipped from review due to trivial changes (1)
  • SysManager/SysManager/Views/ContextMenuView.xaml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build & unit tests
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (15)
SysManager/SysManager/Services/ContextMenuService.cs (6)

388-395: The quoted command parsing issue at these lines was already flagged in a previous review.


74-87: LGTM!


246-303: LGTM!


309-359: LGTM!


364-378: LGTM!


411-435: LGTM!

SysManager/SysManager/Models/ContextMenuEntry.cs (1)

33-38: LGTM!

SysManager/SysManager/ViewModels/ContextMenuViewModel.cs (1)

28-28: LGTM!

Also applies to: 47-47, 120-122, 135-136

SysManager/SysManager/ViewModels/PrivacyViewModel.cs (1)

91-96: Silent failure path in relaunch command remains.

Line 94 still has no user feedback when elevation relaunch returns false; this was already flagged in prior review comments.

SysManager/SysManager/Views/PrivacyView.xaml (1)

14-19: LGTM!

Also applies to: 51-66, 68-69, 119-119

SysManager/SysManager/Views/WindowsFeaturesView.xaml (1)

17-20: LGTM!

Also applies to: 57-72, 74-75, 79-80, 185-185

SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs (2)

205-206: Previously reported timeout issue still present.

The blocking output read still makes the timeout ineffective in practice. This was already flagged earlier; keeping this as a duplicate marker for tracking.


226-231: Previously reported locale-dependent parser is still present.

Header-index parsing still depends on human-readable winget search table text. This was already flagged earlier; marking as duplicate.

SysManager/SysManager/Models/InstallableApp.cs (1)

20-20: LGTM!

SysManager/SysManager/Views/BulkInstallerView.xaml (1)

14-19: LGTM!

Also applies to: 57-121, 123-198, 201-201

Comment on lines +163 to +165
if (string.IsNullOrWhiteSpace(SearchQuery) || SearchQuery.Length < 2) return;
IsSearching = true;
SearchResults.Clear();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear stale search results when query is too short.

Line 163 returns before clearing previous results, so old matches can remain visible for an invalid/short query.

Suggested fix
 private async Task SearchWingetAsync()
 {
-    if (string.IsNullOrWhiteSpace(SearchQuery) || SearchQuery.Length < 2) return;
+    if (string.IsNullOrWhiteSpace(SearchQuery) || SearchQuery.Length < 2)
+    {
+        SearchResults.Clear();
+        StatusMessage = "Enter at least 2 characters to search.";
+        return;
+    }
     IsSearching = true;
     SearchResults.Clear();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (string.IsNullOrWhiteSpace(SearchQuery) || SearchQuery.Length < 2) return;
IsSearching = true;
SearchResults.Clear();
if (string.IsNullOrWhiteSpace(SearchQuery) || SearchQuery.Length < 2)
{
SearchResults.Clear();
StatusMessage = "Enter at least 2 characters to search.";
return;
}
IsSearching = true;
SearchResults.Clear();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs` around lines 163
- 165, The early return in BulkInstallerViewModel (the block using SearchQuery,
IsSearching, and SearchResults) skips clearing previous results, so move or add
SearchResults.Clear() (and set IsSearching = false if appropriate) before the
return when SearchQuery is null/whitespace or shorter than 2; ensure the logic
in the search-start guard always clears stale SearchResults and leaves view
state consistent.

Comment on lines 272 to 345
private static List<InstallableApp> BuildCuratedApps() =>
[
// Browsers
new() { Name = "Google Chrome", WingetId = "Google.Chrome", Category = "Browsers" },
new() { Name = "Firefox", WingetId = "Mozilla.Firefox", Category = "Browsers" },
new() { Name = "Brave", WingetId = "Brave.Brave", Category = "Browsers" },
new() { Name = "Vivaldi", WingetId = "Vivaldi.Vivaldi", Category = "Browsers" },
new() { Name = "Google Chrome", WingetId = "Google.Chrome", Category = "Browsers", Description = "Fast, secure web browser by Google" },
new() { Name = "Firefox", WingetId = "Mozilla.Firefox", Category = "Browsers", Description = "Privacy-focused open-source web browser" },
new() { Name = "Brave", WingetId = "Brave.Brave", Category = "Browsers", Description = "Privacy-first browser with built-in ad blocker" },
new() { Name = "Vivaldi", WingetId = "Vivaldi.Vivaldi", Category = "Browsers", Description = "Highly customizable browser for power users" },

// Communication
new() { Name = "Discord", WingetId = "Discord.Discord", Category = "Communication" },
new() { Name = "Slack", WingetId = "SlackTechnologies.Slack", Category = "Communication" },
new() { Name = "Zoom", WingetId = "Zoom.Zoom", Category = "Communication" },
new() { Name = "Telegram", WingetId = "Telegram.TelegramDesktop", Category = "Communication" },
new() { Name = "Discord", WingetId = "Discord.Discord", Category = "Communication", Description = "Voice, video, and text chat for communities" },
new() { Name = "Slack", WingetId = "SlackTechnologies.Slack", Category = "Communication", Description = "Team messaging and collaboration platform" },
new() { Name = "Zoom", WingetId = "Zoom.Zoom", Category = "Communication", Description = "Video conferencing and online meetings" },
new() { Name = "Telegram", WingetId = "Telegram.TelegramDesktop", Category = "Communication", Description = "Fast, secure cloud-based messaging" },

// Media
new() { Name = "VLC", WingetId = "VideoLAN.VLC", Category = "Media" },
new() { Name = "Spotify", WingetId = "Spotify.Spotify", Category = "Media" },
new() { Name = "foobar2000", WingetId = "PeterPawlowski.foobar2000", Category = "Media" },
new() { Name = "VLC", WingetId = "VideoLAN.VLC", Category = "Media", Description = "Free multimedia player supporting all formats" },
new() { Name = "Spotify", WingetId = "Spotify.Spotify", Category = "Media", Description = "Music streaming with millions of songs" },
new() { Name = "foobar2000", WingetId = "PeterPawlowski.foobar2000", Category = "Media", Description = "Lightweight advanced audio player" },

// Development
new() { Name = "VS Code", WingetId = "Microsoft.VisualStudioCode", Category = "Development" },
new() { Name = "Git", WingetId = "Git.Git", Category = "Development" },
new() { Name = "Node.js", WingetId = "OpenJS.NodeJS.LTS", Category = "Development" },
new() { Name = "Python", WingetId = "Python.Python.3.12", Category = "Development" },
new() { Name = "VS Code", WingetId = "Microsoft.VisualStudioCode", Category = "Development", Description = "Lightweight code editor with extensions" },
new() { Name = "Git", WingetId = "Git.Git", Category = "Development", Description = "Distributed version control system" },
new() { Name = "Node.js", WingetId = "OpenJS.NodeJS.LTS", Category = "Development", Description = "JavaScript runtime for server-side apps" },
new() { Name = "Python", WingetId = "Python.Python.3.12", Category = "Development", Description = "General-purpose programming language" },

// Utilities
new() { Name = "7-Zip", WingetId = "7zip.7zip", Category = "Utilities" },
new() { Name = "Notepad++", WingetId = "Notepad++.Notepad++", Category = "Utilities" },
new() { Name = "Everything", WingetId = "voidtools.Everything", Category = "Utilities" },
new() { Name = "PowerToys", WingetId = "Microsoft.PowerToys", Category = "Utilities" },
new() { Name = "7-Zip", WingetId = "7zip.7zip", Category = "Utilities", Description = "Free file archiver with high compression" },
new() { Name = "Notepad++", WingetId = "Notepad++.Notepad++", Category = "Utilities", Description = "Powerful text and source code editor" },
new() { Name = "Everything", WingetId = "voidtools.Everything", Category = "Utilities", Description = "Instant file search for Windows" },
new() { Name = "PowerToys", WingetId = "Microsoft.PowerToys", Category = "Utilities", Description = "Microsoft utilities for power users" },

// Gaming
new() { Name = "Steam", WingetId = "Valve.Steam", Category = "Gaming" },
new() { Name = "Epic Games", WingetId = "EpicGames.EpicGamesLauncher", Category = "Gaming" },
new() { Name = "GOG Galaxy", WingetId = "GOG.Galaxy", Category = "Gaming" },
new() { Name = "Steam", WingetId = "Valve.Steam", Category = "Gaming", Description = "Gaming platform and digital store" },
new() { Name = "Epic Games", WingetId = "EpicGames.EpicGamesLauncher", Category = "Gaming", Description = "Game store and launcher" },
new() { Name = "GOG Galaxy", WingetId = "GOG.Galaxy", Category = "Gaming", Description = "DRM-free gaming platform" },

// Security
new() { Name = "Bitwarden", WingetId = "Bitwarden.Bitwarden", Category = "Security" },
new() { Name = "Malwarebytes", WingetId = "Malwarebytes.Malwarebytes", Category = "Security" },
new() { Name = "Bitwarden", WingetId = "Bitwarden.Bitwarden", Category = "Security", Description = "Open-source password manager" },
new() { Name = "Malwarebytes", WingetId = "Malwarebytes.Malwarebytes", Category = "Security", Description = "Anti-malware protection and scanning" },

// Office & Productivity
new() { Name = "LibreOffice", WingetId = "TheDocumentFoundation.LibreOffice", Category = "Office & Productivity", Description = "Free open-source office suite" },
new() { Name = "Obsidian", WingetId = "Obsidian.Obsidian", Category = "Office & Productivity", Description = "Knowledge base with Markdown notes" },
new() { Name = "Notion", WingetId = "Notion.Notion", Category = "Office & Productivity", Description = "All-in-one workspace for notes and docs" },
new() { Name = "Adobe Acrobat Reader", WingetId = "Adobe.Acrobat.Reader.64-bit", Category = "Office & Productivity", Description = "View, print, and annotate PDFs" },

// Creativity
new() { Name = "OBS Studio", WingetId = "OBSProject.OBSStudio", Category = "Creativity", Description = "Free streaming and screen recording" },
new() { Name = "GIMP", WingetId = "GIMP.GIMP", Category = "Creativity", Description = "Free image editor (Photoshop alternative)" },
new() { Name = "Audacity", WingetId = "Audacity.Audacity", Category = "Creativity", Description = "Free audio editor and recorder" },
new() { Name = "Blender", WingetId = "BlenderFoundation.Blender", Category = "Creativity", Description = "Free 3D creation suite" },

// Networking & VPN
new() { Name = "qBittorrent", WingetId = "qBittorrent.qBittorrent", Category = "Networking & VPN", Description = "Free open-source BitTorrent client" },
new() { Name = "ProtonVPN", WingetId = "ProtonTechnologies.ProtonVPN", Category = "Networking & VPN", Description = "Free privacy-focused VPN" },
new() { Name = "WireGuard", WingetId = "WireGuard.WireGuard", Category = "Networking & VPN", Description = "Fast modern VPN protocol" },
new() { Name = "PuTTY", WingetId = "SimonTatham.PuTTY", Category = "Networking & VPN", Description = "SSH and Telnet client" },

// Runtimes & Frameworks
new() { Name = ".NET Desktop Runtime 8", WingetId = "Microsoft.DotNet.DesktopRuntime.8", Category = "Runtimes & Frameworks", Description = "Required by many modern apps" },
new() { Name = "Visual C++ Redistributable", WingetId = "Microsoft.VCRedist.2015+.x64", Category = "Runtimes & Frameworks", Description = "Required by games and apps" },
new() { Name = "Java Runtime", WingetId = "Oracle.JavaRuntimeEnvironment", Category = "Runtimes & Frameworks", Description = "Required by Minecraft and enterprise apps" },
new() { Name = "DirectX Runtime", WingetId = "Microsoft.DirectX", Category = "Runtimes & Frameworks", Description = "Required by most games" },

// More Utilities
new() { Name = "WinRAR", WingetId = "RARLab.WinRAR", Category = "Utilities", Description = "Popular file archiver" },
new() { Name = "ShareX", WingetId = "ShareX.ShareX", Category = "Utilities", Description = "Screenshot and screen recording tool" },
new() { Name = "Greenshot", WingetId = "Greenshot.Greenshot", Category = "Utilities", Description = "Lightweight screenshot tool" },
new() { Name = "TreeSize Free", WingetId = "JAMSoftware.TreeSize.Free", Category = "Utilities", Description = "Visualize disk space usage" },

// More Communication
new() { Name = "WhatsApp", WingetId = "WhatsApp.WhatsApp", Category = "Communication", Description = "Messaging app for desktop" },
new() { Name = "Microsoft Teams", WingetId = "Microsoft.Teams", Category = "Communication", Description = "Business communication platform" },
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Curated app count does not meet the stated PR objective.

This curated list currently contains 46 entries, while the PR objective states 47 curated apps. Please either add the missing app entry or update the objective/tests to match the intended shipped scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/ViewModels/BulkInstallerViewModel.cs` around lines 272
- 345, The BuildCuratedApps method returns 46 InstallableApp entries but the PR
objective/tests expect 47; update BuildCuratedApps to either add the missing
InstallableApp entry (create a new new() { Name = "...", WingetId = "...",
Category = "...", Description = "..." } in the returned list) so the list
contains 47 items, or adjust the PR objective/tests to expect 46; locate the
list inside the BuildCuratedApps function and modify it accordingly, ensuring
the InstallableApp objects remain correctly formed.

Comment on lines +43 to +48
[RelayCommand]
private void RelaunchElevated()
{
if (AdminHelper.RelaunchAsAdmin())
System.Windows.Application.Current?.Shutdown();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add feedback when elevation relaunch is cancelled/blocked.

Line 46 handles only success; if relaunch fails, users get no indication of what happened.

Suggested patch
 [RelayCommand]
 private void RelaunchElevated()
 {
-    if (AdminHelper.RelaunchAsAdmin())
-        System.Windows.Application.Current?.Shutdown();
+    if (AdminHelper.RelaunchAsAdmin())
+    {
+        System.Windows.Application.Current?.Shutdown();
+        return;
+    }
+
+    StatusMessage = "Could not relaunch as administrator.";
+    Log.Warning("Windows Features: relaunch as administrator was not started");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SysManager/SysManager/ViewModels/WindowsFeaturesViewModel.cs` around lines 43
- 48, The RelaunchElevated method only shuts down the app on success and
provides no user feedback when AdminHelper.RelaunchAsAdmin() returns false;
update RelaunchElevated to detect the false return and display a clear
user-facing notification (e.g., MessageBox/notification via the VM's messaging
service) indicating that elevation was cancelled or blocked and any recommended
next steps; keep the existing shutdown behavior when RelaunchAsAdmin() returns
true and reference the RelaunchElevated method and AdminHelper.RelaunchAsAdmin()
when making the change.

@laurentiu021 laurentiu021 merged commit bf772e5 into main May 22, 2026
4 of 5 checks passed
@laurentiu021 laurentiu021 deleted the fix/design-polish-batch branch May 22, 2026 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants