Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d01bd69
Import from jiripolasek/PowerToys
jiripolasek Mar 30, 2026
0d1cc4c
Merge main
jiripolasek Mar 30, 2026
d3448ee
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
niels9001 Mar 31, 2026
8ad1456
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
niels9001 Apr 1, 2026
b4ce152
Fix spell-check: add flagged words to allow/expect lists
Copilot Apr 1, 2026
f042f3a
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
niels9001 Apr 1, 2026
40c54b3
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
niels9001 Apr 3, 2026
d3ba693
More extension gallery changes (#46752)
niels9001 Apr 3, 2026
d81e784
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
jiripolasek Apr 3, 2026
570cb85
Drop Icon in favor of IconUrl
jiripolasek Apr 3, 2026
d133b6f
Improve gallery data caching, introduce a generialize http resource c…
jiripolasek Apr 3, 2026
71d342a
Reformat XAML
jiripolasek Apr 3, 2026
237b33a
Move the cache out of the backuped folders
jiripolasek Apr 3, 2026
cf76163
Revert "Fix spell-check: add flagged words to allow/expect lists"
jiripolasek Apr 3, 2026
ddcc462
Exclude extension gallery sample data from spellchecking
jiripolasek Apr 3, 2026
461e474
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
jiripolasek Apr 3, 2026
37bd1b4
Fix spellchecking, attempt 2
jiripolasek Apr 3, 2026
6892960
Placate spellchecker
jiripolasek Apr 3, 2026
4fd3401
Remove localization support
jiripolasek Apr 3, 2026
0e4da80
Make local testing feasible again
jiripolasek Apr 3, 2026
d233b22
Switch from caching manifests to caching the feed
jiripolasek Apr 3, 2026
496ce3f
Make the http cache more generic and improve pruning
jiripolasek Apr 3, 2026
5067cc2
Replace StackPanel in the gallery tile template with a grid to allow …
jiripolasek Apr 4, 2026
3b5d857
Merge main
jiripolasek Apr 4, 2026
46c62cc
S.P.E.L.L.
jiripolasek Apr 4, 2026
25aebbf
Replace static loggers with DI
jiripolasek Apr 4, 2026
fbc3a63
Refactor? You bet!
jiripolasek Apr 4, 2026
964e5af
CmdPal: Add screenshots to gallery (#46843)
jiripolasek Apr 10, 2026
29a2d5a
Adding a carousel (#46894)
niels9001 Apr 11, 2026
c3b9557
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
niels9001 Apr 15, 2026
d8da9ae
Push
niels9001 Apr 16, 2026
2e5b9f2
Run XAML Styler on SettingsWindow.xaml
niels9001 Apr 16, 2026
84d8aae
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
niels9001 Apr 17, 2026
8d7d788
[CmdPal] Remove dead 'merge WinGet results into gallery' plumbing
niels9001 Apr 17, 2026
30dc7b2
[CmdPal] Trim extension gallery docs to match runtime
niels9001 Apr 17, 2026
b1923b7
[CmdPal] Move extension gallery docs under doc/devdocs/modules/cmdpal
niels9001 Apr 17, 2026
6954902
[CmdPal] Drop local gallery manifest schema copy
niels9001 Apr 17, 2026
9de5fb7
Cleaning up some more docs
niels9001 Apr 17, 2026
f5e53f6
Apply suggestions from code review
niels9001 Apr 17, 2026
f7b2a12
Fix check-spelling forbidden pattern: add comma after 'Otherwise'
Copilot Apr 17, 2026
6632df1
Merge main
jiripolasek Apr 25, 2026
3c6dd4a
Fix extension detail gallery so it opens the screenshot the user clic…
jiripolasek Apr 25, 2026
36cbc7a
(Over-)Document that SettingsModel.GalleryFeedUrl is not used in CI b…
jiripolasek Apr 25, 2026
fee108b
Add localization, clean up obsolete entries in RESX/RESW, and move mi…
jiripolasek Apr 26, 2026
b5788c4
Drop old unused files
jiripolasek Apr 26, 2026
faa52d6
Drop native methods from WinGet
jiripolasek Apr 26, 2026
a44f2f2
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
jiripolasek Apr 26, 2026
9cc3f51
Potential fix for pull request finding
niels9001 May 14, 2026
6716168
Update doc feed URL references to use aka.ms short link
Copilot May 14, 2026
a511eb1
Merge branch 'main' into dev/jpolasek/f/46628-cmdpal-extension-gallery
niels9001 May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spell-check/allow/names.txt
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ Bilibili
BVID
capturevideosample
cmdow
contoso
Contoso
Controlz
cortana
Expand Down
4 changes: 2 additions & 2 deletions .github/actions/spell-check/excludes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@
^src/common/ManagedCommon/ColorFormatHelper\.cs$
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
^src/common/sysinternals/Eula/
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
^doc/devdocs/modules/cmdpal/initial-sdk-spec/list-elements-mock-002\.pdn$
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/.*\.TestData\.cs$
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/Text/.*\.cs$
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
Expand Down
1 change: 1 addition & 0 deletions .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,7 @@ resmimetype
RESOURCEID
RESTORETOMAXIMIZED
RETURNONLYFSDIRS
Revalidates
RGBQUAD
rgbs
rgelt
Expand Down
167 changes: 167 additions & 0 deletions doc/devdocs/modules/cmdpal/extension-gallery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Command Palette Extension Gallery

This document describes how Command Palette (CmdPal) discovers extensions for
the in-app **Extension gallery** page.

## At a glance

- The gallery loads a single JSON feed called `extensions.json` from a remote
HTTPS URL, parses it, and renders the entries.
- The default feed lives in the external repo
**`microsoft/CmdPal-Extensions`** at
`https://aka.ms/CmdPal-ExtensionsJson`.
- Feed content + icon images are cached on disk so the page works offline and
survives short network hiccups.
- There is no WinGet discovery, no per-extension `manifest.json` fetch, and no
other network call for rendering the list.

## Implementation pointers

| Concern | File |
| --- | --- |
| Fetching, parsing, caching, pruning | `Microsoft.CmdPal.Common/ExtensionGallery/Services/ExtensionGalleryService.cs` |
| Resolving which URL to fetch | `Microsoft.CmdPal.Common/ExtensionGallery/Services/GalleryFeedUrlProvider.cs` + `Microsoft.CmdPal.UI/Helpers/GalleryServiceRegistration.cs` |
| HTTP + on-disk cache | `Microsoft.CmdPal.Common/ExtensionGallery/Services/ExtensionGalleryHttpClient.cs` (wraps `Microsoft.CmdPal.Common/Services/HttpCaching/HttpCachingClient`) |
| Feed + entry models | `Microsoft.CmdPal.Common/ExtensionGallery/Models/` |

## Feed URL resolution

`ExtensionGalleryService.GetFeedUrl()` returns, in order:

1. The user-configured URL from CmdPal settings (`SettingsModel.GalleryFeedUrl`,
exposed via the hidden `InternalPage` settings page). Any non-empty value
wins. Mostly used for local testing against a custom feed.
2. Otherwise, the built-in default
`https://aka.ms/CmdPal-ExtensionsJson`.

Local `file://` URIs are allowed too — `FetchFeedDocumentAsync` reads the file
directly and bypasses the HTTP cache.

## Feed format

The feed is a single wrapped JSON document with inline entries:

```json
{
"$schema": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/.github/schemas/gallery.schema.json",
"extensions": [
{
"id": "sample-extension",
"title": "Sample Extension",
"description": "A sample extension demonstrating the gallery feed format.",
"author": { "name": "Microsoft", "url": "https://github.com/microsoft" },
"homepage": "https://github.com/microsoft/CmdPal-Extensions",
"iconUrl": "https://.../icon.png",
"screenshotUrls": ["https://.../screenshot-1.png"],
"tags": ["sample"],
"installSources": [
{ "type": "winget", "id": "Contoso.SampleExtension" },
{ "type": "msstore", "id": "9P…" },
{ "type": "url", "uri": "https://github.com/contoso/sample/releases/latest" }
],
"detection": { "packageFamilyName": "Contoso.SampleExtension_1234567890abc" }
}
]
}
```

Only the `extensions` array is read at runtime. The authoritative JSON
schema for an entry lives in the upstream feed repo
([`microsoft/CmdPal-Extensions`](https://github.com/microsoft/CmdPal-Extensions));
don't duplicate it here — it drifts.

### Required + optional entry fields

| Field | Required | Notes |
| --- | --- | --- |
| `id` | yes | Lowercase stable identifier; entries with empty id are dropped. |
| `title` | yes | Display name. |
| `description` | yes | Shown in list and detail views. |
| `author.name` | yes | `author.url` optional. |
| `installSources` | yes | At least one entry; see [Install sources](#install-sources). |
| `homepage`, `iconUrl`, `screenshotUrls`, `tags`, `detection.packageFamilyName` | no | All optional. |

Relative `iconUrl` / `screenshotUrls` are resolved against the feed URL's
directory (useful only for local / `file://` feeds during development).

## Install sources

Each entry's `installSources` is consumed by
`ExtensionGalleryItemViewModel` to decide which install affordances to show.

| `type` | Required field | Behaviour |
| --- | --- | --- |
| `winget` | `id` | Enables the "Install via WinGet" button (uses the shared WinGet service), and joins in-flight install progress + installed/update status. |
| `msstore` | `id` | Opens `ms-windows-store://pdp/?ProductId={id}`. |
| `url` | `uri` | Shown as a "GitHub" or "Website" link depending on host. |

An entry can declare any combination. Sources the runtime does not recognise
are surfaced as an "unknown source" indicator.

## Fetching and caching

`ExtensionGalleryService` uses `ExtensionGalleryHttpClient`, which wraps
`HttpCachingClient` over a file-system cache. Both the feed JSON and any
cacheable icon URLs are cached.

| Setting | Value | Defined in |
| --- | --- | --- |
| Cache root | `{AppCache}\GalleryCache\` | `ExtensionGalleryHttpClient.CacheDirectoryName` |
| Feed TTL | 4 hours | `ExtensionGalleryHttpClient.DefaultTimeToLive` |
| Icon TTL | 24 hours | `ExtensionGalleryService.IconCacheTtl` |
| HTTP timeout | 15 s | `ExtensionGalleryHttpClient` |
| `User-Agent` | `PowerToys-CmdPal/1.0` | `ExtensionGalleryHttpClient` |

`{AppCache}` resolves to `ApplicationData.Current.LocalCacheFolder` when
CmdPal runs packaged, and to
`%LOCALAPPDATA%\Microsoft\PowerToys\Microsoft.CmdPal\Cache\` when unpackaged
(see `ApplicationInfoService.DetermineCacheDirectory`).

### Fetch flow

`GetExtensionsAsync` (normal load) and `RefreshAsync` (user-initiated
refresh, `forceRefresh: true`) both go through `FetchWrappedFeedAsync`:

1. Resolve the feed URL (see above).
2. If the URL is local, read it from disk. Otherwise, hand it to
`HttpCachingClient.GetResourceAsync` which:
- Serves a fresh cached copy if one exists and TTL has not elapsed.
- Otherwise issues a conditional GET (ETag / `If-None-Match`). On `304
Not Modified` it refreshes the cache metadata and returns the cached
body.
- On network failure it returns the last-known cached body with
`UsedFallbackCache = true`, so the UI can show a "stale data" banner.
3. Parse the JSON with the source-generated `GallerySerializationContext`
(strongly-typed `GalleryRemoteIndex` — no reflection, AOT-friendly).
4. Drop entries with missing `id`, normalize relative `iconUrl` and
`screenshotUrls`, and resolve remote icon URIs through the same HTTP
cache so the UI binds to local `file://` URIs.
5. On a successful forced refresh, `PruneCachedResources` deletes cache
entries that are no longer referenced by the current feed (old feed URL
and icon URLs that dropped out of the feed).

### Fetch result flags

`GetExtensionsAsync` returns a `GalleryFetchResult` that the view model uses
for UI hints:

| Flag | Meaning |
| --- | --- |
| `FromCache` | The feed came from cache without hitting the network (TTL still valid). |
| `UsedFallbackCache` | A network request was attempted and failed, and the cached copy was served as fallback. The UI shows a stale-data info bar. |
| `RateLimited` | The origin returned `429 Too Many Requests` and no fallback was available. The UI shows a rate-limit error. |

## Authoring

- Entries for the production gallery are added to the feed repo
`microsoft/CmdPal-Extensions`.
- For editor validation of an entry, reference the schema published in the
upstream repo via the entry's `$schema` field.
- Keep `id` stable once an extension is published — users may have it
installed and the gallery keys install status by id.
- Prefer providing a `winget` source when the extension ships through App
Installer; the gallery uses it both for status ("Installed" / "Update
available") and for the in-app install button.
- `detection.packageFamilyName` lets the gallery recognise an
already-installed packaged extension before WinGet metadata resolves.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Common.ExtensionGallery.Models;

public sealed class GalleryAuthor
{
public string Name { get; set; } = string.Empty;

public string? Url { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Common.ExtensionGallery.Models;

public sealed class GalleryDetection
{
public string? PackageFamilyName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Common.ExtensionGallery.Models;

public sealed class GalleryExtensionEntry
{
public string Id { get; set; } = string.Empty;

public string Title { get; set; } = string.Empty;

public string Description { get; set; } = string.Empty;

public string? ShortDescription { get; set; }

public GalleryAuthor Author { get; set; } = new();

public string? Homepage { get; set; }

public string? Readme { get; set; }

public string? IconUrl { get; set; }

public List<string> ScreenshotUrls { get; set; } = [];

public List<GalleryInstallSource> InstallSources { get; set; } = [];

public GalleryDetection? Detection { get; set; }

public List<string> Tags { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Common.ExtensionGallery.Models;

public sealed class GalleryInstallSource
{
public string Type { get; set; } = string.Empty;

public string? Id { get; set; }

public string? Uri { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CmdPal.Common.ExtensionGallery.Models;

/// <summary>
/// Represents the wrapped gallery index format where extension data is inline.
/// </summary>
public sealed class GalleryRemoteIndex
{
public List<GalleryExtensionEntry> Extensions { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;

namespace Microsoft.CmdPal.Common.ExtensionGallery.Models;

[JsonSerializable(typeof(GalleryExtensionEntry))]
[JsonSerializable(typeof(GalleryRemoteIndex))]
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
public sealed partial class GallerySerializationContext : JsonSerializerContext
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Common.Services.HttpCaching;
using Microsoft.Extensions.Logging;

namespace Microsoft.CmdPal.Common.ExtensionGallery.Services;

/// <summary>
/// Identifies the HTTP client instance used by the extension gallery.
/// </summary>
public sealed partial class ExtensionGalleryHttpClient : IDisposable
{
internal const string CacheDirectoryName = "GalleryCache";
private const int TimeoutSeconds = 15;
private const string UserAgent = "PowerToys-CmdPal/1.0";
private readonly HttpCachingClient _cache;

internal static readonly TimeSpan DefaultTimeToLive = TimeSpan.FromHours(4);

public ExtensionGalleryHttpClient(IApplicationInfoService applicationInfoService, ILogger<ExtensionGalleryHttpClient> logger)
: this(applicationInfoService, innerHandler: null, logger)
{
}

internal ExtensionGalleryHttpClient(IApplicationInfoService applicationInfoService, HttpMessageHandler? innerHandler, ILogger<ExtensionGalleryHttpClient> logger)
: this(
Path.Combine(applicationInfoService.CacheDirectory, CacheDirectoryName),
innerHandler,
logger)
{
ArgumentNullException.ThrowIfNull(applicationInfoService);
}

internal ExtensionGalleryHttpClient(string cacheDirectory, HttpMessageHandler? innerHandler, ILogger<ExtensionGalleryHttpClient> logger)
{
ArgumentException.ThrowIfNullOrWhiteSpace(cacheDirectory);
ArgumentNullException.ThrowIfNull(logger);

_cache = new HttpCachingClient(
cacheDirectory,
DefaultTimeToLive,
TimeSpan.FromSeconds(TimeoutSeconds),
UserAgent,
innerHandler,
logger);
}

internal HttpCachingClient Cache => _cache;

public void Dispose()
{
_cache.Dispose();
}
}
Loading
Loading