-
Notifications
You must be signed in to change notification settings - Fork 7.5k
CmdPal: Use Shell API to determine the default browser in WebSearch #43339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
CmdPal: Use Shell API to determine the default browser in WebSearch #43339
Conversation
...dpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociationProviderBase.cs
Fixed
Show fixed
Hide fixed
This comment has been minimized.
This comment has been minimized.
@check-spelling-bot Report🔴 Please reviewSee the 📂 files view, the 📜action log, or 📝 job summary for details.
See ❌ Event descriptions for more information. These words are not needed and should be removedcabstr CIBUILD djwsxzxb icf installscopeperuser ksa Olllama rap registryroot regroot rtm suntimes TARGETDIR utmPattern suggestions ✂️ (1)You could add these patterns to Alternatively, if a pattern suggestion doesn't make sense for this project, add a If the flagged items are 🤯 false positivesIf items relate to a ...
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the default browser detection mechanism in the Command Palette's Web Search extension to use the Windows Shell API (AssocQueryStringW) as the primary method, with fallbacks to legacy registry access and Microsoft Edge. It introduces a service-based architecture with IBrowserInfoService and multiple provider implementations. The PR also fixes an issue where the browser name in the UI would not update when users changed their default browser while Command Palette was running.
Key Changes
- Introduced
IBrowserInfoServiceinterface andDefaultBrowserInfoServiceimplementation with provider chain pattern - Added
ShellAssociationProviderusing Windows Shell API for protocol associations (https/http) - Retained legacy registry-based detection as
LegacyRegistryAssociationProviderfor fallback compatibility - Updated all command items and pages to use the new browser info service for dynamic browser name retrieval
Reviewed Changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
Helpers/Browser/DefaultBrowserInfoService.cs |
Core service orchestrating browser detection with caching and provider fallback chain |
Helpers/Browser/Providers/ShellAssociationProvider.cs |
Primary provider using AssocQueryStringW to query https/http protocol associations |
Helpers/Browser/Providers/LegacyRegistryAssociationProvider.cs |
Fallback provider using registry access for older browser detection method |
Helpers/Browser/Providers/AssociationProviderBase.cs |
Base class providing shared command parsing and indirect string resolution logic |
Helpers/Browser/Providers/FallbackMsEdgeBrowserProvider.cs |
Final fallback returning hardcoded Microsoft Edge path |
Helpers/NativeMethods.cs |
P/Invoke declarations for AssocQueryStringW shell API |
Helpers/Browser/BrowserInfo.cs |
Record type containing browser path, name, and arguments pattern |
Helpers/Browser/IBrowserInfoService.cs |
Service interface for retrieving default browser information |
Helpers/Browser/BrowserInfoServiceExtensions.cs |
Extension method for opening URLs in the default browser |
Commands/SearchWebCommand.cs |
Updated to use IBrowserInfoService instead of static DefaultBrowserInfo |
Commands/OpenURLCommand.cs |
Updated to use IBrowserInfoService for opening URLs |
Pages/WebSearchListPage.cs |
Updated to dynamically retrieve browser name via service |
FallbackExecuteSearchItem.cs |
Updated to get browser name dynamically on each query update |
FallbackOpenURLItem.cs |
Updated to get browser name dynamically and changed IsValidUrl to private |
WebSearchTopLevelCommandItem.cs |
Constructor now accepts IBrowserInfoService parameter |
WebSearchCommandsProvider.cs |
Creates shared DefaultBrowserInfoService instance and passes to all commands |
Properties/Resources.resx |
Added "default browser" localization resource as fallback display text |
Tests/.../MockBrowserInfoService.cs |
Mock implementation returning fixed browser info for unit tests |
Tests/.../SettingsManagerTests.cs |
Updated test to pass mock browser service |
Tests/.../QueryTests.cs |
Updated tests to pass mock browser service |
Files not reviewed (1)
- src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs: Language not supported
| /// <summary> | ||
| /// Gets information about the system's default web browser. | ||
| /// </summary> | ||
| /// <returns></returns> |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incomplete XML documentation: The <returns> element is empty and should describe what the method returns.
Should be:
/// <returns>A <see cref="BrowserInfo"/> object containing information about the default browser, or null if the browser information cannot be determined.</returns>| /// <returns></returns> | |
| /// <returns>A <see cref="BrowserInfo"/> object containing information about the default browser, or null if the browser information cannot be determined.</returns> |
|
|
||
| public record BrowserInfo | ||
| { | ||
| public required string Path { get; init; } | ||
|
|
||
| public required string Name { get; init; } | ||
|
|
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing XML documentation for public type: The BrowserInfo record is public but lacks XML documentation. All public types, properties, and members should be documented with XML comments explaining their purpose.
Add documentation like:
/// <summary>
/// Contains information about a web browser installation.
/// </summary>
public record BrowserInfo
{
/// <summary>
/// Gets the file path to the browser executable or a packaged application URI.
/// </summary>
public required string Path { get; init; }
/// <summary>
/// Gets the user-friendly name of the browser.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Gets the command line arguments pattern for launching URLs, or null if not applicable.
/// </summary>
public string? ArgumentsPattern { get; init; }
}| public record BrowserInfo | |
| { | |
| public required string Path { get; init; } | |
| public required string Name { get; init; } | |
| /// <summary> | |
| /// Contains information about a web browser installation. | |
| /// </summary> | |
| public record BrowserInfo | |
| { | |
| /// <summary> | |
| /// Gets the file path to the browser executable or a packaged application URI. | |
| /// </summary> | |
| public required string Path { get; init; } | |
| /// <summary> | |
| /// Gets the user-friendly name of the browser. | |
| /// </summary> | |
| public required string Name { get; init; } | |
| /// <summary> | |
| /// Gets the command line arguments pattern for launching URLs, or null if not applicable. | |
| /// </summary> |
| "ProgId") | ||
| ?? GetRegistryValue( | ||
| @"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", | ||
| "ProgId"); |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference exception: On lines 23-24, progId could be null if both registry lookups fail (lines 17-22), but it's used in string interpolation without null checking. This will cause GetRegistryValue to be called with paths containing "null" as a string literal.
Add a null check:
var progId = GetRegistryValue(/*...*/)
?? GetRegistryValue(/*...*/);
if (string.IsNullOrEmpty(progId))
{
return null;
}
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");| "ProgId"); | |
| "ProgId"); | |
| if (string.IsNullOrEmpty(progId)) | |
| { | |
| return null; | |
| } |
| var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch]; | ||
|
|
||
| fixed (char* p = span) | ||
| { | ||
| var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch); | ||
| if (hr != 0 || cch == 0) |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential buffer overflow risk with stackalloc: On line 44, when cch > 512, a heap-allocated array is created, but when cch <= 512, stackalloc is used. However, the second call to AssocQueryStringW on line 48 might update cch to a larger value than initially reported, potentially overflowing the stack-allocated buffer.
The safer approach is to check cch hasn't changed after the second call:
var originalCch = cch;
var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch];
fixed (char* p = span)
{
var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch);
if (hr != 0 || cch == 0 || cch > originalCch)
{
return null;
}
// ...
}| var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch]; | |
| fixed (char* p = span) | |
| { | |
| var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch); | |
| if (hr != 0 || cch == 0) | |
| var originalCch = cch; | |
| var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch]; | |
| fixed (char* p = span) | |
| { | |
| var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch); | |
| if (hr != 0 || cch == 0 || cch > originalCch) |
| private readonly IBrowserInfoService _browserInfoService; | ||
|
|
||
| public string Arguments { get; internal set; } = string.Empty; | ||
| public string Arguments { get; internal set; } |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default initialization removed for Arguments property: The Arguments property no longer has a default value of string.Empty. This could lead to a null reference exception if the property is accessed before being set, even though the constructor does set it.
Consider adding the default initializer back for defensive programming:
public string Arguments { get; internal set; } = string.Empty;| public string Arguments { get; internal set; } | |
| public string Arguments { get; internal set; } = string.Empty; |
| _lastUpdateTickCount = curTickCount; | ||
| lock (_updateLock) | ||
| { |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition in UpdateIfTimePassed: There's a potential race condition where multiple threads could pass the timeout check (line 55) simultaneously before any of them updates _lastUpdateTickCount (line 60). This means multiple threads could enter the lock and call UpdateCore() unnecessarily.
Consider moving the timestamp update inside the lock:
public void UpdateIfTimePassed()
{
var curTickCount = Environment.TickCount64;
if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
{
return;
}
lock (_updateLock)
{
// Double-check after acquiring lock
if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
{
return;
}
_lastUpdateTickCount = curTickCount;
_defaultBrowser = UpdateCore();
}
}
```suggestion
lock (_updateLock)
{
// Double-check after acquiring lock
if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
{
return;
}
_lastUpdateTickCount = curTickCount;
Summary of the Pull Request
This PR introduces a new method for determining the default browser using the Windows Shell API. The new provider selects the browser associated with the HTTPS protocol (falling back to HTTP if necessary). The original implementation is retained as a fallback for now, and the codebase is prepared for future extensions (e.g., manual default-browser selection).
As a flyby, it also fixes an issue where commands continued showing the previous browser name if the user changed their default browser while the Command Palette was running.
One-liner for change log
Fixed default browser selection in the Web Search built-in extension.
PR Checklist
Detailed Description of the Pull Request / Additional comments
Validation Steps Performed