Compare commits

...

8 Commits

Author SHA1 Message Date
Michael Jolley
b8ced06f9d Merge branch 'main' into microsoft-powertoys-cmdpal/instance/-47482 2026-06-25 22:48:13 -05:00
Niels Laute
c777fcc1e4 Fix CmdPal gallery crash when extension has no homepage (#48869)
## Summary

Selecting an extension in the CmdPal Extension Gallery crashed the app
when that extension had **no `homepage`** defined.

## Root cause

In `ExtensionGalleryItemPage.xaml`, the "View repository"
`HyperlinkButton` bound its `NavigateUri` (a `System.Uri`) directly to
the raw string property `ViewModel.Homepage`:

```xml
NavigateUri="{x:Bind ViewModel.Homepage, Mode=OneWay}"
```

`x:Bind` evaluates **all** bindings on an element regardless of
`Visibility`, and to assign a `string` to a `Uri` target it generates a
`new Uri(value)` conversion. When `homepage` is undefined, `Homepage` is
`null`, so the binding executes `new Uri(null)` →
`ArgumentNullException` → the page crashes on load. The
`Visibility="{... HasHomepage}"` collapse did not help because the
`NavigateUri` binding still runs.

Every other `NavigateUri` x:Bind in the codebase (`Link`, `LinkUri`) is
already bound to a `Uri?`, so the homepage hyperlink was the lone
offender.

## Fix

- **`ExtensionGalleryItemViewModel.cs`** — Added a validated `Uri?
HomepageUri` property backed by the existing `_homepageHttpUri` (already
`null` for missing or non-web homepages).
- **`ExtensionGalleryItemPage.xaml`** — Bound `NavigateUri` to
`ViewModel.HomepageUri` instead of the raw string. `null` is valid for
`NavigateUri`, so no conversion occurs. The tooltip still shows the
`Homepage` string.
- **Tests** — Added coverage asserting `HomepageUri` is set for a web
homepage and `null` when missing.

## Verification

- `Microsoft.CmdPal.UI.ViewModels`, `Microsoft.CmdPal.UI` (XAML
compile), and the unit test project all built cleanly (x64/Release, exit
code 0).
- All 24 `ExtensionGalleryItemViewModelTests` pass, including the two
new cases.
- Manually verified in VS that opening an extension without a homepage
no longer crashes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>



https://github.com/user-attachments/assets/b268bafb-6bee-4862-9fbf-7a0e06675e36

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 17:14:37 -05:00
Michael Jolley
28e078897a [CmdPal] Fix memory leak in PerformanceWidgetsPage network band items (#48880)
## Summary

Fixes a memory leak in the Performance Monitor dock extension where
`GetItems()` created **new** `ListItem` instances for `_networkUpItem`
and `_networkDownItem` on every call.

## Problem

When the dock subscribes to `ItemsChanged` and calls `GetItems()` to
refresh, the band page path allocates 2 new `ListItem` objects each time
— the old ones are replaced in the fields but never collected (they
remain referenced by the `DockItemViewModel` wrappers until the next
refresh cycle). Under normal operation this leaks ~2 objects/second
indefinitely.

## Fix

Move `_networkUpItem`/`_networkDownItem` creation into the constructor
(matching the pattern used by CPU, Memory, GPU, and Battery items).
`GetItems()` now returns stable references. The `Updated` event handler
already updates their `.Title` properties, which propagates to the UI
via `PropChanged` → `CommandItemViewModel.Model_PropChanged`.

## Validation

- Build succeeds (`Microsoft.CmdPal.Ext.PerformanceMonitor.csproj`)
- Network up/down band items still receive title updates via the
existing `Updated` handler
- No `RaiseItemsChanged()` needed — `ListItem.Title` setter fires
`PropChanged`, which `DockItemViewModel` already observes

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 21:06:13 +00:00
Michael Jolley
64f1243bdf Skip auto-labeling PRs that already have labels (#48877)
## Summary

The auto-labeler workflow now skips pull requests that already have
labels applied before running the AI classification. This avoids
overwriting or duplicating labels that were manually set by contributors
or maintainers.

## Changes

- Added a check in `labelIssue()` that returns early for PRs with
existing labels, logging which labels are already present.
- Issues continue to be labeled regardless (only PRs get the skip
logic).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 19:22:58 +00:00
Mario Hewardt
e1074bc835 ZoomIt - Update notices (#48843)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Update notices for ZoomIt dependency

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-06-25 17:31:51 +02:00
Michael Jolley
2390aacbfc CmdPal: Prevent same-page settings navigation (#48703)
Fixes #48698 by preventing the Command Palette settings frame from
navigating to the same page again, which avoids adding a self-navigation
entry to the back stack.

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 14:54:39 +02:00
Michael Jolley
02af47f942 Use left-aligned padding and fix edge-case boundary jitter
- Switch from right-aligned {0,4:...} to left-aligned {0,-4:...} so
  values appear left-aligned as before, with trailing space padding
- Use MathF.Round(value, 1) < 10f instead of value < 10 to prevent
  jitter at the decimal/integer boundary (9.95 rounding to '10.0')
- Replace ambiguous {0,4:0.} format with clear {0,-4:0}

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 22:25:29 -05:00
root
826659f0a7 fix(cmdpal): use fixed-width number formatting to reduce dock jitter (#47482)
Instead of bumping MinWidth on all dock items (which wastes space on
non-PerfMon items), fix the root cause: PerfMon network speed format
functions now use right-aligned fixed-width number fields ({0,4:...})
so that text length remains stable as values change magnitude.

Reverts the MinWidth bump (48→24) from the previous approach.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-24 20:30:30 -05:00
7 changed files with 167 additions and 55 deletions

View File

@@ -73,6 +73,13 @@ jobs:
const itemType = issue.pull_request ? 'Pull request' : 'Issue';
// Skip pull requests that already have labels applied.
if (issue.pull_request && issue.labels && issue.labels.length > 0) {
const existingLabels = issue.labels.map(l => l.name).join(', ');
console.log(`${itemType} #${issueNumber} already has labels (${existingLabels}); skipping.`);
return;
}
const title = issue.title ?? '';
const body = issue.body ?? '';

View File

@@ -12,6 +12,7 @@ This software incorporates material from third parties.
- Peek
- PowerDisplay
- Registry Preview
- ZoomIt
## Utility: Color Picker
@@ -1549,6 +1550,69 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Utility: ZoomIt
### libwebp
ZoomIt uses libwebp to encode screenshots in the WebP image format.
**Source**: <https://github.com/webmproject/libwebp>
BSD-3-Clause License
Copyright (c) 2010, Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Google nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Additional IP Rights Grant (Patents)
"These implementations" means the copyrightable works that implement the WebM
codecs distributed by Google as part of the WebM Project.
Google hereby grants to you a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable (except as stated in this section) patent license to
make, have made, use, offer to sell, sell, import, transfer, and otherwise
run, modify and propagate the contents of these implementations of WebM, where
such license applies only to those patent claims, both currently owned by
Google and acquired in the future, licensable by Google that are necessarily
infringed by these implementations of WebM. This grant does not include claims
that would be infringed only as a consequence of further modification of these
implementations. If you or your agent or exclusive licensee institute or order
or agree to the institution of patent litigation or any other patent
enforcement activity against any entity (including a cross-claim or
counterclaim in a lawsuit) alleging that any of these implementations of WebM
or any code incorporated within any of these implementations of WebM
constitute direct or contributory patent infringement, or inducement of
patent infringement, then any patent rights granted to you under this License
for these implementations of WebM shall terminate as of the date such
litigation is filed.
## NuGet Packages used by PowerToys
- AdaptiveCards.ObjectModel.WinUI3

View File

@@ -105,6 +105,13 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
public string? Homepage => _entry.Homepage;
// Validated, browser-openable homepage uri. Null when the entry has no
// homepage or it is not a web uri. NavigateUri bindings must use this
// (a Uri) rather than the raw Homepage string: x:Bind evaluates bindings
// regardless of element visibility, and converting a null/invalid string
// to Uri throws and crashes the page.
public Uri? HomepageUri => _homepageHttpUri;
public Uri IconUri { get; }
public ImageSource IconSource

View File

@@ -232,7 +232,7 @@
Grid.Row="3"
Padding="0"
AutomationProperties.AutomationId="CmdPal_GalleryItemPage_ViewRepository"
NavigateUri="{x:Bind ViewModel.Homepage, Mode=OneWay}"
NavigateUri="{x:Bind ViewModel.HomepageUri, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.Homepage, Mode=OneWay}"
Visibility="{x:Bind help:BindTransformers.BoolToVisibility(ViewModel.HasHomepage), Mode=OneWay, FallbackValue=Collapsed}">
<StackPanel Orientation="Horizontal" Spacing="4">

View File

@@ -160,20 +160,24 @@ public sealed partial class SettingsWindow : WindowEx,
break;
}
if (pageType is not null)
if (pageType is null)
{
NavFrame.Navigate(pageType);
return;
}
// Now, make sure to actually select the correct menu item too
foreach (var obj in NavView.MenuItems)
if (NavFrame.Content?.GetType() == pageType)
{
return;
}
NavFrame.Navigate(pageType);
// Now, make sure to actually select the correct menu item too
foreach (var obj in NavView.MenuItems)
{
if (obj is NavigationViewItem item && item.Tag is string s && s == page)
{
if (obj is NavigationViewItem item)
{
if (item.Tag is string s && s == page)
{
NavView.SelectedItem = item;
}
}
NavView.SelectedItem = item;
}
}
}

View File

@@ -119,6 +119,7 @@ public class ExtensionGalleryItemViewModelTests
var viewModel = CreateViewModel(entry);
Assert.IsFalse(viewModel.HasHomepage);
Assert.IsNull(viewModel.HomepageUri);
Assert.IsFalse(viewModel.HasAuthorUrl);
Assert.IsFalse(viewModel.HasUrlSource);
Assert.IsFalse(viewModel.HasActionableSourceDetails);
@@ -131,6 +132,32 @@ public class ExtensionGalleryItemViewModelTests
Assert.IsFalse(viewModel.OpenInstallUrlCommand.CanExecute(null));
}
[TestMethod]
public void Constructor_SetsHomepageUri_WhenHomepageIsWebUri()
{
var entry = CreateEntry(iconUrl: null);
entry.Homepage = "https://example.com/extension";
var viewModel = CreateViewModel(entry);
Assert.IsTrue(viewModel.HasHomepage);
Assert.AreEqual(new Uri("https://example.com/extension"), viewModel.HomepageUri);
Assert.IsTrue(viewModel.OpenHomepageCommand.CanExecute(null));
}
[TestMethod]
public void Constructor_LeavesHomepageUriNull_WhenHomepageIsMissing()
{
var entry = CreateEntry(iconUrl: null);
entry.Homepage = null;
var viewModel = CreateViewModel(entry);
Assert.IsFalse(viewModel.HasHomepage);
Assert.IsNull(viewModel.HomepageUri);
Assert.IsFalse(viewModel.OpenHomepageCommand.CanExecute(null));
}
[TestMethod]
public void Constructor_EnablesCopyCommand_WhenWinGetIdIsAvailable()
{

View File

@@ -134,6 +134,25 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
MoreCommands = _networkPage.Commands,
};
if (isBandPage)
{
_networkUpItem = new ListItem(_networkPage)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
Icon = Icons.NetworkUpIcon,
MoreCommands = _networkPage.Commands,
};
_networkDownItem = new ListItem(_networkPage)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
Icon = Icons.NetworkDownIcon,
MoreCommands = _networkPage.Commands,
};
}
_networkPage.Updated += (s, e) =>
{
_networkItem.Title = _networkPage.GetItemTitle(isBandPage);
@@ -253,22 +272,6 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
}
else
{
_networkUpItem = new ListItem(_networkPage!)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
Icon = Icons.NetworkUpIcon,
MoreCommands = _networkPage!.Commands,
};
_networkDownItem = new ListItem(_networkPage!)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
Icon = Icons.NetworkDownIcon,
MoreCommands = _networkPage!.Commands,
};
return _batteryItem is not null
? new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem!, _batteryItem! }
: new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem! };
@@ -783,34 +786,34 @@ internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDispos
value /= 1024;
if (value < 1024)
{
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Kbps", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} Kbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Kbps", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} Kbps", value);
}
// Kbits to Mbits
value /= 1024;
if (value < 1024)
{
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Mbps", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} Mbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Mbps", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} Mbps", value);
}
// Mbits to Gbits
value /= 1024;
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Gbps", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} Gbps", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} Gbps", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} Gbps", value);
}
private static string FormatAsBytesPerSecString(float value)
@@ -819,34 +822,34 @@ internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDispos
value /= 1024;
if (value < 1024)
{
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} KB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} KB/s", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} KB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} KB/s", value);
}
// KB to MB
value /= 1024;
if (value < 1024)
{
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} MB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} MB/s", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} MB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} MB/s", value);
}
// MB to GB
value /= 1024;
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} GB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} GB/s", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} GB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} GB/s", value);
}
private static string FormatAsBinaryBytesPerSecString(float value)
@@ -855,34 +858,34 @@ internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDispos
value /= 1024;
if (value < 1024)
{
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} KiB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} KiB/s", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} KiB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} KiB/s", value);
}
// KiB to MiB
value /= 1024;
if (value < 1024)
{
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} MiB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} MiB/s", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} MiB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} MiB/s", value);
}
// MiB to GiB
value /= 1024;
if (value < 100)
if (MathF.Round(value, 1) < 10f)
{
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} GiB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0.0} GiB/s", value);
}
return string.Format(CultureInfo.InvariantCulture, "{0:0} GiB/s", value);
return string.Format(CultureInfo.InvariantCulture, "{0,-4:0} GiB/s", value);
}
internal override void PushActivate()