diff --git a/PowerToys.sln b/PowerToys.sln
index 4e5524c757..4c9c92ce47 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -811,6 +811,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2945,6 +2947,14 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.Build.0 = Debug|ARM64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.ActiveCfg = Debug|x64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.Build.0 = Debug|x64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.ActiveCfg = Release|ARM64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3267,6 +3277,7 @@ Global
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj
new file mode 100644
index 0000000000..73abdbe772
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+ false
+ Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests
+ true
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/UrlHelperTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/UrlHelperTests.cs
new file mode 100644
index 0000000000..8635a5e3c5
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests/UrlHelperTests.cs
@@ -0,0 +1,274 @@
+// 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;
+using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests;
+
+[TestClass]
+public class UrlHelperTests
+{
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ [DataRow("\t")]
+ [DataRow("\r\n")]
+ public void IsValidUrl_ReturnsFalse_WhenUrlIsNullOrWhitespace(string url)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(url);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ [DataRow("test\nurl")]
+ [DataRow("test\rurl")]
+ [DataRow("http://example.com\nmalicious")]
+ [DataRow("https://test.com\r\nheader")]
+ public void IsValidUrl_ReturnsFalse_WhenUrlContainsNewlines(string url)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(url);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ [DataRow("com")]
+ [DataRow("org")]
+ [DataRow("localhost")]
+ [DataRow("test")]
+ [DataRow("http")]
+ [DataRow("https")]
+ public void IsValidUrl_ReturnsFalse_WhenUrlDoesNotContainDot(string url)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(url);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ [DataRow("https://www.example.com")]
+ [DataRow("http://test.org")]
+ [DataRow("ftp://files.example.net")]
+ [DataRow("file://localhost/path/to/file.txt")]
+ [DataRow("https://subdomain.example.co.uk")]
+ [DataRow("http://192.168.1.1")]
+ [DataRow("https://example.com:8080/path")]
+ public void IsValidUrl_ReturnsTrue_WhenUrlIsWellFormedAbsolute(string url)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(url);
+
+ // Assert
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ [DataRow("www.example.com")]
+ [DataRow("example.org")]
+ [DataRow("subdomain.test.net")]
+ [DataRow("github.com/user/repo")]
+ [DataRow("stackoverflow.com/questions/123")]
+ [DataRow("192.168.1.1")]
+ public void IsValidUrl_ReturnsTrue_WhenUrlIsValidWithoutProtocol(string url)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(url);
+
+ // Assert
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ [DataRow("not a url")]
+ [DataRow("invalid..url")]
+ [DataRow("http://")]
+ [DataRow("https://")]
+ [DataRow("://example.com")]
+ [DataRow("ht tp://example.com")]
+ public void IsValidUrl_ReturnsFalse_WhenUrlIsInvalid(string url)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(url);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ [DataRow(" https://www.example.com ")]
+ [DataRow("\t\tgithub.com\t\t")]
+ [DataRow(" \r\n stackoverflow.com \r\n ")]
+ public void IsValidUrl_TrimsWhitespace_BeforeValidation(string url)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(url);
+
+ // Assert
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ [DataRow("tel:+1234567890")]
+ [DataRow("javascript:alert('test')")]
+ public void IsValidUrl_ReturnsFalse_ForNonWebProtocols(string url)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(url);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ [DataRow(null)]
+ [DataRow("")]
+ [DataRow(" ")]
+ public void NormalizeUrl_ReturnsInput_WhenUrlIsNullOrWhitespace(string url)
+ {
+ // Act
+ var result = UrlHelper.NormalizeUrl(url);
+
+ // Assert
+ Assert.AreEqual(url, result);
+ }
+
+ [TestMethod]
+ [DataRow("https://www.example.com")]
+ [DataRow("http://test.org")]
+ [DataRow("ftp://files.example.net")]
+ [DataRow("file://localhost/path/to/file.txt")]
+ public void NormalizeUrl_ReturnsUnchanged_WhenUrlIsAlreadyWellFormed(string url)
+ {
+ // Act
+ var result = UrlHelper.NormalizeUrl(url);
+
+ // Assert
+ Assert.AreEqual(url, result);
+ }
+
+ [TestMethod]
+ [DataRow("www.example.com", "https://www.example.com")]
+ [DataRow("example.org", "https://example.org")]
+ [DataRow("github.com/user/repo", "https://github.com/user/repo")]
+ [DataRow("stackoverflow.com/questions/123", "https://stackoverflow.com/questions/123")]
+ public void NormalizeUrl_AddsHttpsPrefix_WhenNoProtocolPresent(string input, string expected)
+ {
+ // Act
+ var result = UrlHelper.NormalizeUrl(input);
+
+ // Assert
+ Assert.AreEqual(expected, result);
+ }
+
+ [TestMethod]
+ [DataRow(" www.example.com ", "https://www.example.com")]
+ [DataRow("\t\tgithub.com\t\t", "https://github.com")]
+ [DataRow(" \r\n stackoverflow.com \r\n ", "https://stackoverflow.com")]
+ public void NormalizeUrl_TrimsWhitespace_BeforeNormalizing(string input, string expected)
+ {
+ // Act
+ var result = UrlHelper.NormalizeUrl(input);
+
+ // Assert
+ Assert.AreEqual(expected, result);
+ }
+
+ [TestMethod]
+ [DataRow(@"C:\Users\Test\Documents\file.txt")]
+ [DataRow(@"D:\Projects\MyProject\readme.md")]
+ [DataRow(@"E:\")]
+ [DataRow(@"F:")]
+ [DataRow(@"G:\folder\subfolder")]
+ public void IsValidUrl_ReturnsTrue_ForValidLocalPaths(string path)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(path);
+
+ // Assert
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ [DataRow(@"\\server\share")]
+ [DataRow(@"\\server\share\folder")]
+ [DataRow(@"\\192.168.1.100\public")]
+ [DataRow(@"\\myserver\documents\file.docx")]
+ [DataRow(@"\\domain.com\share\folder\file.pdf")]
+ public void IsValidUrl_ReturnsTrue_ForValidNetworkPaths(string path)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(path);
+
+ // Assert
+ Assert.IsTrue(result);
+ }
+
+ [TestMethod]
+ [DataRow(@"\\")]
+ [DataRow(@":")]
+ [DataRow(@"Z")]
+ [DataRow(@"folder")]
+ [DataRow(@"folder\file.txt")]
+ [DataRow(@"documents\project\readme.md")]
+ [DataRow(@"./config/settings.json")]
+ [DataRow(@"../data/input.csv")]
+ public void IsValidUrl_ReturnsFalse_ForInvalidPathsAndRelativePaths(string path)
+ {
+ // Act
+ var result = UrlHelper.IsValidUrl(path);
+
+ // Assert
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ [DataRow(@"C:\Users\Test\Documents\file.txt")]
+ [DataRow(@"D:\Projects\MyProject")]
+ [DataRow(@"E:\")]
+ public void NormalizeUrl_ConvertsLocalPathToFileUri_WhenValidLocalPath(string path)
+ {
+ // Act
+ var result = UrlHelper.NormalizeUrl(path);
+
+ // Assert
+ Assert.IsTrue(result.StartsWith("file:///", StringComparison.OrdinalIgnoreCase));
+ Assert.IsTrue(result.Contains(path.Replace('\\', '/')));
+ }
+
+ [TestMethod]
+ [DataRow(@"\\server\share")]
+ [DataRow(@"\\192.168.1.100\public")]
+ [DataRow(@"\\myserver\documents")]
+ public void NormalizeUrl_ConvertsNetworkPathToFileUri_WhenValidNetworkPath(string path)
+ {
+ // Act
+ var result = UrlHelper.NormalizeUrl(path);
+
+ // Assert
+ Assert.IsTrue(result.StartsWith("file://", StringComparison.OrdinalIgnoreCase));
+ Assert.IsTrue(result.Contains(path.Replace('\\', '/')));
+ }
+
+ [TestMethod]
+ [DataRow("file:///C:/Users/Test/file.txt")]
+ [DataRow("file://server/share/folder")]
+ public void NormalizeUrl_ReturnsUnchanged_WhenAlreadyFileUri(string fileUri)
+ {
+ // Act
+ var result = UrlHelper.NormalizeUrl(fileUri);
+
+ // Assert
+ Assert.AreEqual(fileUri, result);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs
new file mode 100644
index 0000000000..60e7851761
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs
@@ -0,0 +1,144 @@
+// 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;
+using System.IO;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
+
+internal static class UrlHelper
+{
+ ///
+ /// Validates if a string is a valid URL or file path
+ ///
+ /// The string to validate
+ /// True if the string is a valid URL or file path, false otherwise
+ internal static bool IsValidUrl(string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ return false;
+ }
+
+ // Trim whitespace for validation
+ url = url.Trim();
+
+ // URLs should not contain newlines
+ if (url.Contains('\n', StringComparison.Ordinal) || url.Contains('\r', StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ // Check if it's a valid file path (local or network)
+ if (IsValidFilePath(url))
+ {
+ return true;
+ }
+
+ if (!url.Contains('.', StringComparison.OrdinalIgnoreCase))
+ {
+ // eg: 'com', 'org'. We don't think it's a valid url.
+ // This can simplify the logic of checking if the url is valid.
+ return false;
+ }
+
+ if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
+ {
+ return true;
+ }
+
+ if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
+ !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
+ !url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) &&
+ !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
+ {
+ if (Uri.IsWellFormedUriString("https://" + url, UriKind.Absolute))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Normalizes a URL or file path by adding appropriate schema if none is present
+ ///
+ /// The URL or file path to normalize
+ /// Normalized URL or file path with schema
+ internal static string NormalizeUrl(string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ return url;
+ }
+
+ // Trim whitespace
+ url = url.Trim();
+
+ // If it's a valid file path, convert to file:// URI
+ if (IsValidFilePath(url) && !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ // Convert to file URI (path is already absolute since we only accept absolute paths)
+ return new Uri(url).ToString();
+ }
+ catch
+ {
+ // If conversion fails, return original
+ return url;
+ }
+ }
+
+ if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
+ {
+ if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
+ !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) &&
+ !url.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) &&
+ !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
+ {
+ url = "https://" + url;
+ }
+ }
+
+ return url;
+ }
+
+ ///
+ /// Checks if a string represents a valid file path (local or network)
+ ///
+ /// The string to check
+ /// True if the string is a valid file path, false otherwise
+ private static bool IsValidFilePath(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return false;
+ }
+
+ try
+ {
+ // Check for UNC paths (network paths starting with \\)
+ if (path.StartsWith(@"\\", StringComparison.Ordinal))
+ {
+ // Basic UNC path validation: \\server\share or \\server\share\path
+ var parts = path.Substring(2).Split('\\', StringSplitOptions.RemoveEmptyEntries);
+ return parts.Length >= 2; // At minimum: server and share
+ }
+
+ // Check for drive letters (C:\ or C:)
+ if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
+ {
+ return true;
+ }
+
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/KeyChords.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/KeyChords.cs
index 5d59d0d1f2..e30969b56c 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/KeyChords.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/KeyChords.cs
@@ -2,11 +2,6 @@
// 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;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
@@ -16,4 +11,6 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
internal static class KeyChords
{
internal static KeyChord DeleteEntry { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete);
+
+ internal static KeyChord OpenUrl { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.O);
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs
index ac19335bfa..9b5aae6f7d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs
@@ -22,6 +22,7 @@ internal sealed partial class ClipboardListItem : ListItem
private readonly CommandContextItem _deleteContextMenuItem;
private readonly CommandContextItem? _pasteCommand;
private readonly CommandContextItem? _copyCommand;
+ private readonly CommandContextItem? _openUrlCommand;
private readonly Lazy _lazyDetails;
public override IDetails? Details
@@ -72,11 +73,26 @@ internal sealed partial class ClipboardListItem : ListItem
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager));
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text));
+
+ // Check if the text content is a valid URL and add OpenUrl command
+ if (UrlHelper.IsValidUrl(_item.Content ?? string.Empty))
+ {
+ var normalizedUrl = UrlHelper.NormalizeUrl(_item.Content ?? string.Empty);
+ _openUrlCommand = new CommandContextItem(new OpenUrlCommand(normalizedUrl))
+ {
+ RequestedShortcut = KeyChords.OpenUrl,
+ };
+ }
+ else
+ {
+ _openUrlCommand = null;
+ }
}
else
{
_pasteCommand = null;
_copyCommand = null;
+ _openUrlCommand = null;
}
RefreshCommands();
@@ -99,12 +115,7 @@ internal sealed partial class ClipboardListItem : ListItem
{
case PrimaryAction.Paste:
Command = _pasteCommand?.Command;
- MoreCommands =
- [
- _copyCommand!,
- new Separator(),
- _deleteContextMenuItem,
- ];
+ MoreCommands = BuildMoreCommands(_copyCommand);
if (_item.IsText)
{
@@ -124,12 +135,7 @@ internal sealed partial class ClipboardListItem : ListItem
case PrimaryAction.Copy:
default:
Command = _copyCommand?.Command;
- MoreCommands =
- [
- _pasteCommand!,
- new Separator(),
- _deleteContextMenuItem,
- ];
+ MoreCommands = BuildMoreCommands(_pasteCommand);
if (_item.IsText)
{
@@ -148,6 +154,26 @@ internal sealed partial class ClipboardListItem : ListItem
}
}
+ private IContextItem[] BuildMoreCommands(CommandContextItem? firstCommand)
+ {
+ var commands = new List();
+
+ if (firstCommand != null)
+ {
+ commands.Add(firstCommand);
+ }
+
+ if (_openUrlCommand != null)
+ {
+ commands.Add(_openUrlCommand);
+ }
+
+ commands.Add(new Separator());
+ commands.Add(_deleteContextMenuItem);
+
+ return commands.ToArray();
+ }
+
private Details CreateDetails()
{
IDetailsElement[] metadata =
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..fbc2b32860
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// 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.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests")]
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.resx
index 70226f7292..0af6ee4cfc 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.resx
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Properties/Resources.resx
@@ -183,4 +183,7 @@
Copy to Clipboard
+
+ Open URL
+
\ No newline at end of file