diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 9f18bbb300..c77154b466 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -727,7 +727,6 @@ HWNDPARENT
HWNDPREV
hyjiacan
IAI
-icf
ICONERROR
ICONLOCATION
IDCANCEL
@@ -800,6 +799,7 @@ invokecommand
ipcmanager
IPREVIEW
ipreviewhandlervisualssetfont
+irdp
IPTC
irow
irprops
@@ -1057,6 +1057,7 @@ msrc
msstore
msvcp
MT
+mstsc
MTND
MULTIPLEUSE
multizone
@@ -1206,8 +1207,11 @@ OOBEUI
openas
opencode
OPENFILENAME
+openrdp
opensource
openxmlformats
+ollama
+onnx
OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE
ORSCANS
@@ -1420,6 +1424,8 @@ RAWPATH
rbhid
rclsid
RCZOOMIT
+remotedesktop
+rdp
RDW
READMODE
READOBJECTS
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 75b2399c8a..6744b991aa 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -69,7 +69,7 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
-
+
diff --git a/PowerToys.sln b/PowerToys.sln
index e34779c5bb..4e42cc9e30 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -834,6 +834,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageModelProvider", "sr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj", "{2B3FB837-23DE-629F-82C6-42304E7083C9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj", "{DB34808A-FF91-D06E-A426-AFB5A8BD583B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -3036,6 +3040,22 @@ Global
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64
+ {2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.Build.0 = Debug|ARM64
+ {2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.ActiveCfg = Debug|x64
+ {2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.Build.0 = Debug|x64
+ {2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.ActiveCfg = Release|ARM64
+ {2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.Build.0 = Release|ARM64
+ {2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.ActiveCfg = Release|x64
+ {2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.Build.0 = Release|x64
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.Build.0 = Debug|ARM64
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.ActiveCfg = Debug|x64
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.Build.0 = Debug|x64
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.ActiveCfg = Release|ARM64
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.Build.0 = Release|ARM64
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.ActiveCfg = Release|x64
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3367,6 +3387,8 @@ Global
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
+ {2B3FB837-23DE-629F-82C6-42304E7083C9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
+ {DB34808A-FF91-D06E-A426-AFB5A8BD583B} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs
index 996475d559..b13a72d276 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs
@@ -36,6 +36,7 @@ public partial class MainListPage : DynamicListPage,
"com.microsoft.cmdpal.builtin.websearch",
"com.microsoft.cmdpal.builtin.windowssettings",
"com.microsoft.cmdpal.builtin.datetime",
+ "com.microsoft.cmdpal.builtin.remotedesktop",
];
private readonly IServiceProvider _serviceProvider;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
index 917716be19..f91b9e304a 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs
@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.ClipboardHistory;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CmdPal.Ext.Registry;
+using Microsoft.CmdPal.Ext.RemoteDesktop;
using Microsoft.CmdPal.Ext.Shell;
using Microsoft.CmdPal.Ext.System;
using Microsoft.CmdPal.Ext.TimeDate;
@@ -151,6 +152,7 @@ public partial class App : Application
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
// Models
services.AddSingleton();
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
index bd7402e4fd..f1702c302c 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj
@@ -118,6 +118,7 @@
+
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs
new file mode 100644
index 0000000000..837246b731
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs
@@ -0,0 +1,125 @@
+// 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.Globalization;
+using System.Reflection;
+using System.Text;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
+
+[TestClass]
+public class FallbackRemoteDesktopItemTests
+{
+ private static readonly CompositeFormat OpenHostCompositeFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
+
+ [TestMethod]
+ public void UpdateQuery_WhenMatchingConnectionExists_UsesConnectionName()
+ {
+ var connectionName = "my-rdp-server";
+
+ // Arrange
+ var setup = CreateFallback(connectionName);
+ var fallback = setup.Fallback;
+
+ // Act
+ fallback.UpdateQuery("my-rdp-server");
+
+ // Assert
+ Assert.AreEqual(connectionName, fallback.Title);
+ var expectedSubtitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, connectionName);
+ Assert.AreEqual(expectedSubtitle, fallback.Subtitle);
+
+ var command = fallback.Command as OpenRemoteDesktopCommand;
+ Assert.IsNotNull(command);
+ Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name);
+ Assert.AreEqual(connectionName, GetCommandHost(command));
+ }
+
+ [TestMethod]
+ public void UpdateQuery_WhenQueryIsValidHostWithoutExistingConnection_UsesQuery()
+ {
+ // Arrange
+ var setup = CreateFallback();
+ var fallback = setup.Fallback;
+ const string hostname = "test.corp";
+
+ // Act
+ fallback.UpdateQuery(hostname);
+
+ // Assert
+ var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname);
+ Assert.AreEqual(expectedTitle, fallback.Title);
+ Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
+
+ var command = fallback.Command as OpenRemoteDesktopCommand;
+ Assert.IsNotNull(command);
+ Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name);
+ Assert.AreEqual(hostname, GetCommandHost(command));
+ }
+
+ [TestMethod]
+ public void UpdateQuery_WhenQueryIsWhitespace_ResetsCommand()
+ {
+ // Arrange
+ var setup = CreateFallback("rdp-server-two");
+ var fallback = setup.Fallback;
+
+ // Act
+ fallback.UpdateQuery(" ");
+
+ // Assert
+ Assert.AreEqual(Resources.remotedesktop_command_open, fallback.Title);
+ Assert.AreEqual(string.Empty, fallback.Subtitle);
+
+ var command = fallback.Command as OpenRemoteDesktopCommand;
+ Assert.IsNotNull(command);
+ Assert.AreEqual(Resources.remotedesktop_command_open, command.Name);
+ Assert.AreEqual(string.Empty, GetCommandHost(command));
+ }
+
+ [TestMethod]
+ public void UpdateQuery_WhenQueryIsInvalidHost_ClearsCommand()
+ {
+ // Arrange
+ var setup = CreateFallback("rdp-server-three");
+ var fallback = setup.Fallback;
+
+ // Act
+ fallback.UpdateQuery("not a valid host");
+
+ // Assert
+ Assert.AreEqual(Resources.remotedesktop_command_open, fallback.Title);
+ Assert.AreEqual(string.Empty, fallback.Subtitle);
+
+ var command = fallback.Command as OpenRemoteDesktopCommand;
+ Assert.IsNotNull(command);
+ Assert.AreEqual(Resources.remotedesktop_command_open, command.Name);
+ Assert.AreEqual(string.Empty, GetCommandHost(command));
+ }
+
+ private static string GetCommandHost(OpenRemoteDesktopCommand command)
+ {
+ var field = typeof(OpenRemoteDesktopCommand).GetField("_rdpHost", BindingFlags.NonPublic | BindingFlags.Instance);
+ if (field is null)
+ {
+ return string.Empty;
+ }
+
+ return field.GetValue(command) as string ?? string.Empty;
+ }
+
+ private static (FallbackRemoteDesktopItem Fallback, IRdpConnectionManager Manager) CreateFallback(params string[] connectionNames)
+ {
+ var settingsManager = new MockSettingsManager(connectionNames);
+ var connectionsManager = new MockRDPConnectionsManager(settingsManager);
+
+ var fallback = new FallbackRemoteDesktopItem(connectionsManager);
+
+ return (fallback, connectionsManager);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj
new file mode 100644
index 0000000000..0b998ec4ad
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+ false
+ true
+ Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests
+ $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\
+ false
+ false
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRDPConnectionsManager.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRDPConnectionsManager.cs
new file mode 100644
index 0000000000..36c3151c6f
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRDPConnectionsManager.cs
@@ -0,0 +1,23 @@
+// 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.Collections.Generic;
+using System.Linq;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
+
+internal sealed class MockRDPConnectionsManager : IRdpConnectionManager
+{
+ private readonly List _connections = new();
+
+ public IReadOnlyCollection Connections => _connections.AsReadOnly();
+
+ public MockRDPConnectionsManager(ISettingsInterface settingsManager)
+ {
+ _connections.AddRange(settingsManager.PredefinedConnections.Select(ConnectionHelpers.MapToResult));
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs
new file mode 100644
index 0000000000..1a81dcc7ee
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs
@@ -0,0 +1,23 @@
+// 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.Collections.Generic;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
+using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
+
+internal sealed class MockSettingsManager : ISettingsInterface
+{
+ private readonly List _connections;
+
+ public IReadOnlyCollection PredefinedConnections => _connections;
+
+ public ToolkitSettings Settings { get; } = new();
+
+ public MockSettingsManager(params string[] predefinedConnections)
+ {
+ _connections = new(predefinedConnections);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RDPConnectionsManagerTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RDPConnectionsManagerTests.cs
new file mode 100644
index 0000000000..dabea49a63
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RDPConnectionsManagerTests.cs
@@ -0,0 +1,52 @@
+// 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.Linq;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
+
+[TestClass]
+public class RDPConnectionsManagerTests
+{
+ [TestMethod]
+ public void Constructor_AddsOpenCommandItem()
+ {
+ // Act
+ var manager = new RDPConnectionsManager(new MockSettingsManager(["test.local"]));
+
+ // Assert
+ Assert.IsTrue(manager.Connections.Any(item => string.IsNullOrEmpty(item.ConnectionName)));
+ }
+
+ [TestMethod]
+ public void FindConnection_ReturnsExactMatch()
+ {
+ // Arrange
+ var connectionName = "rdp-test";
+ var connection = new ConnectionListItem(connectionName);
+
+ // Act
+ var result = ConnectionHelpers.FindConnection(connectionName, new[] { connection });
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.AreEqual(connectionName, result.ConnectionName);
+ }
+
+ [TestMethod]
+ public void FindConnection_ReturnsNullForWhitespaceQuery()
+ {
+ // Arrange
+ var connection = new ConnectionListItem("rdp-test");
+
+ // Act
+ var result = ConnectionHelpers.FindConnection(" ", new[] { connection });
+
+ // Assert
+ Assert.IsNull(result);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs
new file mode 100644
index 0000000000..54698997ee
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs
@@ -0,0 +1,101 @@
+// 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.Linq;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
+
+[TestClass]
+public class RemoteDesktopCommandProviderTests
+{
+ [TestMethod]
+ public void ProviderHasCorrectId()
+ {
+ // Setup
+ var provider = new RemoteDesktopCommandProvider();
+
+ // Assert
+ Assert.AreEqual("com.microsoft.cmdpal.builtin.remotedesktop", provider.Id);
+ }
+
+ [TestMethod]
+ public void ProviderHasDisplayName()
+ {
+ // Setup
+ var provider = new RemoteDesktopCommandProvider();
+
+ // Assert
+ Assert.IsNotNull(provider.DisplayName);
+ Assert.IsTrue(provider.DisplayName.Length > 0);
+ }
+
+ [TestMethod]
+ public void ProviderHasIcon()
+ {
+ // Setup
+ var provider = new RemoteDesktopCommandProvider();
+
+ // Assert
+ Assert.IsNotNull(provider.Icon);
+ }
+
+ [TestMethod]
+ public void TopLevelCommandsNotEmpty()
+ {
+ // Setup
+ var provider = new RemoteDesktopCommandProvider();
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+ Assert.IsTrue(commands.Length > 0);
+ }
+
+ [TestMethod]
+ public void FallbackCommandsNotEmpty()
+ {
+ // Setup
+ var provider = new RemoteDesktopCommandProvider();
+
+ // Act
+ var commands = provider.FallbackCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+ Assert.IsTrue(commands.Length > 0);
+ }
+
+ [TestMethod]
+ public void TopLevelCommandsContainListPageCommand()
+ {
+ // Setup
+ var provider = new RemoteDesktopCommandProvider();
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.AreEqual(1, commands.Length);
+ Assert.IsInstanceOfType(commands.Single().Command, typeof(RemoteDesktopListPage));
+ }
+
+ [TestMethod]
+ public void FallbackCommandsContainFallbackItem()
+ {
+ // Setup
+ var provider = new RemoteDesktopCommandProvider();
+
+ // Act
+ var commands = provider.FallbackCommands();
+
+ // Assert
+ Assert.AreEqual(1, commands.Length);
+ Assert.IsInstanceOfType(commands.Single(), typeof(FallbackRemoteDesktopItem));
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png
new file mode 100644
index 0000000000..52d97dbfe9
Binary files /dev/null and b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png differ
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.svg
new file mode 100644
index 0000000000..e683f4d040
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.svg
@@ -0,0 +1,21 @@
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs
new file mode 100644
index 0000000000..888a1d2f71
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs
@@ -0,0 +1,35 @@
+// 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.Globalization;
+using System.Text;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+
+internal sealed partial class ConnectionListItem : ListItem
+{
+ public ConnectionListItem(string connectionName)
+ {
+ ConnectionName = connectionName;
+
+ if (string.IsNullOrEmpty(connectionName))
+ {
+ Title = Resources.remotedesktop_open_rdp;
+ Subtitle = Resources.remotedesktop_subtitle;
+ }
+ else
+ {
+ Title = connectionName;
+ CompositeFormat remoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
+ Subtitle = string.Format(CultureInfo.CurrentCulture, remoteDesktopOpenHostFormat, connectionName);
+ }
+
+ Icon = Icons.RDPIcon;
+ Command = new OpenRemoteDesktopCommand(connectionName);
+ }
+
+ public string ConnectionName { get; }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs
new file mode 100644
index 0000000000..415746670f
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs
@@ -0,0 +1,74 @@
+// 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.Globalization;
+using System.Linq;
+using System.Text;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+
+internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem
+{
+ private const string _id = "com.microsoft.cmdpal.builtin.remotedesktop.fallback";
+
+ private static readonly UriHostNameType[] ValidUriHostNameTypes = [
+ UriHostNameType.IPv6,
+ UriHostNameType.IPv4,
+ UriHostNameType.Dns
+ ];
+
+ private static readonly CompositeFormat RemoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
+ private readonly IRdpConnectionManager _rdpConnectionsManager;
+
+ public FallbackRemoteDesktopItem(IRdpConnectionManager rdpConnectionsManager)
+ : base(new OpenRemoteDesktopCommand(string.Empty), Resources.remotedesktop_title)
+ {
+ _rdpConnectionsManager = rdpConnectionsManager;
+
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ Icon = Icons.RDPIcon;
+ }
+
+ public override void UpdateQuery(string query)
+ {
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ Command = new OpenRemoteDesktopCommand(string.Empty);
+ return;
+ }
+
+ var connections = _rdpConnectionsManager.Connections.Where(w => !string.IsNullOrWhiteSpace(w.ConnectionName));
+
+ var queryConnection = ConnectionHelpers.FindConnection(query, connections);
+
+ if (queryConnection is not null && !string.IsNullOrWhiteSpace(queryConnection.ConnectionName))
+ {
+ var connectionName = queryConnection.ConnectionName;
+
+ Command = new OpenRemoteDesktopCommand(connectionName);
+ Title = connectionName;
+ Subtitle = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
+ }
+ else if (ValidUriHostNameTypes.Contains(Uri.CheckHostName(query)))
+ {
+ var connectionName = query.Trim();
+ Command = new OpenRemoteDesktopCommand(connectionName);
+ Title = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
+ Subtitle = Resources.remotedesktop_title;
+ }
+ else
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ Command = new OpenRemoteDesktopCommand(string.Empty);
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs
new file mode 100644
index 0000000000..679b015a1e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs
@@ -0,0 +1,82 @@
+// 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.Diagnostics;
+using System.Text;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+
+internal sealed partial class OpenRemoteDesktopCommand : BaseObservable, IInvokableCommand
+{
+ private static readonly CompositeFormat ProcessErrorFormat =
+ CompositeFormat.Parse(Resources.remotedesktop_log_mstsc_error);
+
+ private static readonly CompositeFormat InvalidHostnameFormat =
+ CompositeFormat.Parse(Resources.remotedesktop_log_invalid_hostname);
+
+ public string Name { get; }
+
+ public string Id { get; } = "com.microsoft.cmdpal.builtin.remotedesktop.openrdp";
+
+ public IIconInfo Icon => Icons.RDPIcon;
+
+ private readonly string _rdpHost;
+
+ public OpenRemoteDesktopCommand(string rdpHost)
+ {
+ _rdpHost = rdpHost;
+
+ Name = string.IsNullOrWhiteSpace(_rdpHost) ?
+ Resources.remotedesktop_command_open :
+ Resources.remotedesktop_command_connect;
+ }
+
+ public ICommandResult Invoke(object sender)
+ {
+ using var process = new Process();
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.WorkingDirectory = Environment.SpecialFolder.MyDocuments.ToString();
+ process.StartInfo.FileName = "mstsc";
+
+ if (!string.IsNullOrWhiteSpace(_rdpHost))
+ {
+ // validate that _rdpHost is a proper hostname or IP address
+ if (Uri.CheckHostName(_rdpHost) == UriHostNameType.Unknown)
+ {
+ return CommandResult.ShowToast(new ToastArgs()
+ {
+ Message = string.Format(
+ System.Globalization.CultureInfo.CurrentCulture,
+ InvalidHostnameFormat,
+ _rdpHost),
+ Result = CommandResult.KeepOpen(),
+ });
+ }
+
+ process.StartInfo.Arguments = $"/v:{_rdpHost}";
+ }
+
+ try
+ {
+ process.Start();
+
+ return CommandResult.Dismiss();
+ }
+ catch (Exception ex)
+ {
+ return CommandResult.ShowToast(new ToastArgs()
+ {
+ Message = string.Format(
+ System.Globalization.CultureInfo.CurrentCulture,
+ ProcessErrorFormat,
+ ex.Message),
+ Result = CommandResult.KeepOpen(),
+ });
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs
new file mode 100644
index 0000000000..5fac986169
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs
@@ -0,0 +1,30 @@
+// 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.Collections.Generic;
+using System.Linq;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+
+internal static class ConnectionHelpers
+{
+ public static ConnectionListItem MapToResult(string item) => new(item);
+
+ public static ConnectionListItem? FindConnection(string query, IEnumerable connections)
+ {
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ return null;
+ }
+
+ var matchedConnection = ListHelpers.FilterList(
+ connections,
+ query,
+ (s, i) => ListHelpers.ScoreListItem(s, i))
+ .FirstOrDefault();
+ return matchedConnection;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRDPConnectionManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRDPConnectionManager.cs
new file mode 100644
index 0000000000..4d04126dfb
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRDPConnectionManager.cs
@@ -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.
+
+using System.Collections.Generic;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+
+internal interface IRdpConnectionManager
+{
+ IReadOnlyCollection Connections { get; }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RDPConnectionsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RDPConnectionsManager.cs
new file mode 100644
index 0000000000..b9b7d62e1a
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RDPConnectionsManager.cs
@@ -0,0 +1,89 @@
+// 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.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
+using Microsoft.Win32;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+
+internal class RDPConnectionsManager : IRdpConnectionManager
+{
+ private readonly ISettingsInterface _settingsManager;
+ private readonly ConnectionListItem _openRdpCommandListItem = new(string.Empty);
+
+ private ReadOnlyCollection _connections = new(Array.Empty());
+
+ private const int MinutesToCache = 1;
+ private DateTime? _connectionsLastLoaded;
+
+ public RDPConnectionsManager(ISettingsInterface settingsManager)
+ {
+ _settingsManager = settingsManager;
+ _settingsManager.Settings.SettingsChanged += (s, e) =>
+ {
+ _connectionsLastLoaded = null;
+ };
+ }
+
+ public IReadOnlyCollection Connections
+ {
+ get
+ {
+ if (!_connectionsLastLoaded.HasValue ||
+ (DateTime.Now - _connectionsLastLoaded.Value).TotalMinutes >= MinutesToCache)
+ {
+ var registryConnections = GetRdpConnectionsFromRegistry();
+ var predefinedConnections = GetPredefinedConnectionsFromSettings();
+ _connectionsLastLoaded = DateTime.Now;
+
+ var newConnections = new List(registryConnections.Count + predefinedConnections.Count + 1);
+ newConnections.AddRange(registryConnections);
+ newConnections.AddRange(predefinedConnections);
+ newConnections.Insert(0, _openRdpCommandListItem);
+
+ Interlocked.Exchange(ref _connections, new ReadOnlyCollection(newConnections));
+ }
+
+ return _connections;
+ }
+ }
+
+ private List GetRdpConnectionsFromRegistry()
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Terminal Server Client\Default");
+
+ var validConnections = new List();
+
+ if (key is not null)
+ {
+ validConnections = key.GetValueNames()
+ .Select(name => key.GetValue(name))
+ .OfType() // Keep only string values
+ .Select(v => v.Trim()) // Normalize
+ .Where(v => !string.IsNullOrWhiteSpace(v))
+ .Distinct() // Remove dupes if any
+ .Select(ConnectionHelpers.MapToResult)
+ .ToList();
+ }
+
+ return validConnections;
+ }
+
+ private List GetPredefinedConnectionsFromSettings()
+ {
+ var validConnections = _settingsManager.PredefinedConnections
+ .Select(s => s.Trim())
+ .Where(value => !string.IsNullOrWhiteSpace(value))
+ .Select(ConnectionHelpers.MapToResult)
+ .ToList();
+
+ return validConnections;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs
new file mode 100644
index 0000000000..eec9e48e24
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs
@@ -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.
+
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop;
+
+internal static class Icons
+{
+ internal static IconInfo RDPIcon { get; } = IconHelpers.FromRelativePath("Assets\\RemoteDesktop.svg");
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj
new file mode 100644
index 0000000000..2a561b9b9e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj
@@ -0,0 +1,44 @@
+
+
+
+
+
+ Microsoft.CmdPal.Ext.RemoteDesktop
+ $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
+ false
+ false
+
+ Microsoft.CmdPal.Ext.RemoteDesktop.pri
+ enable
+
+
+
+
+
+
+
+
+ Resources.resx
+ True
+ True
+
+
+
+
+ Resources.Designer.cs
+ PublicResXFileCodeGenerator
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs
new file mode 100644
index 0000000000..42a0165277
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs
@@ -0,0 +1,27 @@
+// 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.Linq;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
+
+internal sealed partial class RemoteDesktopListPage : ListPage
+{
+ private readonly IRdpConnectionManager _rdpConnectionManager;
+
+ public RemoteDesktopListPage(IRdpConnectionManager rdpConnectionManager)
+ {
+ Icon = Icons.RDPIcon;
+ Name = Resources.remotedesktop_title;
+ Id = "com.microsoft.cmdpal.builtin.remotedesktop";
+
+ _rdpConnectionManager = rdpConnectionManager;
+ }
+
+ public override IListItem[] GetItems() => _rdpConnectionManager.Connections.ToArray();
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..4a6c84ddea
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/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.RemoteDesktop.UnitTests")]
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..de0b924c33
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs
@@ -0,0 +1,153 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.RemoteDesktop.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Connect.
+ ///
+ public static string remotedesktop_command_connect {
+ get {
+ return ResourceManager.GetString("remotedesktop_command_connect", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Open.
+ ///
+ public static string remotedesktop_command_open {
+ get {
+ return ResourceManager.GetString("remotedesktop_command_open", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address..
+ ///
+ public static string remotedesktop_log_invalid_hostname {
+ get {
+ return ResourceManager.GetString("remotedesktop_log_invalid_hostname", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}.
+ ///
+ public static string remotedesktop_log_mstsc_error {
+ get {
+ return ResourceManager.GetString("remotedesktop_log_mstsc_error", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Connect to {0}.
+ ///
+ public static string remotedesktop_open_host {
+ get {
+ return ResourceManager.GetString("remotedesktop_open_host", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Open Remote Desktop Client.
+ ///
+ public static string remotedesktop_open_rdp {
+ get {
+ return ResourceManager.GetString("remotedesktop_open_rdp", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A list of connections to include in the query results by default.
+ ///
+ public static string remotedesktop_settings_predefined_connections_description {
+ get {
+ return ResourceManager.GetString("remotedesktop_settings_predefined_connections_description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Predefined connections.
+ ///
+ public static string remotedesktop_settings_predefined_connections_title {
+ get {
+ return ResourceManager.GetString("remotedesktop_settings_predefined_connections_title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Establish Remote Desktop connections.
+ ///
+ public static string remotedesktop_subtitle {
+ get {
+ return ResourceManager.GetString("remotedesktop_subtitle", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Remote Desktop.
+ ///
+ public static string remotedesktop_title {
+ get {
+ return ResourceManager.GetString("remotedesktop_title", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx
new file mode 100644
index 0000000000..bfbf1d3ac5
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Remote Desktop
+
+
+ Establish Remote Desktop connections
+
+
+ Open
+
+
+ Connect to {0}
+
+
+ Connect
+
+
+ Open Remote Desktop Client
+
+
+ Predefined connections
+
+
+ A list of connections to include in the query results by default
+
+
+ Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}
+
+
+ The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address.
+
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs
new file mode 100644
index 0000000000..eefd467d5b
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs
@@ -0,0 +1,45 @@
+// 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.Ext.RemoteDesktop.Commands;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop;
+
+public partial class RemoteDesktopCommandProvider : CommandProvider
+{
+ private readonly CommandItem listPageCommand;
+ private readonly FallbackRemoteDesktopItem fallback;
+
+ public RemoteDesktopCommandProvider()
+ {
+ Id = "com.microsoft.cmdpal.builtin.remotedesktop";
+ DisplayName = Resources.remotedesktop_title;
+ Icon = Icons.RDPIcon;
+
+ var settingsManager = new SettingsManager();
+ var rdpConnectionsManager = new RDPConnectionsManager(settingsManager);
+ var listPage = new RemoteDesktopListPage(rdpConnectionsManager);
+
+ fallback = new FallbackRemoteDesktopItem(rdpConnectionsManager);
+
+ listPageCommand = new CommandItem(listPage)
+ {
+ Subtitle = Resources.remotedesktop_subtitle,
+ Icon = Icons.RDPIcon,
+ MoreCommands = [
+ new CommandContextItem(settingsManager.Settings.SettingsPage),
+ ],
+ };
+ }
+
+ public override ICommandItem[] TopLevelCommands() => [listPageCommand];
+
+ public override IFallbackCommandItem[] FallbackCommands() => [fallback];
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs
new file mode 100644
index 0000000000..dbca0d3833
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs
@@ -0,0 +1,15 @@
+// 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.Collections.Generic;
+using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
+
+internal interface ISettingsInterface
+{
+ public IReadOnlyCollection PredefinedConnections { get; }
+
+ public ToolkitSettings Settings { get; }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs
new file mode 100644
index 0000000000..1469e448d7
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs
@@ -0,0 +1,53 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
+
+internal class SettingsManager : JsonSettingsManager, ISettingsInterface
+{
+ // Line break character used in WinUI3 TextBox and TextBlock.
+ private const char TEXTBOXNEWLINE = '\r';
+
+ private static readonly string _namespace = "com.microsoft.cmdpal.builtin.remotedesktop";
+
+ private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
+
+ private readonly TextSetting _predefinedConnections = new(
+ Namespaced(nameof(PredefinedConnections)),
+ Resources.remotedesktop_settings_predefined_connections_title,
+ Resources.remotedesktop_settings_predefined_connections_description,
+ string.Empty)
+ {
+ Multiline = true,
+ Placeholder = $"server1.domain.com{TEXTBOXNEWLINE}server2.domain.com{TEXTBOXNEWLINE}192.168.1.1",
+ };
+
+ public IReadOnlyCollection PredefinedConnections => _predefinedConnections.Value?.Split(TEXTBOXNEWLINE).ToList() ?? [];
+
+ internal static string SettingsJsonPath()
+ {
+ var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
+ Directory.CreateDirectory(directory);
+
+ return Path.Combine(directory, "settings.json");
+ }
+
+ public SettingsManager()
+ {
+ FilePath = SettingsJsonPath();
+
+ Settings.Add(_predefinedConnections);
+
+ // Load settings from file upon initialization
+ LoadSettings();
+
+ Settings.SettingsChanged += (s, a) => this.SaveSettings();
+ }
+}