Compare commits

...

12 Commits

Author SHA1 Message Date
Michael Jolley
3924e04578 Fleshing out TS SDK 2026-06-17 13:58:31 -05:00
Michael Jolley
34bdb12cc5 Fleshing out TS SDK 2026-06-15 22:21:17 -05:00
Michael Jolley
734a50ee97 Initial pass at JS extensions. 2026-06-14 22:43:12 -05:00
Michael Jolley
c953f84fa3 Reverting changes to ShellViewModel 2026-06-12 22:08:16 -05:00
Michael Jolley
a8d699b145 More clean-up & testing 2026-06-12 22:06:09 -05:00
Michael Jolley
d2cee3a497 Removing unneeded class 2026-06-12 20:48:41 -05:00
Michael Jolley
757f60d719 Merge branch 'main' into michaeljolley/refactor-tlcm-extension-services 2026-06-12 18:23:22 -05:00
Michael Jolley
438507cdd4 Final cleanup 2026-06-12 18:21:31 -05:00
Michael Jolley
89cf4e3115 Continuing reorg clean-up 2026-06-12 17:51:13 -05:00
Michael Jolley
59bc58afe2 Address PR feedback: clean up types, graceful cancellation, extract class
- Use IExtensionWrapper directly (with existing using) in
  ExtensionGalleryViewModel instead of fully-qualified name
- Replace ct.ThrowIfCancellationRequested() with graceful early return
  in BuiltInExtensionService.LoadProvidersAsync
- Extract ExtensionStartResult into its own file
- Restore 'Go back to the main page, but keep it open' comment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-09 22:47:05 -05:00
Michael Jolley
e22bbd05a5 Fix check-spelling: replace TLCM and startable in comments
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-09 21:45:50 -05:00
Michael Jolley
6b0779a7c5 Refactor TopLevelCommandManager to use IExtensionService implementations
Extract extension loading from TopLevelCommandManager into separate
IExtensionService implementations:

- BuiltInExtensionService: wraps in-proc ICommandProvider instances from DI
- WinRTExtensionService: manages out-of-process WinRT AppExtension providers

TLCM now receives IEnumerable<IExtensionService> and orchestrates them
uniformly. This reduces TLCM from ~900 to ~700 lines and separates
provider-specific concerns (timeout, retry, catalog events) from
command orchestration.

Additional fixes:
- Break circular DI dependency: BuiltInsCommandProvider no longer needs
  IRootPageService. Uses GoHomeDockCommand (returns CommandResult.GoHome())
  with onBeforeShowConfirmation callback to show palette at dock position.
- Restore two-phase loading: PreLoadAsync loads only built-ins (instant),
  PostLoadRootPageAsync loads WinRT extensions on background thread.
- Add try/finally around IsLoading to prevent stuck loading indicator.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-09 21:29:33 -05:00
134 changed files with 13882 additions and 558 deletions

View File

@@ -1,75 +0,0 @@
// 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 Windows.Foundation;
namespace Microsoft.CmdPal.Common.Services;
public interface IExtensionService
{
/// <summary>
/// Gets the currently cached installed Command Palette extensions.
/// </summary>
/// <param name="includeDisabledExtensions">True to include disabled extensions in the result.</param>
/// <returns>A sequence of installed Command Palette extensions from the current in-memory cache.</returns>
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false);
/// <summary>
/// Forces a fresh scan of installed Command Palette extensions and updates the in-memory cache.
/// </summary>
/// <param name="includeDisabledExtensions">True to include disabled extensions in the result.</param>
/// <returns>A sequence of installed Command Palette extensions after the cache has been rebuilt.</returns>
Task<IEnumerable<IExtensionWrapper>> RefreshInstalledExtensionsAsync(bool includeDisabledExtensions = false);
// Task<IEnumerable<string>> GetInstalledHomeWidgetPackageFamilyNamesAsync(bool includeDisabledExtensions = false);
/// <summary>
/// Gets the installed Command Palette extensions for a specific provider type.
/// </summary>
/// <param name="providerType">The provider type to match.</param>
/// <param name="includeDisabledExtensions">True to include disabled extensions in the result.</param>
/// <returns>A sequence of installed Command Palette extensions for the requested provider type.</returns>
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(Microsoft.CommandPalette.Extensions.ProviderType providerType, bool includeDisabledExtensions = false);
/// <summary>
/// Gets a cached installed extension by its unique id.
/// </summary>
/// <param name="extensionUniqueId">The unique id of the extension to look up.</param>
/// <returns>The cached extension if found; otherwise, null.</returns>
IExtensionWrapper? GetInstalledExtension(string extensionUniqueId);
/// <summary>
/// Signals running extensions to stop.
/// </summary>
Task SignalStopExtensionsAsync();
/// <summary>
/// Raised when one or more extensions are added to the installed set.
/// </summary>
event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;
/// <summary>
/// Raised when one or more extensions are removed from the installed set.
/// </summary>
event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionRemoved;
/// <summary>
/// Enables an installed extension by unique id.
/// </summary>
/// <param name="extensionUniqueId">The unique id of the extension to enable.</param>
void EnableExtension(string extensionUniqueId);
/// <summary>
/// Disables an installed extension by unique id.
/// </summary>
/// <param name="extensionUniqueId">The unique id of the extension to disable.</param>
void DisableExtension(string extensionUniqueId);
///// <summary>
///// Gets a boolean indicating whether the extension was disabled due to the corresponding Windows optional feature
///// being absent from the machine or in an unknown state.
///// </summary>
///// <param name="extension">The out of proc extension object</param>
///// <returns>True only if the extension was disabled. False otherwise.</returns>
// public Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension);
}

View File

@@ -125,6 +125,49 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
isValid = true;
}
/// <summary>
/// Creates a wrapper for a JavaScript extension where the <see cref="ICommandProvider"/>
/// is obtained directly (not through <see cref="IExtensionWrapper.GetExtensionObject"/>).
/// </summary>
/// <param name="extension">The JS extension wrapper managing the node process.</param>
/// <param name="provider">The command provider proxy obtained via JSON-RPC.</param>
/// <param name="mainThread">The UI thread scheduler.</param>
public CommandProviderWrapper(IExtensionWrapper extension, ICommandProvider provider, TaskScheduler mainThread)
{
_taskScheduler = mainThread;
TopLevelPageContext = new(this, _taskScheduler);
Extension = extension;
ExtensionHost = new CommandPaletteHost(extension);
_commandProvider = new(provider);
try
{
var model = _commandProvider.Unsafe!;
model.InitializeWithHost(ExtensionHost);
model.ItemsChanged += CommandProvider_ItemsChanged;
isValid = true;
Id = provider.Id;
DisplayName = provider.DisplayName;
Icon = new(provider.Icon);
Icon.InitializeProperties();
Settings = new(provider.Settings, this, _taskScheduler);
Logger.LogDebug($"Initialized JS extension command provider {Extension.PackageFamilyName}:{Extension.ExtensionUniqueId}");
}
catch (Exception e)
{
Logger.LogError("Failed to initialize CommandProvider for JS extension.");
Logger.LogError($"Extension was {Extension!.PackageFamilyName}");
Logger.LogError(e.ToString());
}
isValid = true;
}
private ProviderSettings GetProviderSettings(SettingsModel settings)
{
if (!settings.ProviderSettings.TryGetValue(ProviderId, out var ps))

View File

@@ -2,6 +2,7 @@
// 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.UI.ViewModels.Commands;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -18,8 +19,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
private readonly FallbackReloadItem _fallbackReloadItem = new();
private readonly FallbackLogItem _fallbackLogItem = new();
private readonly NewExtensionPage _newExtension = new();
private readonly IRootPageService _rootPageService;
private readonly GoHomeDockCommand _goHomeDockCommand = new();
public override ICommandItem[] TopLevelCommands() =>
[
@@ -41,22 +41,16 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
_fallbackLogItem,
];
public BuiltInsCommandProvider(IRootPageService rootPageService)
public BuiltInsCommandProvider()
{
Id = "com.microsoft.cmdpal.builtin.core";
DisplayName = Properties.Resources.builtin_display_name;
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
_rootPageService = rootPageService;
}
public override ICommandItem[]? GetDockBands()
{
var rootPage = _rootPageService.GetRootPage();
List<ICommandItem> bandItems = new();
bandItems.Add(new WrappedDockItem(rootPage, Properties.Resources.builtin_command_palette_title));
return bandItems.ToArray();
return [new WrappedDockItem(_goHomeDockCommand, Properties.Resources.builtin_command_palette_title)];
}
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);

View File

@@ -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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels.Commands;
/// <summary>
/// A lightweight command used as a dock band item that navigates the user
/// back to the Command Palette home page when invoked.
/// </summary>
internal sealed partial class GoHomeDockCommand : InvokableCommand
{
public GoHomeDockCommand()
{
Name = Properties.Resources.builtin_command_palette_title;
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
}
public override ICommandResult Invoke() => CommandResult.GoHome();
}

View File

@@ -36,6 +36,7 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
private const string SourceTypeWinGet = "winget";
private const string SourceTypeStore = "msstore";
private const string SourceTypeNpm = "npm";
private const string SourceTypeUrl = "url";
private const string SourceTypeGitHub = "github";
private const string SourceTypeWebsite = "website";
@@ -129,6 +130,10 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
public bool HasStoreSource => HasSource(SourceTypeStore);
public bool HasNpmSource => HasSource(SourceTypeNpm);
public string? NpmPackageId => GetSource(SourceTypeNpm)?.Id;
public bool HasUrlSource => _installSourcesByType.ContainsKey(SourceTypeUrl) && InstallUrl is not null;
public bool HasHomepage => _homepageHttpUri is not null;
@@ -166,7 +171,7 @@ public sealed partial class ExtensionGalleryItemViewModel : ObservableObject
public bool ShowUnknownSourceIndicator => HasUnknownSource || !HasKnownSourceIndicator;
public bool HasActionableSourceDetails => HasStoreSource || HasWinGetSource || HasHomepage || HasUrlSource;
public bool HasActionableSourceDetails => HasStoreSource || HasWinGetSource || HasNpmSource || HasHomepage || HasUrlSource;
public bool ShowNoSourceDetails => !HasActionableSourceDetails;

View File

@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Common.WinGet.Models;
using Microsoft.CmdPal.Common.WinGet.Services;
using Microsoft.CmdPal.UI.ViewModels.Properties;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.Logging;
@@ -50,7 +51,7 @@ public sealed partial class ExtensionGalleryViewModel : ObservableObject, IDispo
"Failed to check WinGet package status");
private readonly IExtensionGalleryService _galleryService;
private readonly IExtensionService _extensionService;
private readonly IEnumerable<IExtensionService> _extensionServices;
private readonly ILogger<ExtensionGalleryViewModel> _logger;
private readonly ExtensionGalleryItemViewModelFactory _galleryExtensionViewModelFactory;
private readonly IWinGetPackageManagerService? _winGetPackageManagerService;
@@ -162,7 +163,7 @@ public sealed partial class ExtensionGalleryViewModel : ObservableObject, IDispo
public ExtensionGalleryViewModel(
IExtensionGalleryService galleryService,
IExtensionService extensionService,
IEnumerable<IExtensionService> extensionServices,
ILogger<ExtensionGalleryViewModel> logger,
ExtensionGalleryItemViewModelFactory galleryExtensionViewModelFactory,
IWinGetPackageManagerService? winGetPackageManagerService = null,
@@ -171,7 +172,7 @@ public sealed partial class ExtensionGalleryViewModel : ObservableObject, IDispo
TaskScheduler? uiScheduler = null)
{
_galleryService = galleryService;
_extensionService = extensionService;
_extensionServices = extensionServices;
_logger = logger;
_galleryExtensionViewModelFactory = galleryExtensionViewModelFactory;
_winGetPackageManagerService = winGetPackageManagerService;
@@ -347,17 +348,23 @@ public sealed partial class ExtensionGalleryViewModel : ObservableObject, IDispo
List<ExtensionGalleryItemViewModel> snapshot;
try
{
var installedExtensions = refreshInstalledExtensions
? await RunInBackgroundAsync(
() => _extensionService.RefreshInstalledExtensionsAsync(includeDisabledExtensions: true),
cancellationToken)
: await RunInBackgroundAsync(
() => _extensionService.GetInstalledExtensionsAsync(includeDisabledExtensions: true),
cancellationToken);
var allInstalledExtensions = new List<IExtensionWrapper>();
foreach (var service in _extensionServices)
{
var extensions = refreshInstalledExtensions
? await RunInBackgroundAsync(
() => service.RefreshInstalledExtensionsAsync(includeDisabledExtensions: true),
cancellationToken)
: await RunInBackgroundAsync(
() => service.GetInstalledExtensionsAsync(includeDisabledExtensions: true),
cancellationToken);
allInstalledExtensions.AddRange(extensions);
}
cancellationToken.ThrowIfCancellationRequested();
var installedPfns = new HashSet<string>(
installedExtensions
allInstalledExtensions
.Select(e => e.PackageFamilyName)
.Where(pfn => !string.IsNullOrEmpty(pfn)),
StringComparer.OrdinalIgnoreCase);

View File

@@ -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.
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record ProviderEnabledStateChangedMessage(string ProviderId, bool IsEnabled);

View File

@@ -0,0 +1,558 @@
// 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.
#pragma warning disable SA1402 // File may only contain a single type - closely related adapters grouped together
#pragma warning disable SA1649 // File name should match first type name
#pragma warning disable SA1300 // Element should begin with upper-case letter (private event backing fields)
#pragma warning disable SA1516 // Elements should be separated by blank line
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using System.Text.Json.Nodes;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Services.JsonRpc;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
/// <summary>
/// Adapts JSON command data to ICommand and IInvokableCommand interfaces.
/// </summary>
internal sealed class JSCommandAdapter : ICommand, IInvokableCommand
{
private readonly JsonElement _data;
private readonly JsonRpcConnection _connection;
private readonly Lock _eventLock = new();
private event TypedEventHandler<object, IPropChangedEventArgs>? _propChanged;
public JSCommandAdapter(JsonElement data, JsonRpcConnection connection)
{
_data = data;
_connection = connection;
}
public string Name => GetStringProperty("displayName") ?? GetStringProperty("name") ?? string.Empty;
public string Id => GetStringProperty("id") ?? string.Empty;
public IIconInfo Icon => GetIconInfo();
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add
{
lock (_eventLock)
{
_propChanged += value;
}
}
remove
{
lock (_eventLock)
{
_propChanged -= value;
}
}
}
public void RaisePropChanged(string propertyName)
{
lock (_eventLock)
{
_propChanged?.Invoke(this, new PropChangedEventArgs(propertyName));
}
}
public ICommandResult Invoke(object sender)
{
try
{
var response = _connection.SendRequestAsync(
"command/invoke",
new JsonObject { ["commandId"] = Id },
CancellationToken.None).GetAwaiter().GetResult();
if (response.Error != null)
{
Logger.LogError($"Command invoke error: {response.Error.Message}");
return CommandResult.KeepOpen();
}
return ParseCommandResult(response.Result);
}
catch (Exception ex)
{
Logger.LogError($"Failed to invoke command {Id}: {ex.Message}");
return CommandResult.KeepOpen();
}
}
private string? GetStringProperty(string name)
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString();
}
return null;
}
private IIconInfo GetIconInfo()
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("icon", out var iconProp))
{
return JSIconInfoAdapter.FromJson(iconProp);
}
return new IconInfo(string.Empty);
}
private ICommandResult ParseCommandResult(JsonElement? result)
{
return JSCommandResultAdapter.ParseCommandResult(result, _connection);
}
}
/// <summary>
/// Adapts JSON command item data to ICommandItem interface.
/// </summary>
internal sealed class JSCommandItemAdapter : ICommandItem
{
private readonly JsonElement _data;
private readonly JsonRpcConnection _connection;
private readonly Lock _eventLock = new();
private ICommand? _command;
private event TypedEventHandler<object, IPropChangedEventArgs>? _propChanged;
public JSCommandItemAdapter(JsonElement data, JsonRpcConnection connection)
{
_data = data;
_connection = connection;
}
public ICommand Command
{
get
{
if (_command == null)
{
var commandData = _data;
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("command", out var commandElement) &&
commandElement.ValueKind == JsonValueKind.Object)
{
commandData = commandElement;
}
_command = JSCommandFactory.CreateCommandFromJson(commandData, _connection);
}
return _command;
}
}
public IContextItem[] MoreCommands => [];
public IIconInfo Icon => GetIconInfo();
public string Title => GetStringProperty("displayName") ?? GetStringProperty("title") ?? string.Empty;
public string Subtitle => GetStringProperty("description") ?? GetStringProperty("subtitle") ?? string.Empty;
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add
{
lock (_eventLock)
{
_propChanged += value;
}
}
remove
{
lock (_eventLock)
{
_propChanged -= value;
}
}
}
public void RaisePropChanged(string propertyName)
{
lock (_eventLock)
{
_propChanged?.Invoke(this, new PropChangedEventArgs(propertyName));
}
}
private string? GetStringProperty(string name)
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString();
}
return null;
}
private IIconInfo GetIconInfo()
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("icon", out var iconProp))
{
return JSIconInfoAdapter.FromJson(iconProp);
}
return new IconInfo(string.Empty);
}
}
/// <summary>
/// Utility for creating the appropriate ICommand implementation from JSON data,
/// inspecting the _type discriminator to return page proxies for pages.
/// </summary>
internal static class JSCommandFactory
{
internal static ICommand CreateCommandFromJson(JsonElement data, JsonRpcConnection connection)
{
if (data.ValueKind == JsonValueKind.Object &&
data.TryGetProperty("_type", out var typeProp) && typeProp.ValueKind == JsonValueKind.String)
{
var commandId = string.Empty;
if (data.TryGetProperty("id", out var idProp) && idProp.ValueKind == JsonValueKind.String)
{
commandId = idProp.GetString() ?? string.Empty;
}
var pageType = typeProp.GetString();
if (pageType == "dynamicListPage")
{
return new JSDynamicListPageProxy(commandId, connection, data);
}
if (pageType == "listPage")
{
return new JSListPageProxy(commandId, connection, data);
}
if (pageType == "contentPage")
{
return new JSContentPageProxy(commandId, data, connection);
}
}
return new JSCommandAdapter(data, connection);
}
}
/// <summary>
/// Adapts JSON fallback command item data to IFallbackCommandItem interface.
/// </summary>
internal sealed class JSFallbackCommandItemAdapter : IFallbackCommandItem, IFallbackCommandItem2
{
private readonly JsonElement _data;
private readonly JsonRpcConnection _connection;
private readonly Lock _eventLock = new();
private ICommand? _command;
private JSFallbackHandler? _fallbackHandler;
private string? _displayTitleOverride;
private event TypedEventHandler<object, IPropChangedEventArgs>? _propChanged;
public JSFallbackCommandItemAdapter(JsonElement data, JsonRpcConnection connection)
{
_data = data;
_connection = connection;
}
public ICommand Command
{
get
{
if (_command == null)
{
var commandData = _data;
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("command", out var commandElement) &&
commandElement.ValueKind == JsonValueKind.Object)
{
commandData = commandElement;
}
_command = JSCommandFactory.CreateCommandFromJson(commandData, _connection);
}
return _command;
}
}
public IContextItem[] MoreCommands => [];
public IIconInfo Icon => GetIconInfo();
public string Title => GetStringProperty("displayName") ?? GetStringProperty("title") ?? string.Empty;
public string Subtitle => GetStringProperty("description") ?? GetStringProperty("subtitle") ?? string.Empty;
public string DisplayTitle => _displayTitleOverride ?? GetStringProperty("displayTitle") ?? Title;
public string Id => GetStringProperty("id") ?? string.Empty;
public void UpdateDisplayTitle(string newTitle)
{
_displayTitleOverride = newTitle;
}
public IFallbackHandler FallbackHandler
{
get
{
_fallbackHandler ??= new JSFallbackHandler(_connection, Id);
return _fallbackHandler;
}
}
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add
{
lock (_eventLock)
{
_propChanged += value;
}
}
remove
{
lock (_eventLock)
{
_propChanged -= value;
}
}
}
public void RaisePropChanged(string propertyName)
{
lock (_eventLock)
{
_propChanged?.Invoke(this, new PropChangedEventArgs(propertyName));
}
}
private string? GetStringProperty(string name)
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString();
}
return null;
}
private IIconInfo GetIconInfo()
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("icon", out var iconProp))
{
return JSIconInfoAdapter.FromJson(iconProp);
}
return new IconInfo(string.Empty);
}
private sealed class JSFallbackHandler : IFallbackHandler
{
private readonly JsonRpcConnection _connection;
private readonly string _commandId;
public JSFallbackHandler(JsonRpcConnection connection, string commandId)
{
_connection = connection;
_commandId = commandId;
}
public void UpdateQuery(string query)
{
try
{
_connection.SendNotificationAsync(
"fallback/updateQuery",
new JsonObject { ["commandId"] = _commandId, ["query"] = query },
CancellationToken.None).Wait(TimeSpan.FromSeconds(2));
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to send fallback query update: {ex.Message}");
}
}
}
}
/// <summary>
/// Adapts JSON icon data to IIconInfo and IIconData interfaces.
/// </summary>
internal sealed class JSIconInfoAdapter : IIconInfo
{
private readonly JSIconDataAdapter? _light;
private readonly JSIconDataAdapter? _dark;
private JSIconInfoAdapter(JSIconDataAdapter? light, JSIconDataAdapter? dark)
{
_light = light;
_dark = dark;
}
public IIconData Light => _light ?? new JSIconDataAdapter(string.Empty, null);
public IIconData Dark => _dark ?? new JSIconDataAdapter(string.Empty, null);
public static JSIconInfoAdapter FromJson(JsonElement iconJson)
{
if (iconJson.ValueKind != JsonValueKind.Object)
{
return new JSIconInfoAdapter(null, null);
}
#pragma warning disable CA1507 // JSON property names, not C# members
JSIconDataAdapter? light = null;
JSIconDataAdapter? dark = null;
if (iconJson.TryGetProperty("Light", out var lightProp) || iconJson.TryGetProperty("light", out lightProp))
{
light = ParseIconData(lightProp);
}
if (iconJson.TryGetProperty("Dark", out var darkProp) || iconJson.TryGetProperty("dark", out darkProp))
{
dark = ParseIconData(darkProp);
}
// If only "icon"/"Icon" property exists, use it for both light and dark
if (light == null && dark == null)
{
if (iconJson.TryGetProperty("Icon", out var iconProp) || iconJson.TryGetProperty("icon", out iconProp))
{
var iconData = ParseIconData(iconProp);
light = iconData;
dark = iconData;
}
}
#pragma warning restore CA1507
return new JSIconInfoAdapter(light, dark);
}
private static JSIconDataAdapter? ParseIconData(JsonElement iconDataJson)
{
#pragma warning disable CA1507 // JSON property names, not C# members
string? iconPath = null;
string? base64Data = null;
if (iconDataJson.ValueKind == JsonValueKind.String)
{
iconPath = iconDataJson.GetString();
}
else if (iconDataJson.ValueKind == JsonValueKind.Object)
{
if ((iconDataJson.TryGetProperty("Icon", out var iconProp) || iconDataJson.TryGetProperty("icon", out iconProp)) &&
iconProp.ValueKind == JsonValueKind.String)
{
iconPath = iconProp.GetString();
}
if ((iconDataJson.TryGetProperty("Data", out var dataProp) || iconDataJson.TryGetProperty("data", out dataProp)) &&
dataProp.ValueKind == JsonValueKind.String)
{
base64Data = dataProp.GetString();
}
}
#pragma warning restore CA1507
if (!string.IsNullOrEmpty(iconPath) || !string.IsNullOrEmpty(base64Data))
{
return new JSIconDataAdapter(iconPath ?? string.Empty, base64Data);
}
return null;
}
internal sealed class JSIconDataAdapter : IIconData
{
private readonly string _icon;
private readonly string? _base64Data;
public JSIconDataAdapter(string icon, string? base64Data)
{
_icon = icon;
_base64Data = base64Data;
}
public string Icon => _icon;
public IRandomAccessStreamReference? Data
{
get
{
if (string.IsNullOrEmpty(_base64Data))
{
return null;
}
try
{
byte[] bytes;
if (_base64Data.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
bytes = DecodeDataUri(_base64Data);
}
else
{
bytes = Convert.FromBase64String(_base64Data);
}
var stream = new InMemoryRandomAccessStream();
stream.WriteAsync(bytes.AsBuffer()).GetResults();
stream.Seek(0);
return RandomAccessStreamReference.CreateFromStream(stream);
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to decode icon data: {ex.Message}");
return null;
}
}
}
private static byte[] DecodeDataUri(string dataUri)
{
// data:[<mediatype>][;base64],<data>
var commaIndex = dataUri.IndexOf(',', StringComparison.Ordinal);
if (commaIndex < 0)
{
throw new FormatException("Invalid data URI: no comma separator");
}
var header = dataUri[..commaIndex];
var data = dataUri[(commaIndex + 1)..];
if (header.Contains(";base64", StringComparison.OrdinalIgnoreCase))
{
return Convert.FromBase64String(data);
}
// URL-encoded data (e.g., SVG)
var decoded = Uri.UnescapeDataString(data);
return System.Text.Encoding.UTF8.GetBytes(decoded);
}
}
}

View File

@@ -0,0 +1,588 @@
// 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.
#pragma warning disable SA1300 // Element should begin with upper-case letter (private event backing fields)
#pragma warning disable CS4014 // Because this call is not awaited
using System.Text.Json;
using System.Text.Json.Nodes;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Services.JsonRpc;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
/// <summary>
/// Proxy that implements ICommandProvider by forwarding calls to a Node.js extension via JSON-RPC.
/// </summary>
public sealed class JSCommandProviderProxy : ICommandProvider, IDisposable
{
private readonly JsonRpcConnection _rpcConnection;
private readonly JSExtensionManifest _manifest;
private readonly IconInfo _defaultIcon;
private readonly Lock _eventLock = new();
private readonly Dictionary<(string Message, int State), StatusMessage> _shownStatusMessages = new();
private readonly Dictionary<string, JSFallbackCommandItemAdapter> _fallbackAdapters = new();
private IExtensionHost? _host;
private bool _isDisposed;
private ICommandSettings? _settingsCache;
private bool _settingsQueried;
private event TypedEventHandler<object, IItemsChangedEventArgs>? _itemsChanged;
public JSCommandProviderProxy(JsonRpcConnection rpcConnection, JSExtensionManifest manifest)
{
_rpcConnection = rpcConnection ?? throw new ArgumentNullException(nameof(rpcConnection));
_manifest = manifest ?? throw new ArgumentNullException(nameof(manifest));
_defaultIcon = new IconInfo(string.Empty);
RegisterNotificationHandlers();
}
public string Id => _manifest.Name ?? "unknown";
public string DisplayName => _manifest.DisplayName ?? _manifest.Name ?? "Unknown";
public IIconInfo Icon => _defaultIcon;
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged
{
add
{
lock (_eventLock)
{
_itemsChanged += value;
}
}
remove
{
lock (_eventLock)
{
_itemsChanged -= value;
}
}
}
public ICommandItem[] TopLevelCommands()
{
try
{
var response = _rpcConnection.SendRequestAsync(
"provider/getTopLevelCommands",
null,
CancellationToken.None).GetAwaiter().GetResult();
if (response.Error != null)
{
Logger.LogError($"TopLevelCommands error: {response.Error.Message}");
return [];
}
return ParseCommandItems(response.Result);
}
catch (Exception ex)
{
Logger.LogError($"Failed to get top-level commands: {ex.Message}");
return [];
}
}
public IFallbackCommandItem[]? FallbackCommands()
{
try
{
var response = _rpcConnection.SendRequestAsync(
"provider/getFallbackCommands",
null,
CancellationToken.None).GetAwaiter().GetResult();
if (response.Error != null)
{
Logger.LogWarning($"FallbackCommands error: {response.Error.Message}");
return null;
}
return ParseFallbackCommandItems(response.Result);
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to get fallback commands: {ex.Message}");
return null;
}
}
public ICommand? GetCommand(string id)
{
try
{
var response = _rpcConnection.SendRequestAsync(
"provider/getCommand",
new JsonObject { ["commandId"] = id },
CancellationToken.None).GetAwaiter().GetResult();
if (response.Error != null)
{
Logger.LogWarning($"GetCommand error for {id}: {response.Error.Message}");
return null;
}
if (!response.Result.HasValue || response.Result.Value.ValueKind == JsonValueKind.Null)
{
return null;
}
var data = response.Result.Value;
// Check for page type discriminator to return the appropriate proxy
if (data.TryGetProperty("_type", out var typeProp) && typeProp.ValueKind == JsonValueKind.String)
{
var pageType = typeProp.GetString();
if (pageType == "dynamicListPage")
{
return new JSDynamicListPageProxy(id, _rpcConnection);
}
if (pageType == "listPage")
{
return new JSListPageProxy(id, _rpcConnection);
}
if (pageType == "contentPage")
{
return new JSContentPageProxy(id, _rpcConnection);
}
}
return new JSCommandAdapter(data, _rpcConnection);
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to get command {id}: {ex.Message}");
return null;
}
}
public ICommandSettings? Settings
{
get
{
if (_settingsQueried)
{
return _settingsCache;
}
_settingsQueried = true;
try
{
var response = _rpcConnection.SendRequestAsync(
"provider/getSettings",
null,
CancellationToken.None).GetAwaiter().GetResult();
if (response.Error != null)
{
Logger.LogDebug($"Settings not available for {DisplayName}: {response.Error.Message}");
return null;
}
if (!response.Result.HasValue || response.Result.Value.ValueKind == JsonValueKind.Null)
{
return null;
}
var data = response.Result.Value;
var pageId = string.Empty;
if (data.TryGetProperty("id", out var idProp) && idProp.ValueKind == JsonValueKind.String)
{
pageId = idProp.GetString() ?? string.Empty;
}
if (!string.IsNullOrEmpty(pageId))
{
_settingsCache = new JSCommandSettingsProxy(pageId, _rpcConnection);
}
}
catch (Exception ex)
{
Logger.LogDebug($"Failed to get settings for {DisplayName}: {ex.Message}");
}
return _settingsCache;
}
}
public bool Frozen => true;
public void InitializeWithHost(IExtensionHost host)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
Logger.LogDebug($"JSCommandProviderProxy initialized with host for {DisplayName}");
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
lock (_eventLock)
{
_itemsChanged = null;
}
_host = null;
}
private void RegisterNotificationHandlers()
{
_rpcConnection.RegisterNotificationHandler("provider/itemsChanged", HandleItemsChangedNotification);
_rpcConnection.RegisterNotificationHandler("command/propChanged", HandleCommandPropChangedNotification);
_rpcConnection.RegisterNotificationHandler("page/propChanged", HandlePagePropChangedNotification);
_rpcConnection.RegisterNotificationHandler("content/propChanged", HandleContentPropChangedNotification);
_rpcConnection.RegisterNotificationHandler("provider/propChanged", HandleProviderPropChangedNotification);
_rpcConnection.RegisterNotificationHandler("host/logMessage", HandleLogMessageNotification);
_rpcConnection.RegisterNotificationHandler("host/showStatus", HandleShowStatusNotification);
_rpcConnection.RegisterNotificationHandler("host/hideStatus", HandleHideStatusNotification);
_rpcConnection.RegisterNotificationHandler("host/copyText", HandleCopyTextNotification);
}
private void HandleItemsChangedNotification(JsonElement paramsElement)
{
try
{
var totalItems = -1;
if (paramsElement.TryGetProperty("totalItems", out var totalItemsProp))
{
totalItems = totalItemsProp.GetInt32();
}
lock (_eventLock)
{
_itemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
}
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling itemsChanged notification: {ex.Message}");
}
}
private void HandleCommandPropChangedNotification(JsonElement paramsElement)
{
try
{
var commandId = string.Empty;
if (paramsElement.TryGetProperty("commandId", out var commandIdProp))
{
commandId = commandIdProp.GetString() ?? string.Empty;
}
// Update fallback adapter properties if applicable
if (!string.IsNullOrEmpty(commandId) && _fallbackAdapters.TryGetValue(commandId, out var fallbackAdapter))
{
if (paramsElement.TryGetProperty("properties", out var propsProp) && propsProp.ValueKind == JsonValueKind.Object)
{
if (propsProp.TryGetProperty("displayTitle", out var displayTitleProp) && displayTitleProp.ValueKind == JsonValueKind.String)
{
fallbackAdapter.UpdateDisplayTitle(displayTitleProp.GetString() ?? string.Empty);
}
}
fallbackAdapter.RaisePropChanged("DisplayTitle");
}
Logger.LogDebug($"[{DisplayName}] command/propChanged: commandId={commandId}");
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling command/propChanged notification: {ex.Message}");
}
}
private void HandlePagePropChangedNotification(JsonElement paramsElement)
{
try
{
var pageId = string.Empty;
if (paramsElement.TryGetProperty("pageId", out var pageIdProp))
{
pageId = pageIdProp.GetString() ?? string.Empty;
}
var propertyName = string.Empty;
if (paramsElement.TryGetProperty("propertyName", out var propertyNameProp))
{
propertyName = propertyNameProp.GetString() ?? string.Empty;
}
Logger.LogDebug($"[{DisplayName}] page/propChanged: pageId={pageId}, property={propertyName}");
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling page/propChanged notification: {ex.Message}");
}
}
private void HandleContentPropChangedNotification(JsonElement paramsElement)
{
try
{
var contentId = string.Empty;
if (paramsElement.TryGetProperty("contentId", out var contentIdProp))
{
contentId = contentIdProp.GetString() ?? string.Empty;
}
var propertyName = string.Empty;
if (paramsElement.TryGetProperty("propertyName", out var propertyNameProp))
{
propertyName = propertyNameProp.GetString() ?? string.Empty;
}
Logger.LogDebug($"[{DisplayName}] content/propChanged: contentId={contentId}, property={propertyName}");
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling content/propChanged notification: {ex.Message}");
}
}
private void HandleProviderPropChangedNotification(JsonElement paramsElement)
{
try
{
var propertyName = string.Empty;
if (paramsElement.TryGetProperty("propertyName", out var propertyNameProp))
{
propertyName = propertyNameProp.GetString() ?? string.Empty;
}
Logger.LogDebug($"[{DisplayName}] provider/propChanged: property={propertyName}");
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling provider/propChanged notification: {ex.Message}");
}
}
private void HandleLogMessageNotification(JsonElement paramsElement)
{
try
{
if (!paramsElement.TryGetProperty("message", out var messageProp))
{
return;
}
var message = messageProp.GetString() ?? string.Empty;
var state = 2; // Default to Info
if (paramsElement.TryGetProperty("state", out var stateProp))
{
state = stateProp.GetInt32();
}
// Map MessageState enum: 0=Trace, 1=Debug, 2=Info, 3=Warning, 4=Error
switch (state)
{
case 0:
case 1:
Logger.LogDebug($"[{DisplayName}] {message}");
break;
case 2:
Logger.LogInfo($"[{DisplayName}] {message}");
break;
case 3:
Logger.LogWarning($"[{DisplayName}] {message}");
break;
case 4:
Logger.LogError($"[{DisplayName}] {message}");
break;
default:
Logger.LogInfo($"[{DisplayName}] {message}");
break;
}
// Forward to host if available
if (_host != null)
{
var logMessage = new LogMessage { Message = message, State = (MessageState)state };
_host.LogMessage(logMessage);
}
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling logMessage notification: {ex.Message}");
}
}
private void HandleShowStatusNotification(JsonElement paramsElement)
{
try
{
if (_host == null)
{
return;
}
if (!paramsElement.TryGetProperty("message", out var messageProp))
{
return;
}
var message = string.Empty;
var state = 2; // Default to Info
if (messageProp.ValueKind == JsonValueKind.Object)
{
if (messageProp.TryGetProperty("Message", out var msgText))
{
message = msgText.GetString() ?? string.Empty;
}
if (messageProp.TryGetProperty("State", out var msgState))
{
state = msgState.GetInt32();
}
}
else if (messageProp.ValueKind == JsonValueKind.String)
{
message = messageProp.GetString() ?? string.Empty;
}
var statusMessage = new StatusMessage
{
Message = message,
State = (MessageState)state,
};
_shownStatusMessages[(message, state)] = statusMessage;
var context = StatusContext.Extension;
if (paramsElement.TryGetProperty("context", out var contextProp))
{
if (contextProp.ValueKind == JsonValueKind.Number)
{
context = (StatusContext)contextProp.GetInt32();
}
else if (contextProp.ValueKind == JsonValueKind.String)
{
var contextStr = contextProp.GetString();
if (contextStr == "page")
{
context = StatusContext.Page;
}
}
}
_host.ShowStatus(statusMessage, context);
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling showStatus notification: {ex.Message}");
}
}
private void HandleHideStatusNotification(JsonElement paramsElement)
{
try
{
if (_host == null)
{
return;
}
var message = string.Empty;
var state = 2; // Default to Info
if (paramsElement.TryGetProperty("message", out var messageProp) &&
messageProp.ValueKind == JsonValueKind.Object)
{
if (messageProp.TryGetProperty("Message", out var msgText))
{
message = msgText.GetString() ?? string.Empty;
}
if (messageProp.TryGetProperty("State", out var msgState))
{
state = msgState.GetInt32();
}
}
var key = (message, state);
if (_shownStatusMessages.TryGetValue(key, out var originalMessage))
{
_shownStatusMessages.Remove(key);
_host.HideStatus(originalMessage);
}
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling hideStatus notification: {ex.Message}");
}
}
private void HandleCopyTextNotification(JsonElement paramsElement)
{
try
{
if (paramsElement.TryGetProperty("text", out var textProp) && textProp.ValueKind == JsonValueKind.String)
{
var text = textProp.GetString() ?? string.Empty;
ClipboardHelper.SetText(text);
}
}
catch (Exception ex)
{
Logger.LogWarning($"Error handling copyText notification: {ex.Message}");
}
}
private ICommandItem[] ParseCommandItems(JsonElement? result)
{
if (!result.HasValue || result.Value.ValueKind != JsonValueKind.Array)
{
return [];
}
var items = new List<ICommandItem>();
foreach (var element in result.Value.EnumerateArray())
{
items.Add(new JSCommandItemAdapter(element, _rpcConnection));
}
return items.ToArray();
}
private IFallbackCommandItem[]? ParseFallbackCommandItems(JsonElement? result)
{
if (!result.HasValue || result.Value.ValueKind != JsonValueKind.Array)
{
return null;
}
var items = new List<IFallbackCommandItem>();
foreach (var element in result.Value.EnumerateArray())
{
var adapter = new JSFallbackCommandItemAdapter(element, _rpcConnection);
items.Add(adapter);
var id = adapter.Id;
if (!string.IsNullOrEmpty(id))
{
_fallbackAdapters[id] = adapter;
}
}
return items.ToArray();
}
}

View File

@@ -0,0 +1,26 @@
// 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.UI.ViewModels.Services.JsonRpc;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
/// <summary>
/// Proxy that implements ICommandSettings for JavaScript extensions by wrapping
/// a JSContentPageProxy for the settings page. The settings page is fetched via
/// the <c>provider/getSettings</c> JSON-RPC method and then relies on existing
/// <c>contentPage/getContent</c> and <c>form/submit</c> handlers for interaction.
/// </summary>
internal sealed class JSCommandSettingsProxy : ICommandSettings
{
private readonly JSContentPageProxy _settingsPage;
public JSCommandSettingsProxy(string settingsPageId, JsonRpcConnection connection)
{
_settingsPage = new JSContentPageProxy(settingsPageId, connection);
}
public IContentPage SettingsPage => _settingsPage;
}

View File

@@ -0,0 +1,793 @@
// 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.
#pragma warning disable SA1402 // File may only contain a single type - closely related content adapters grouped together
#pragma warning disable SA1649 // File name should match first type name
using System.Text.Json;
using System.Text.Json.Nodes;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Services.JsonRpc;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
/// <summary>
/// Static helper for parsing IContent arrays and enhanced ICommandResult
/// including GoToPage, ShowToast, and Confirm args.
/// </summary>
internal static class JSCommandResultAdapter
{
internal static ICommandResult ParseCommandResult(JsonElement? result, JsonRpcConnection? connection = null)
{
if (!result.HasValue || result.Value.ValueKind == JsonValueKind.Null)
{
return CommandResult.Dismiss();
}
var kindValue = 0;
if (result.Value.TryGetProperty("Kind", out var kindProp) ||
result.Value.TryGetProperty("kind", out kindProp))
{
kindValue = kindProp.GetInt32();
}
var kind = (CommandResultKind)kindValue;
var hasArgs = result.Value.TryGetProperty("Args", out var argsProp) ||
result.Value.TryGetProperty("args", out argsProp);
if (hasArgs && argsProp.ValueKind == JsonValueKind.Object)
{
if (kind == CommandResultKind.GoToPage)
{
return ParseGoToPage(argsProp, connection);
}
if (kind == CommandResultKind.ShowToast)
{
return ParseShowToast(argsProp);
}
if (kind == CommandResultKind.Confirm)
{
return ParseConfirm(argsProp, connection);
}
}
if (kind == CommandResultKind.GoHome)
{
return CommandResult.GoHome();
}
if (kind == CommandResultKind.GoBack)
{
return CommandResult.GoBack();
}
if (kind == CommandResultKind.Hide)
{
return CommandResult.Hide();
}
if (kind == CommandResultKind.KeepOpen)
{
return CommandResult.KeepOpen();
}
return CommandResult.Dismiss();
}
internal static IContent[] ParseContentArray(JsonElement? result, string pageId, JsonRpcConnection connection)
{
if (!result.HasValue || result.Value.ValueKind != JsonValueKind.Array)
{
return [];
}
var count = result.Value.GetArrayLength();
var items = new IContent[count];
var i = 0;
foreach (var item in result.Value.EnumerateArray())
{
items[i++] = ParseContentItem(item, pageId, connection);
}
return items;
}
internal static IContent ParseContentItem(JsonElement element, string pageId, JsonRpcConnection connection)
{
var type = string.Empty;
if (element.TryGetProperty("type", out var typeProp) && typeProp.ValueKind == JsonValueKind.String)
{
type = typeProp.GetString() ?? string.Empty;
}
if (type == "form")
{
return new JSFormContentProxy(pageId, element, connection);
}
if (type == "tree")
{
return new JSTreeContentAdapter(element, pageId, connection);
}
if (type == "plainText")
{
return new JSPlainTextContentAdapter(element);
}
if (type == "image")
{
return new JSImageContentAdapter(element);
}
return new JSMarkdownContentAdapter(element);
}
internal static OptionalColor ParseOptionalColor(JsonElement element)
{
if (element.ValueKind != JsonValueKind.Object)
{
return default;
}
if (!element.TryGetProperty("hasValue", out var hasValueProp) || hasValueProp.ValueKind != JsonValueKind.True)
{
return default;
}
if (!element.TryGetProperty("color", out var colorProp) || colorProp.ValueKind != JsonValueKind.String)
{
return default;
}
var hex = colorProp.GetString();
if (string.IsNullOrEmpty(hex) || hex[0] != '#')
{
return default;
}
try
{
if (hex.Length == 7)
{
var r = Convert.ToByte(hex.Substring(1, 2), 16);
var g = Convert.ToByte(hex.Substring(3, 2), 16);
var b = Convert.ToByte(hex.Substring(5, 2), 16);
return ColorHelpers.FromRgb(r, g, b);
}
if (hex.Length == 9)
{
var a = Convert.ToByte(hex.Substring(1, 2), 16);
var r = Convert.ToByte(hex.Substring(3, 2), 16);
var g = Convert.ToByte(hex.Substring(5, 2), 16);
var b = Convert.ToByte(hex.Substring(7, 2), 16);
return ColorHelpers.FromArgb(a, r, g, b);
}
}
catch
{
// Invalid hex format; fall through to default
}
return default;
}
private static ICommandResult ParseGoToPage(JsonElement args, JsonRpcConnection? connection)
{
var pageId = string.Empty;
if ((args.TryGetProperty("PageId", out var pageIdProp) || args.TryGetProperty("pageId", out pageIdProp)) &&
pageIdProp.ValueKind == JsonValueKind.String)
{
pageId = pageIdProp.GetString() ?? string.Empty;
}
var navMode = NavigationMode.Push;
if (args.TryGetProperty("Mode", out var navModeProp) || args.TryGetProperty("mode", out navModeProp) || args.TryGetProperty("navigationMode", out navModeProp))
{
if (navModeProp.ValueKind == JsonValueKind.Number)
{
navMode = (NavigationMode)navModeProp.GetInt32();
}
else if (navModeProp.ValueKind == JsonValueKind.String)
{
var modeStr = navModeProp.GetString()?.ToLowerInvariant();
navMode = modeStr switch
{
"push" => NavigationMode.Push,
"goback" or "goBack" => NavigationMode.GoBack,
"gohome" or "goHome" => NavigationMode.GoHome,
_ => NavigationMode.Push,
};
}
}
// Resolve the page via JSONRPC if we have a connection and a pageId
IPage? resolvedPage = null;
if (connection != null && !string.IsNullOrEmpty(pageId))
{
try
{
var response = connection.SendRequestAsync(
"provider/getCommand",
new JsonObject { ["commandId"] = pageId },
CancellationToken.None).GetAwaiter().GetResult();
if (response.Error == null && response.Result.HasValue)
{
var command = JSCommandFactory.CreateCommandFromJson(response.Result.Value, connection);
if (command is IPage page)
{
resolvedPage = page;
}
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to resolve page '{pageId}': {ex.Message}");
}
}
return new JSGoToPageCommandResult
{
Kind = CommandResultKind.GoToPage,
Args = new GoToPageArgs { PageId = pageId, NavigationMode = navMode },
Page = resolvedPage,
};
}
private static ICommandResult ParseShowToast(JsonElement args)
{
string? message = null;
if ((args.TryGetProperty("Message", out var messageProp) || args.TryGetProperty("message", out messageProp)) &&
messageProp.ValueKind == JsonValueKind.String)
{
message = messageProp.GetString();
}
return CommandResult.ShowToast(new ToastArgs { Message = message });
}
private static ICommandResult ParseConfirm(JsonElement args, JsonRpcConnection? connection)
{
string? title = null;
string? description = null;
ICommand? primaryCommand = null;
var isCritical = false;
if ((args.TryGetProperty("Title", out var titleProp) || args.TryGetProperty("title", out titleProp)) &&
titleProp.ValueKind == JsonValueKind.String)
{
title = titleProp.GetString();
}
if ((args.TryGetProperty("Description", out var descProp) || args.TryGetProperty("description", out descProp)) &&
descProp.ValueKind == JsonValueKind.String)
{
description = descProp.GetString();
}
if (args.TryGetProperty("IsPrimaryCommandCritical", out var criticalProp) ||
args.TryGetProperty("isPrimaryCommandCritical", out criticalProp))
{
isCritical = criticalProp.ValueKind == JsonValueKind.True;
}
if (connection != null &&
(args.TryGetProperty("PrimaryCommand", out var cmdProp) || args.TryGetProperty("primaryCommand", out cmdProp)) &&
cmdProp.ValueKind == JsonValueKind.Object)
{
primaryCommand = new JSCommandAdapter(cmdProp, connection);
}
return CommandResult.Confirm(new ConfirmationArgs
{
Title = title,
Description = description,
PrimaryCommand = primaryCommand,
IsPrimaryCommandCritical = isCritical,
});
}
}
/// <summary>
/// Custom ICommandResult for GoToPage that carries a pre-resolved IPage reference.
/// This allows UnsafeHandleCommandResult to navigate directly to the page
/// without needing access to the JsonRpcConnection.
/// </summary>
internal sealed class JSGoToPageCommandResult : ICommandResult
{
public CommandResultKind Kind { get; init; }
public ICommandResultArgs? Args { get; init; }
public IPage? Page { get; init; }
}
/// <summary>
/// Adapts JSON content page data to IContentPage interface.
/// </summary>
internal sealed class JSContentPageProxy : IContentPage
{
private readonly string _pageId;
private readonly JsonElement _data;
private readonly JsonRpcConnection _connection;
public JSContentPageProxy(string pageId, JsonElement data, JsonRpcConnection connection)
{
_pageId = pageId;
_data = data;
_connection = connection;
}
public JSContentPageProxy(string pageId, JsonRpcConnection connection)
{
_pageId = pageId;
_data = default;
_connection = connection;
}
public string Name => GetStringProperty("name") ?? string.Empty;
public string Id => _pageId;
public IIconInfo Icon => GetIconInfo();
public string Title => GetStringProperty("title") ?? string.Empty;
public bool IsLoading
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("isLoading", out var prop))
{
return prop.ValueKind == JsonValueKind.True;
}
return false;
}
}
public OptionalColor AccentColor
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("accentColor", out var prop))
{
return JSCommandResultAdapter.ParseOptionalColor(prop);
}
return default;
}
}
public IDetails Details
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("details", out var detailsProp) && detailsProp.ValueKind == JsonValueKind.Object)
{
return new JSDetailsAdapter(detailsProp);
}
return null!;
}
}
public IContextItem[] Commands
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("commands", out var commandsProp) && commandsProp.ValueKind == JsonValueKind.Array)
{
var count = commandsProp.GetArrayLength();
var items = new IContextItem[count];
var i = 0;
foreach (var item in commandsProp.EnumerateArray())
{
items[i++] = new JSContextItemAdapter(item);
}
return items;
}
return [];
}
}
public IContent[] GetContent()
{
try
{
var response = _connection.SendRequestAsync(
"contentPage/getContent",
new JsonObject { ["pageId"] = _pageId },
CancellationToken.None).GetAwaiter().GetResult();
if (response.Error != null)
{
Logger.LogWarning($"Content page getContent error: {response.Error.Message}");
return [];
}
var resultElement = response.Result;
if (resultElement.HasValue &&
resultElement.Value.ValueKind == JsonValueKind.Object &&
resultElement.Value.TryGetProperty("content", out var contentArray))
{
resultElement = contentArray;
}
return JSCommandResultAdapter.ParseContentArray(resultElement, _pageId, _connection);
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to get content for page {_pageId}: {ex.Message}");
return [];
}
}
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add { }
remove { }
}
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged
{
add { }
remove { }
}
private string? GetStringProperty(string name)
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty(name, out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString();
}
return null;
}
private IIconInfo GetIconInfo()
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("icon", out var iconProp))
{
return JSIconInfoAdapter.FromJson(iconProp);
}
return new IconInfo(string.Empty);
}
}
/// <summary>
/// Adapts JSON markdown content data to IMarkdownContent interface.
/// </summary>
internal sealed class JSMarkdownContentAdapter : IMarkdownContent
{
private readonly JsonElement _data;
public JSMarkdownContentAdapter(JsonElement data)
{
_data = data;
}
public string Body
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("body", out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString() ?? string.Empty;
}
return string.Empty;
}
}
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add { }
remove { }
}
}
/// <summary>
/// Adapts JSON form content data to IFormContent interface.
/// </summary>
internal sealed class JSFormContentProxy : IFormContent
{
private readonly string _pageId;
private readonly JsonElement _data;
private readonly JsonRpcConnection _connection;
public JSFormContentProxy(string pageId, JsonElement data, JsonRpcConnection connection)
{
_pageId = pageId;
_data = data;
_connection = connection;
}
public string TemplateJson
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("templateJson", out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString() ?? string.Empty;
}
return string.Empty;
}
}
public string DataJson
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("dataJson", out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString() ?? string.Empty;
}
return string.Empty;
}
}
public string StateJson
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("stateJson", out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString() ?? string.Empty;
}
return string.Empty;
}
}
public ICommandResult SubmitForm(string inputs, string data)
{
try
{
var response = _connection.SendRequestAsync(
"form/submit",
new JsonObject { ["pageId"] = _pageId, ["inputs"] = inputs, ["data"] = data },
CancellationToken.None).GetAwaiter().GetResult();
if (response.Error != null)
{
Logger.LogWarning($"Form submit error: {response.Error.Message}");
return CommandResult.KeepOpen();
}
return JSCommandResultAdapter.ParseCommandResult(response.Result, _connection);
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to submit form for page {_pageId}: {ex.Message}");
return CommandResult.KeepOpen();
}
}
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add { }
remove { }
}
}
/// <summary>
/// Adapts JSON tree content data to ITreeContent interface.
/// </summary>
internal sealed class JSTreeContentAdapter : ITreeContent
{
private readonly JsonElement _data;
private readonly string _pageId;
private readonly JsonRpcConnection _connection;
public JSTreeContentAdapter(JsonElement data, string pageId, JsonRpcConnection connection)
{
_data = data;
_pageId = pageId;
_connection = connection;
}
public IContent RootContent
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("rootContent", out var rootProp) && rootProp.ValueKind == JsonValueKind.Object)
{
return JSCommandResultAdapter.ParseContentItem(rootProp, _pageId, _connection);
}
return new JSMarkdownContentAdapter(default);
}
}
public IContent[] GetChildren()
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("children", out var childrenProp) && childrenProp.ValueKind == JsonValueKind.Array)
{
var count = childrenProp.GetArrayLength();
var items = new IContent[count];
var i = 0;
foreach (var child in childrenProp.EnumerateArray())
{
items[i++] = JSCommandResultAdapter.ParseContentItem(child, _pageId, _connection);
}
return items;
}
return [];
}
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add { }
remove { }
}
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged
{
add { }
remove { }
}
}
/// <summary>
/// Adapts JSON context item data to IContextItem marker interface.
/// </summary>
internal sealed class JSContextItemAdapter : IContextItem
{
private readonly JsonElement _data;
public JSContextItemAdapter(JsonElement data)
{
_data = data;
}
}
/// <summary>
/// Adapts JSON plain text content data to IPlainTextContent interface.
/// </summary>
internal sealed class JSPlainTextContentAdapter : IPlainTextContent
{
private readonly JsonElement _data;
public JSPlainTextContentAdapter(JsonElement data)
{
_data = data;
}
public string Text
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("text", out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString() ?? string.Empty;
}
return string.Empty;
}
}
public FontFamily FontFamily
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("fontFamily", out var prop) && prop.ValueKind == JsonValueKind.String)
{
var value = prop.GetString();
return value == "monospace" ? FontFamily.Monospace : FontFamily.UserInterface;
}
return FontFamily.UserInterface;
}
}
public bool WrapWords
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("wrapWords", out var prop))
{
return prop.ValueKind == JsonValueKind.True;
}
return true;
}
}
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add { }
remove { }
}
}
/// <summary>
/// Adapts JSON image content data to IImageContent interface.
/// </summary>
internal sealed class JSImageContentAdapter : IImageContent
{
private readonly JsonElement _data;
public JSImageContentAdapter(JsonElement data)
{
_data = data;
}
public IIconInfo Image
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("image", out var imageProp))
{
return JSIconInfoAdapter.FromJson(imageProp);
}
return new IconInfo(string.Empty);
}
}
public int MaxWidth
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("maxWidth", out var prop) && prop.ValueKind == JsonValueKind.Number)
{
return prop.GetInt32();
}
return -1; // Unlimited
}
}
public int MaxHeight
{
get
{
if (_data.ValueKind == JsonValueKind.Object &&
_data.TryGetProperty("maxHeight", out var prop) && prop.ValueKind == JsonValueKind.Number)
{
return prop.GetInt32();
}
return -1; // Unlimited
}
}
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
{
add { }
remove { }
}
}

View File

@@ -0,0 +1,223 @@
// 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.
#pragma warning disable SA1402 // File may only contain a single type - manifest and related types grouped together
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
/// <summary>
/// Represents the resolved extension manifest, built from a package.json file
/// that contains a "cmdpal" section (similar to VS Code's contributes field).
/// </summary>
public sealed record JSExtensionManifest
{
/// <summary>
/// Gets the internal identifier for the extension (from package.json "name", required).
/// </summary>
public string? Name { get; init; }
/// <summary>
/// Gets the display name shown to users (from cmdpal.displayName).
/// </summary>
public string? DisplayName { get; init; }
/// <summary>
/// Gets the version string (from package.json "version").
/// </summary>
public string? Version { get; init; }
/// <summary>
/// Gets the description of the extension (from package.json "description").
/// </summary>
public string? Description { get; init; }
/// <summary>
/// Gets the icon glyph or relative path (from cmdpal.icon).
/// </summary>
public string? Icon { get; init; }
/// <summary>
/// Gets the entry point script path (cmdpal.main overrides package.json "main", required).
/// </summary>
public string? Main { get; init; }
/// <summary>
/// Gets the publisher or author name (from cmdpal.publisher).
/// </summary>
public string? Publisher { get; init; }
/// <summary>
/// Gets a value indicating whether debug mode is enabled for this extension.
/// When true, the Node.js process is started with --inspect to allow debugger attachment.
/// </summary>
public bool Debug { get; init; }
/// <summary>
/// Gets the port number for the Node.js inspector when debug mode is enabled.
/// If not specified, a default port starting at 9229 is assigned automatically.
/// </summary>
public int? DebugPort { get; init; }
/// <summary>
/// Gets the engine requirements (from package.json "engines").
/// </summary>
public JSExtensionEngines? Engines { get; init; }
/// <summary>
/// Gets the capabilities exposed by the extension (from cmdpal.capabilities).
/// </summary>
public string[]? Capabilities { get; init; }
/// <summary>
/// Validates that required fields are present.
/// </summary>
/// <returns>True if the manifest is valid; otherwise, false.</returns>
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Main);
}
/// <summary>
/// Loads an extension manifest from a package.json file that contains a "cmdpal" section.
/// </summary>
/// <param name="packageJsonPath">The full path to the package.json file.</param>
/// <returns>The resolved manifest if successful and valid; otherwise, null.</returns>
public static async Task<JSExtensionManifest?> LoadFromFileAsync(string packageJsonPath)
{
if (string.IsNullOrWhiteSpace(packageJsonPath))
{
return null;
}
if (!File.Exists(packageJsonPath))
{
return null;
}
try
{
using var stream = File.OpenRead(packageJsonPath);
var packageJson = await JsonSerializer.DeserializeAsync(
stream,
JSExtensionManifestJsonContext.Default.JSPackageJson);
if (packageJson?.CmdPal is null)
{
return null;
}
var cmdpal = packageJson.CmdPal;
// cmdpal.main overrides top-level main
var entryPoint = !string.IsNullOrWhiteSpace(cmdpal.Main)
? cmdpal.Main
: packageJson.Main;
var manifest = new JSExtensionManifest
{
Name = packageJson.Name,
DisplayName = cmdpal.DisplayName,
Version = packageJson.Version,
Description = packageJson.Description,
Icon = cmdpal.Icon,
Main = entryPoint,
Publisher = cmdpal.Publisher,
Debug = cmdpal.Debug,
DebugPort = cmdpal.DebugPort,
Engines = packageJson.Engines,
Capabilities = cmdpal.Capabilities,
};
if (!manifest.IsValid())
{
return null;
}
return manifest;
}
catch
{
return null;
}
}
}
/// <summary>
/// Represents the top-level package.json structure for extension discovery.
/// </summary>
public sealed record JSPackageJson
{
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("version")]
public string? Version { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("main")]
public string? Main { get; init; }
[JsonPropertyName("engines")]
public JSExtensionEngines? Engines { get; init; }
[JsonPropertyName("cmdpal")]
public JSCmdPalSection? CmdPal { get; init; }
}
/// <summary>
/// Represents the "cmdpal" section within package.json containing CmdPal-specific metadata.
/// </summary>
public sealed record JSCmdPalSection
{
[JsonPropertyName("displayName")]
public string? DisplayName { get; init; }
[JsonPropertyName("icon")]
public string? Icon { get; init; }
[JsonPropertyName("main")]
public string? Main { get; init; }
[JsonPropertyName("publisher")]
public string? Publisher { get; init; }
[JsonPropertyName("debug")]
public bool Debug { get; init; }
[JsonPropertyName("debugPort")]
public int? DebugPort { get; init; }
[JsonPropertyName("capabilities")]
public string[]? Capabilities { get; init; }
}
/// <summary>
/// Represents the engine requirements for a JavaScript/TypeScript extension.
/// </summary>
public sealed record JSExtensionEngines
{
/// <summary>
/// Gets the Node.js version requirement (e.g., ">=18").
/// </summary>
[JsonPropertyName("node")]
public string? Node { get; init; }
}
/// <summary>
/// JSON serialization context for extension manifest types.
/// </summary>
[JsonSerializable(typeof(JSPackageJson))]
[JsonSerializable(typeof(JSCmdPalSection))]
[JsonSerializable(typeof(JSExtensionManifest))]
[JsonSerializable(typeof(JSExtensionEngines))]
[JsonSerializable(typeof(string[]))]
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
internal sealed partial class JSExtensionManifestJsonContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,559 @@
// 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.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Services.JsonRpc;
using Microsoft.CommandPalette.Extensions;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
/// <summary>
/// Adapter that manages a single Node.js extension process and presents it as an IExtensionWrapper to the CmdPal host.
/// </summary>
public sealed class JSExtensionWrapper : IExtensionWrapper, IDisposable
{
private readonly JSExtensionManifest _manifest;
private readonly string _manifestDirectory;
private readonly Lock _lock = new();
private readonly List<ProviderType> _providerTypes = [];
private static int _nextDebugPort = 9229;
private Process? _nodeProcess;
private JsonRpcConnection? _rpcConnection;
private JSCommandProviderProxy? _commandProviderProxy;
private bool _isDisposed;
private int _consecutiveCrashCount;
/// <summary>
/// Gets the number of times this extension has been restarted (due to crashes or hot-reload).
/// </summary>
public int RestartCount { get; private set; }
/// <summary>
/// Gets a value indicating whether the extension is considered healthy.
/// An extension becomes unhealthy after exceeding 3 consecutive crashes.
/// </summary>
public bool IsHealthy { get; private set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="JSExtensionWrapper"/> class.
/// </summary>
/// <param name="manifest">The parsed extension manifest.</param>
/// <param name="manifestDirectory">The directory containing the manifest file.</param>
public JSExtensionWrapper(JSExtensionManifest manifest, string manifestDirectory)
{
_manifest = manifest ?? throw new ArgumentNullException(nameof(manifest));
_manifestDirectory = manifestDirectory ?? throw new ArgumentNullException(nameof(manifestDirectory));
if (!_manifest.IsValid())
{
throw new ArgumentException("Invalid manifest", nameof(manifest));
}
// Map manifest capabilities to provider types
var caps = _manifest.Capabilities;
if (caps != null)
{
foreach (var cap in caps)
{
if (string.Equals(cap, "commands", StringComparison.OrdinalIgnoreCase))
{
AddProviderType(ProviderType.Commands);
}
}
}
else
{
// Default: assume commands capability if not specified
AddProviderType(ProviderType.Commands);
}
}
public string PackageDisplayName => _manifest.DisplayName ?? _manifest.Name ?? "Unknown";
public string ExtensionDisplayName => _manifest.DisplayName ?? _manifest.Name ?? "Unknown";
public string PackageFullName => $"js!{_manifest.Name}";
public string PackageFamilyName => $"js!{_manifest.Name}";
public string Publisher => _manifest.Publisher ?? "Unknown";
public string ExtensionClassId
{
get
{
// Generate a deterministic "GUID-like" identifier from the manifest name
if (string.IsNullOrWhiteSpace(_manifest.Name))
{
return "unknown";
}
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(_manifest.Name));
var hashString = Convert.ToHexString(hash);
return $"js-{hashString[..32]}";
}
}
public DateTimeOffset InstalledDate
{
get
{
try
{
var manifestPath = Path.Combine(_manifestDirectory, "package.json");
if (File.Exists(manifestPath))
{
return File.GetCreationTimeUtc(manifestPath);
}
}
catch
{
// Fallback if file operations fail
}
return DateTimeOffset.UtcNow;
}
}
public PackageVersion Version
{
get
{
if (string.IsNullOrWhiteSpace(_manifest.Version))
{
return new PackageVersion { Major = 1, Minor = 0, Build = 0, Revision = 0 };
}
var parts = _manifest.Version.Split('.');
return new PackageVersion
{
Major = parts.Length > 0 && ushort.TryParse(parts[0], out var major) ? major : (ushort)1,
Minor = parts.Length > 1 && ushort.TryParse(parts[1], out var minor) ? minor : (ushort)0,
Build = parts.Length > 2 && ushort.TryParse(parts[2], out var build) ? build : (ushort)0,
Revision = 0,
};
}
}
public string ExtensionUniqueId => $"js!{_manifest.Name}";
public bool IsRunning()
{
lock (_lock)
{
if (_nodeProcess == null || _rpcConnection == null)
{
return false;
}
try
{
return !_nodeProcess.HasExited;
}
catch
{
return false;
}
}
}
public async Task StartExtensionAsync()
{
ObjectDisposedException.ThrowIf(_isDisposed, this);
lock (_lock)
{
if (_nodeProcess != null && _rpcConnection != null)
{
try
{
if (!_nodeProcess.HasExited)
{
return; // Already running
}
}
catch
{
// Process handle invalid — fall through to restart
}
}
}
Logger.LogDebug($"Starting JS extension {_manifest.DisplayName ?? _manifest.Name}");
try
{
var entryPoint = Path.Combine(_manifestDirectory, _manifest.Main ?? string.Empty);
// Retry up to 5 times with 1s backoff — the entry point file may
// still be in flight when the directory watcher fires.
for (var attempt = 0; attempt < 5; attempt++)
{
if (File.Exists(entryPoint))
{
break;
}
if (attempt == 4)
{
Logger.LogError($"Entry point not found after retries: {entryPoint}");
return;
}
await Task.Delay(1000).ConfigureAwait(false);
}
var psi = new ProcessStartInfo
{
FileName = "node",
Arguments = BuildNodeArguments(entryPoint),
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = _manifestDirectory,
};
var nodeProcess = Process.Start(psi);
if (nodeProcess == null)
{
Logger.LogError($"Failed to start Node.js process for {_manifest.Name}");
return;
}
// Drain stderr asynchronously to prevent the process from blocking
// if the stderr pipe buffer fills up
nodeProcess.ErrorDataReceived += (_, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
{
Logger.LogDebug($"[{_manifest.Name} stderr] {e.Data}");
}
};
nodeProcess.BeginErrorReadLine();
var loggerAdapter = new LoggerAdapter();
var rpcConnection = new JsonRpcConnection(nodeProcess, loggerAdapter);
rpcConnection.OnError += ex => Logger.LogError($"JSON-RPC error in {_manifest.Name}: {ex.Message}");
rpcConnection.OnDisconnected += HandleDisconnection;
// Store the process and connection BEFORE awaiting initialize so
// IsRunning() reflects the actual process state immediately.
lock (_lock)
{
_nodeProcess = nodeProcess;
_rpcConnection = rpcConnection;
}
nodeProcess.Exited += (_, _) => HandleDisconnection();
nodeProcess.EnableRaisingEvents = true;
rpcConnection.StartListening();
// Send initialize request to the extension
var initResponse = await rpcConnection.SendRequestAsync(
"initialize",
new JsonObject { ["extensionId"] = _manifest.Name },
CancellationToken.None).ConfigureAwait(false);
if (initResponse.Error != null)
{
Logger.LogError($"Initialization failed for {_manifest.Name}: {initResponse.Error.Message}");
SignalDispose();
return;
}
// Extension started and initialized successfully — reset crash tracking
ResetCrashCount();
Logger.LogInfo($"Successfully started JS extension {_manifest.DisplayName ?? _manifest.Name}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to start JS extension {_manifest.Name}: {ex.Message}");
SignalDispose();
}
}
public void SignalDispose()
{
lock (_lock)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
try
{
if (_rpcConnection != null && IsRunning())
{
// Send dispose notification (fire-and-forget)
_rpcConnection.SendNotificationAsync("dispose", null, CancellationToken.None).Wait(TimeSpan.FromSeconds(2));
}
}
catch (Exception ex)
{
Logger.LogWarning($"Error sending dispose notification to {_manifest.Name}: {ex.Message}");
}
try
{
_rpcConnection?.Dispose();
}
catch
{
// Best effort
}
try
{
if (_nodeProcess != null && !_nodeProcess.HasExited)
{
_nodeProcess.Kill(entireProcessTree: true);
_nodeProcess.WaitForExit(2000);
}
_nodeProcess?.Dispose();
}
catch (Exception ex)
{
Logger.LogWarning($"Error terminating Node.js process for {_manifest.Name}: {ex.Message}");
}
_nodeProcess = null;
_rpcConnection = null;
_commandProviderProxy = null;
}
}
public void Dispose()
{
SignalDispose();
}
public IExtension? GetExtensionObject()
{
// JS extensions don't have COM objects - the wrapper itself is the bridge
return null;
}
public void AddProviderType(ProviderType providerType)
{
lock (_lock)
{
if (!_providerTypes.Contains(providerType))
{
_providerTypes.Add(providerType);
}
}
}
public bool HasProviderType(ProviderType providerType)
{
lock (_lock)
{
return _providerTypes.Contains(providerType);
}
}
public async Task<T?> GetProviderAsync<T>()
where T : class
{
if (typeof(T) != typeof(ICommandProvider))
{
return null;
}
await StartExtensionAsync().ConfigureAwait(false);
lock (_lock)
{
if (!IsRunning() || _rpcConnection == null)
{
return null;
}
if (_commandProviderProxy == null)
{
_commandProviderProxy = new JSCommandProviderProxy(_rpcConnection, _manifest);
}
return _commandProviderProxy as T;
}
}
public async Task<IEnumerable<T>> GetListOfProvidersAsync<T>()
where T : class
{
var provider = await GetProviderAsync<T>().ConfigureAwait(false);
if (provider != null)
{
return new[] { provider };
}
return [];
}
private void HandleDisconnection()
{
lock (_lock)
{
if (!_isDisposed)
{
_consecutiveCrashCount++;
Logger.LogWarning($"Node.js process for {_manifest.Name} disconnected unexpectedly (crash #{_consecutiveCrashCount})");
if (_consecutiveCrashCount > 3)
{
IsHealthy = false;
Logger.LogError($"JS extension {_manifest.Name} disabled after {_consecutiveCrashCount} consecutive crashes");
}
_nodeProcess = null;
_rpcConnection = null;
_commandProviderProxy = null;
}
}
}
/// <summary>
/// Resets the consecutive crash counter. Call after a successful operation to indicate the extension is stable.
/// </summary>
internal void ResetCrashCount()
{
lock (_lock)
{
_consecutiveCrashCount = 0;
}
}
/// <summary>
/// Stops the running Node.js process and starts a fresh one.
/// Used by hot-reload to restart the extension after source file changes.
/// </summary>
internal async Task RestartAsync()
{
lock (_lock)
{
if (_isDisposed || !IsHealthy)
{
return;
}
}
Logger.LogInfo($"Restarting JS extension {_manifest.DisplayName ?? _manifest.Name}");
// Gracefully shut down the current process
lock (_lock)
{
try
{
if (_rpcConnection != null && IsRunning())
{
_rpcConnection.SendNotificationAsync("dispose", null, CancellationToken.None).Wait(TimeSpan.FromSeconds(2));
}
}
catch (Exception ex)
{
Logger.LogWarning($"Error sending dispose during restart of {_manifest.Name}: {ex.Message}");
}
try
{
_rpcConnection?.Dispose();
}
catch
{
// Best effort
}
try
{
if (_nodeProcess != null && !_nodeProcess.HasExited)
{
_nodeProcess.Kill(entireProcessTree: true);
_nodeProcess.WaitForExit(2000);
}
_nodeProcess?.Dispose();
}
catch (Exception ex)
{
Logger.LogWarning($"Error terminating Node.js process during restart of {_manifest.Name}: {ex.Message}");
}
_nodeProcess = null;
_rpcConnection = null;
_commandProviderProxy = null;
_isDisposed = false;
}
RestartCount++;
await StartExtensionAsync().ConfigureAwait(false);
}
/// <summary>
/// Gets the manifest directory path for this extension.
/// </summary>
internal string ManifestDirectory => _manifestDirectory;
private string BuildNodeArguments(string entryPoint)
{
if (_manifest.Debug)
{
var port = _manifest.DebugPort ?? Interlocked.Increment(ref _nextDebugPort);
var debugUrl = $"chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:{port}";
Logger.LogInfo($"Debug mode enabled for {_manifest.Name} on port {port}. Attach debugger at: {debugUrl}");
return $"--inspect={port} \"{entryPoint}\"";
}
return $"\"{entryPoint}\"";
}
private sealed class LoggerAdapter : Microsoft.Extensions.Logging.ILogger
{
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull => null;
public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) => true;
public void Log<TState>(
Microsoft.Extensions.Logging.LogLevel logLevel,
Microsoft.Extensions.Logging.EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
var message = formatter(state, exception);
switch (logLevel)
{
case Microsoft.Extensions.Logging.LogLevel.Error:
case Microsoft.Extensions.Logging.LogLevel.Critical:
Logger.LogError(message);
break;
case Microsoft.Extensions.Logging.LogLevel.Warning:
Logger.LogWarning(message);
break;
case Microsoft.Extensions.Logging.LogLevel.Information:
Logger.LogInfo(message);
break;
default:
Logger.LogDebug(message);
break;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -120,7 +120,7 @@ public partial class ProviderSettingsViewModel : ObservableObject
ProviderSettings = s.ProviderSettings.SetItem(_provider.ProviderId, newSettings),
});
_providerSettings = newSettings;
WeakReferenceMessenger.Default.Send<ReloadCommandsMessage>(new());
WeakReferenceMessenger.Default.Send<ProviderEnabledStateChangedMessage>(new(_provider.ProviderId, value));
OnPropertyChanged(nameof(IsEnabled));
OnPropertyChanged(nameof(ExtensionSubtext));
OnPropertyChanged(nameof(Icon));

View File

@@ -0,0 +1,80 @@
// 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.CommandPalette.Extensions;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Extension service that manages in-process built-in command providers
/// registered in the DI container as <see cref="ICommandProvider"/>.
/// </summary>
public sealed class BuiltInExtensionService : IExtensionService
{
private readonly IEnumerable<ICommandProvider> _commandProviders;
private readonly TaskScheduler _taskScheduler;
private readonly List<IExtensionWrapper> _wrappers = [];
#pragma warning disable CS0067 // Events are required by the interface but not raised by this implementation
public event TypedEventHandler<IExtensionService, IEnumerable<CommandProviderWrapper>>? OnProviderAdded;
public event TypedEventHandler<IExtensionService, IEnumerable<CommandProviderWrapper>>? OnProviderRemoved;
#pragma warning restore CS0067
public BuiltInExtensionService(IEnumerable<ICommandProvider> commandProviders, TaskScheduler taskScheduler)
{
_commandProviders = commandProviders;
_taskScheduler = taskScheduler;
}
public Task<IEnumerable<CommandProviderWrapper>> LoadProvidersAsync(CancellationToken ct)
{
if (ct.IsCancellationRequested)
{
return Task.FromResult<IEnumerable<CommandProviderWrapper>>([]);
}
var wrappers = new List<CommandProviderWrapper>();
foreach (var provider in _commandProviders)
{
wrappers.Add(new CommandProviderWrapper(provider, _taskScheduler));
}
return Task.FromResult<IEnumerable<CommandProviderWrapper>>(wrappers);
}
public Task SignalStopAsync()
{
// Built-in providers are in-proc and don't need explicit stop signaling.
return Task.CompletedTask;
}
public Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
{
return Task.FromResult<IEnumerable<IExtensionWrapper>>(_wrappers);
}
public Task<IEnumerable<IExtensionWrapper>> RefreshInstalledExtensionsAsync(bool includeDisabledExtensions = false)
{
// Built-in set is fixed at startup; refresh is a no-op.
return GetInstalledExtensionsAsync(includeDisabledExtensions);
}
public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
{
return _wrappers.FirstOrDefault(w => w.ExtensionUniqueId == extensionUniqueId);
}
public void EnableExtension(string extensionUniqueId)
{
// Nothing to do here. We're built-in extensions.
}
public void DisableExtension(string extensionUniqueId)
{
// Nothing to do here. We're built-in extensions.
}
}

View File

@@ -0,0 +1,48 @@
// 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.Diagnostics;
using Microsoft.CmdPal.Common.Services;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Represents the outcome of attempting to start a single WinRT extension.
/// </summary>
internal sealed class ExtensionStartResult
{
public IExtensionWrapper Extension { get; }
public CommandProviderWrapper? Wrapper { get; private init; }
public Task? PendingStartTask { get; private init; }
public Stopwatch? Stopwatch { get; private init; }
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Wrapper))]
public bool IsStarted => Wrapper is not null;
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(PendingStartTask), nameof(Stopwatch))]
public bool IsTimedOut => PendingStartTask is not null;
private ExtensionStartResult(IExtensionWrapper extension)
{
Extension = extension;
}
public static ExtensionStartResult Started(IExtensionWrapper extension, CommandProviderWrapper wrapper)
{
return new ExtensionStartResult(extension) { Wrapper = wrapper };
}
public static ExtensionStartResult TimedOut(IExtensionWrapper extension, Task pendingStartTask, Stopwatch sw)
{
return new ExtensionStartResult(extension) { PendingStartTask = pendingStartTask, Stopwatch = sw };
}
public static ExtensionStartResult Failed(IExtensionWrapper extension)
{
return new ExtensionStartResult(extension);
}
}

View File

@@ -0,0 +1,67 @@
// 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 Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
public interface IExtensionService
{
/// <summary>
/// Loads command providers managed by this service. Returns providers that
/// are immediately ready. Slow or late providers arrive via <see cref="OnProviderAdded"/>.
/// </summary>
/// <param name="ct">Cancellation token owned by the caller to cancel in-flight loading.</param>
/// <returns>Command provider wrappers that are started and ready for command loading.</returns>
Task<IEnumerable<CommandProviderWrapper>> LoadProvidersAsync(CancellationToken ct);
/// <summary>
/// Signals running providers managed by this service to stop/dispose.
/// </summary>
Task SignalStopAsync();
/// <summary>
/// Gets the currently cached installed extensions managed by this service.
/// </summary>
/// <param name="includeDisabledExtensions">True to include disabled extensions in the result.</param>
/// <returns>A sequence of installed extensions from the current in-memory cache.</returns>
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false);
/// <summary>
/// Forces a fresh scan of installed extensions and updates the in-memory cache.
/// </summary>
/// <param name="includeDisabledExtensions">True to include disabled extensions in the result.</param>
/// <returns>A sequence of installed extensions after the cache has been rebuilt.</returns>
Task<IEnumerable<IExtensionWrapper>> RefreshInstalledExtensionsAsync(bool includeDisabledExtensions = false);
/// <summary>
/// Gets a cached installed extension by its unique id.
/// </summary>
/// <param name="extensionUniqueId">The unique id of the extension to look up.</param>
/// <returns>The cached extension if found; otherwise, null.</returns>
IExtensionWrapper? GetInstalledExtension(string extensionUniqueId);
/// <summary>
/// Enables an installed extension by unique id.
/// </summary>
/// <param name="extensionUniqueId">The unique id of the extension to enable.</param>
void EnableExtension(string extensionUniqueId);
/// <summary>
/// Disables an installed extension by unique id.
/// </summary>
/// <param name="extensionUniqueId">The unique id of the extension to disable.</param>
void DisableExtension(string extensionUniqueId);
/// <summary>
/// Raised when one or more providers become available (late start, new package install, etc.).
/// </summary>
event TypedEventHandler<IExtensionService, IEnumerable<CommandProviderWrapper>>? OnProviderAdded;
/// <summary>
/// Raised when one or more providers are removed (package uninstall, etc.).
/// </summary>
event TypedEventHandler<IExtensionService, IEnumerable<CommandProviderWrapper>>? OnProviderRemoved;
}

View File

@@ -0,0 +1,488 @@
// 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.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary>
/// Extension service that manages JavaScript/TypeScript extensions running as individual Node.js processes.
/// Each extension gets its own process communicating over JSON-RPC 2.0 via stdio with LSP-style framing.
/// Supports hot-reload (via FileSystemWatcher), crash recovery, and debug attachment.
/// </summary>
public sealed class JavaScriptExtensionService : IExtensionService, IDisposable
{
private static readonly string ExtensionsPath = GetDefaultExtensionsPath();
private readonly TaskScheduler _taskScheduler;
private readonly Lock _extensionsLock = new();
private readonly List<JSExtensionWrapper> _extensions = [];
private readonly List<CommandProviderWrapper> _providerWrappers = [];
private readonly HashSet<string> _disabledExtensions = [];
private readonly Lock _sourceWatcherLock = new();
private readonly Dictionary<string, FileSystemWatcher> _sourceFileWatchers = [];
private readonly Dictionary<string, Timer> _debounceTimers = [];
private FileSystemWatcher? _directoryWatcher;
private bool _disposed;
#pragma warning disable CS0067 // Events are required by the interface but not raised by this implementation yet
public event TypedEventHandler<IExtensionService, IEnumerable<CommandProviderWrapper>>? OnProviderAdded;
public event TypedEventHandler<IExtensionService, IEnumerable<CommandProviderWrapper>>? OnProviderRemoved;
#pragma warning restore CS0067
public JavaScriptExtensionService(TaskScheduler taskScheduler)
{
_taskScheduler = taskScheduler;
}
public async Task<IEnumerable<CommandProviderWrapper>> LoadProvidersAsync(CancellationToken ct)
{
if (ct.IsCancellationRequested)
{
return [];
}
var sw = Stopwatch.StartNew();
if (!Directory.Exists(ExtensionsPath))
{
try
{
Directory.CreateDirectory(ExtensionsPath);
Logger.LogDebug($"Created JS extensions directory: {ExtensionsPath}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to create JS extensions directory {ExtensionsPath}: {ex.Message}");
return [];
}
}
var wrappers = await DiscoverAndLoadExtensionsAsync(ExtensionsPath, ct).ConfigureAwait(false);
StartDirectoryWatcher();
sw.Stop();
Logger.LogInfo($"JavaScriptExtensionService: Loaded {wrappers.Count} extension(s) in {sw.ElapsedMilliseconds} ms");
return wrappers;
}
public Task SignalStopAsync()
{
StopDirectoryWatcher();
StopAllSourceFileWatchers();
lock (_extensionsLock)
{
foreach (var ext in _extensions)
{
try
{
ext.SignalDispose();
}
catch (Exception ex)
{
Logger.LogError($"Failed to stop JS extension {ext.ExtensionDisplayName}: {ex.Message}");
}
}
_extensions.Clear();
_providerWrappers.Clear();
}
return Task.CompletedTask;
}
public Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
{
lock (_extensionsLock)
{
if (includeDisabledExtensions)
{
return Task.FromResult<IEnumerable<IExtensionWrapper>>(_extensions.ToList());
}
return Task.FromResult<IEnumerable<IExtensionWrapper>>(
_extensions.Where(e => !_disabledExtensions.Contains(e.ExtensionUniqueId)).ToList());
}
}
public async Task<IEnumerable<IExtensionWrapper>> RefreshInstalledExtensionsAsync(bool includeDisabledExtensions = false)
{
// Re-scan the directory for any new extensions
if (Directory.Exists(ExtensionsPath))
{
var subdirs = Directory.GetDirectories(ExtensionsPath);
foreach (var subdir in subdirs)
{
var manifestPath = Path.Combine(subdir, "package.json");
if (!File.Exists(manifestPath))
{
continue;
}
// Skip if already loaded
var dirName = Path.GetFileName(subdir);
bool alreadyLoaded;
lock (_extensionsLock)
{
alreadyLoaded = _extensions.Any(e => e.ManifestDirectory == subdir);
}
if (!alreadyLoaded)
{
await LoadExtensionFromDirectoryAsync(subdir, CancellationToken.None).ConfigureAwait(false);
}
}
}
return await GetInstalledExtensionsAsync(includeDisabledExtensions).ConfigureAwait(false);
}
public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
{
lock (_extensionsLock)
{
return _extensions.FirstOrDefault(e => e.ExtensionUniqueId == extensionUniqueId);
}
}
public void EnableExtension(string extensionUniqueId)
{
_disabledExtensions.Remove(extensionUniqueId);
}
public void DisableExtension(string extensionUniqueId)
{
_disabledExtensions.Add(extensionUniqueId);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
StopDirectoryWatcher();
StopAllSourceFileWatchers();
lock (_extensionsLock)
{
foreach (var ext in _extensions)
{
ext.Dispose();
}
_extensions.Clear();
_providerWrappers.Clear();
}
}
private async Task<List<CommandProviderWrapper>> DiscoverAndLoadExtensionsAsync(string extensionsPath, CancellationToken ct)
{
var wrappers = new List<CommandProviderWrapper>();
if (!Directory.Exists(extensionsPath))
{
return wrappers;
}
var subdirectories = Directory.GetDirectories(extensionsPath);
foreach (var subdir in subdirectories)
{
if (ct.IsCancellationRequested)
{
break;
}
var wrapper = await LoadExtensionFromDirectoryAsync(subdir, ct).ConfigureAwait(false);
if (wrapper != null)
{
wrappers.Add(wrapper);
}
}
return wrappers;
}
private async Task<CommandProviderWrapper?> LoadExtensionFromDirectoryAsync(string extensionDirectory, CancellationToken ct)
{
var manifestPath = Path.Combine(extensionDirectory, "package.json");
if (!File.Exists(manifestPath))
{
return null;
}
try
{
var manifest = await JSExtensionManifest.LoadFromFileAsync(manifestPath).ConfigureAwait(false);
if (manifest == null)
{
Logger.LogWarning($"Invalid manifest at {manifestPath}");
return null;
}
var extensionWrapper = new JSExtensionWrapper(manifest, extensionDirectory);
lock (_extensionsLock)
{
_extensions.Add(extensionWrapper);
}
await extensionWrapper.StartExtensionAsync().ConfigureAwait(false);
if (!extensionWrapper.IsRunning())
{
Logger.LogError($"Failed to start JS extension {manifest.DisplayName ?? manifest.Name}");
return null;
}
var provider = await extensionWrapper.GetProviderAsync<ICommandProvider>().ConfigureAwait(false);
if (provider == null)
{
Logger.LogWarning($"JS extension {manifest.DisplayName ?? manifest.Name} does not provide ICommandProvider");
return null;
}
var wrapper = new CommandProviderWrapper(extensionWrapper, provider, _taskScheduler);
lock (_extensionsLock)
{
_providerWrappers.Add(wrapper);
}
Logger.LogInfo($"Loaded JS extension: {manifest.DisplayName ?? manifest.Name}");
// Start source file watcher for hot-reload in dev mode
StartSourceFileWatcher(extensionWrapper, extensionDirectory);
return wrapper;
}
catch (Exception ex)
{
Logger.LogError($"Failed to load JS extension from {extensionDirectory}: {ex.Message}");
return null;
}
}
private void StartDirectoryWatcher()
{
if (!Directory.Exists(ExtensionsPath))
{
return;
}
try
{
_directoryWatcher = new FileSystemWatcher(ExtensionsPath)
{
NotifyFilter = NotifyFilters.DirectoryName,
EnableRaisingEvents = true,
};
_directoryWatcher.Created += OnExtensionDirectoryCreated;
_directoryWatcher.Deleted += OnExtensionDirectoryDeleted;
Logger.LogDebug($"Started directory watcher for {ExtensionsPath}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to start directory watcher for {ExtensionsPath}: {ex.Message}");
}
}
private void StopDirectoryWatcher()
{
if (_directoryWatcher != null)
{
_directoryWatcher.Created -= OnExtensionDirectoryCreated;
_directoryWatcher.Deleted -= OnExtensionDirectoryDeleted;
_directoryWatcher.Dispose();
_directoryWatcher = null;
}
}
private void OnExtensionDirectoryCreated(object sender, FileSystemEventArgs e)
{
_ = Task.Run(async () =>
{
// Small delay to let files finish copying
await Task.Delay(500).ConfigureAwait(false);
var wrapper = await LoadExtensionFromDirectoryAsync(e.FullPath, CancellationToken.None).ConfigureAwait(false);
if (wrapper != null)
{
OnProviderAdded?.Invoke(this, [wrapper]);
}
});
}
private void OnExtensionDirectoryDeleted(object sender, FileSystemEventArgs e)
{
_ = Task.Run(async () =>
{
await RemoveExtensionByDirectoryAsync(e.FullPath).ConfigureAwait(false);
});
}
private Task RemoveExtensionByDirectoryAsync(string directoryPath)
{
JSExtensionWrapper? extensionToRemove = null;
CommandProviderWrapper? wrapperToRemove = null;
lock (_extensionsLock)
{
extensionToRemove = _extensions.FirstOrDefault(e => e.ManifestDirectory == directoryPath);
if (extensionToRemove != null)
{
_extensions.Remove(extensionToRemove);
wrapperToRemove = _providerWrappers.FirstOrDefault(w => w.Extension == extensionToRemove);
if (wrapperToRemove != null)
{
_providerWrappers.Remove(wrapperToRemove);
}
}
}
if (extensionToRemove != null)
{
StopSourceFileWatcher(directoryPath);
extensionToRemove.SignalDispose();
if (wrapperToRemove != null)
{
OnProviderRemoved?.Invoke(this, [wrapperToRemove]);
}
}
return Task.CompletedTask;
}
private void StartSourceFileWatcher(JSExtensionWrapper extensionWrapper, string extensionDirectory)
{
try
{
var watcher = new FileSystemWatcher(extensionDirectory, "*.js")
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName,
IncludeSubdirectories = true,
EnableRaisingEvents = true,
};
watcher.Changed += (sender, e) => OnSourceFileChanged(extensionWrapper, extensionDirectory, e);
watcher.Created += (sender, e) => OnSourceFileChanged(extensionWrapper, extensionDirectory, e);
lock (_sourceWatcherLock)
{
_sourceFileWatchers[extensionDirectory] = watcher;
}
Logger.LogDebug($"Started source file watcher for {extensionWrapper.ExtensionDisplayName} at {extensionDirectory}");
}
catch (Exception ex)
{
Logger.LogError($"Failed to start source file watcher for {extensionWrapper.ExtensionDisplayName}: {ex.Message}");
}
}
private void StopSourceFileWatcher(string extensionDirectory)
{
lock (_sourceWatcherLock)
{
if (_sourceFileWatchers.TryGetValue(extensionDirectory, out var watcher))
{
watcher.EnableRaisingEvents = false;
watcher.Dispose();
_sourceFileWatchers.Remove(extensionDirectory);
}
if (_debounceTimers.TryGetValue(extensionDirectory, out var timer))
{
timer.Dispose();
_debounceTimers.Remove(extensionDirectory);
}
}
}
private void StopAllSourceFileWatchers()
{
lock (_sourceWatcherLock)
{
foreach (var kvp in _sourceFileWatchers)
{
kvp.Value.EnableRaisingEvents = false;
kvp.Value.Dispose();
}
_sourceFileWatchers.Clear();
foreach (var kvp in _debounceTimers)
{
kvp.Value.Dispose();
}
_debounceTimers.Clear();
}
}
private void OnSourceFileChanged(JSExtensionWrapper extensionWrapper, string extensionDirectory, FileSystemEventArgs e)
{
// Skip node_modules changes
if (e.FullPath.Contains("node_modules", StringComparison.OrdinalIgnoreCase))
{
return;
}
lock (_sourceWatcherLock)
{
if (_debounceTimers.TryGetValue(extensionDirectory, out var existingTimer))
{
existingTimer.Dispose();
}
_debounceTimers[extensionDirectory] = new Timer(
_ => _ = Task.Run(() => RestartExtensionAsync(extensionWrapper)),
null,
500,
Timeout.Infinite);
}
}
private async Task RestartExtensionAsync(JSExtensionWrapper extensionWrapper)
{
if (!extensionWrapper.IsHealthy)
{
Logger.LogWarning($"Skipping hot-reload for unhealthy extension {extensionWrapper.ExtensionDisplayName}");
return;
}
Logger.LogInfo($"Hot-reload: restarting {extensionWrapper.ExtensionDisplayName}");
await extensionWrapper.RestartAsync().ConfigureAwait(false);
if (!extensionWrapper.IsRunning())
{
Logger.LogError($"Hot-reload failed: {extensionWrapper.ExtensionDisplayName} did not restart");
return;
}
Logger.LogInfo($"Hot-reload completed for {extensionWrapper.ExtensionDisplayName}");
}
private static string GetDefaultExtensionsPath()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(localAppData, "Microsoft", "PowerToys", "CommandPalette", "JSExtensions");
}
}

View File

@@ -0,0 +1,366 @@
// 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.
#pragma warning disable CA1848 // Use LoggerMessage delegates - this is a low-level protocol class using ILogger directly
#pragma warning disable CA1873 // Logging argument evaluation
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace Microsoft.CmdPal.UI.ViewModels.Services.JsonRpc;
/// <summary>
/// Manages JSON-RPC 2.0 communication with a Node.js child process over stdin/stdout using LSP-style length-prefixed framing.
/// </summary>
public sealed class JsonRpcConnection : IDisposable
{
private readonly Process _process;
private readonly ILogger _logger;
private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonRpcResponse>> _pendingRequests = new();
private readonly ConcurrentDictionary<string, Action<JsonElement>> _notificationHandlers = new();
private readonly SemaphoreSlim _writeLock = new(1, 1);
private readonly Lock _idLock = new();
private readonly CancellationTokenSource _disposalCts = new();
private readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(10);
private int _nextRequestId = 1;
private Task? _readLoopTask;
private bool _disposed;
/// <summary>
/// Raised when an error occurs during reading or processing messages.
/// </summary>
public event Action<Exception>? OnError;
/// <summary>
/// Raised when the connection is disconnected (process exit or stream close).
/// </summary>
public event Action? OnDisconnected;
/// <summary>
/// Initializes a new instance of the <see cref="JsonRpcConnection"/> class.
/// </summary>
/// <param name="process">The Node.js child process to communicate with.</param>
/// <param name="logger">Logger for diagnostics.</param>
public JsonRpcConnection(Process process, ILogger logger)
{
_process = process ?? throw new ArgumentNullException(nameof(process));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Starts listening for messages from the process stdout on a background thread.
/// </summary>
public void StartListening()
{
if (_readLoopTask != null)
{
throw new InvalidOperationException("Already listening");
}
_readLoopTask = Task.Run(ReadLoopAsync, _disposalCts.Token);
}
/// <summary>
/// Sends a JSON-RPC request and waits for the response.
/// </summary>
/// <param name="method">The method name to invoke.</param>
/// <param name="params">Optional parameters for the method.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The JSON-RPC response.</returns>
public async Task<JsonRpcResponse> SendRequestAsync(string method, JsonNode? @params, CancellationToken ct)
{
ObjectDisposedException.ThrowIf(_disposed, this);
int id;
lock (_idLock)
{
id = _nextRequestId++;
}
var request = new JsonRpcRequest
{
Id = id,
Method = method,
Params = @params,
};
var tcs = new TaskCompletionSource<JsonRpcResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
_pendingRequests[id] = tcs;
try
{
await SendMessageAsync(request, ct).ConfigureAwait(false);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct, _disposalCts.Token);
timeoutCts.CancelAfter(_defaultTimeout);
using (timeoutCts.Token.Register(() => tcs.TrySetCanceled()))
{
return await tcs.Task.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
_pendingRequests.TryRemove(id, out _);
throw;
}
}
/// <summary>
/// Sends a JSON-RPC notification (no response expected).
/// </summary>
/// <param name="method">The method name.</param>
/// <param name="params">Optional parameters.</param>
/// <param name="ct">Cancellation token.</param>
public async Task SendNotificationAsync(string method, JsonNode? @params, CancellationToken ct)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var notification = new JsonRpcNotification
{
Method = method,
Params = @params,
};
await SendMessageAsync(notification, ct).ConfigureAwait(false);
}
/// <summary>
/// Registers a handler for incoming notifications of a specific method.
/// </summary>
/// <param name="method">The notification method name.</param>
/// <param name="handler">The handler to invoke when the notification is received.</param>
public void RegisterNotificationHandler(string method, Action<JsonElement> handler)
{
_notificationHandlers[method] = handler;
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_disposalCts.Cancel();
try
{
_readLoopTask?.Wait(TimeSpan.FromSeconds(2));
}
catch
{
// Best effort
}
foreach (var kvp in _pendingRequests)
{
kvp.Value.TrySetCanceled();
}
_pendingRequests.Clear();
_writeLock.Dispose();
_disposalCts.Dispose();
}
private async Task SendMessageAsync(object message, CancellationToken ct)
{
var json = message switch
{
JsonRpcRequest req => JsonSerializer.Serialize(req, JsonRpcSerializerContext.Default.JsonRpcRequest),
JsonRpcNotification notif => JsonSerializer.Serialize(notif, JsonRpcSerializerContext.Default.JsonRpcNotification),
_ => throw new ArgumentException("Invalid message type", nameof(message)),
};
var contentBytes = Encoding.UTF8.GetBytes(json);
var header = $"Content-Length: {contentBytes.Length}\r\n\r\n";
var headerBytes = Encoding.ASCII.GetBytes(header);
await _writeLock.WaitAsync(ct).ConfigureAwait(false);
try
{
var stdin = _process.StandardInput.BaseStream;
await stdin.WriteAsync(headerBytes, ct).ConfigureAwait(false);
await stdin.WriteAsync(contentBytes, ct).ConfigureAwait(false);
await stdin.FlushAsync(ct).ConfigureAwait(false);
_logger.LogDebug("Sent {MessageType}: {Json}", message.GetType().Name, json);
}
finally
{
_writeLock.Release();
}
}
private async Task ReadLoopAsync()
{
try
{
var stdout = _process.StandardOutput.BaseStream;
var headerBuffer = new byte[1024];
var contentBuffer = new byte[65536];
while (!_disposalCts.Token.IsCancellationRequested && !_process.HasExited)
{
var contentLength = await ReadContentLengthAsync(stdout, headerBuffer, _disposalCts.Token).ConfigureAwait(false);
if (contentLength <= 0)
{
_logger.LogWarning("Read loop: contentLength={ContentLength}, exiting loop", contentLength);
break;
}
// Ensure buffer is large enough
if (contentLength > contentBuffer.Length)
{
contentBuffer = new byte[contentLength];
}
// Read exactly contentLength bytes
var totalRead = 0;
while (totalRead < contentLength)
{
var read = await stdout.ReadAsync(contentBuffer.AsMemory(totalRead, contentLength - totalRead), _disposalCts.Token).ConfigureAwait(false);
if (read == 0)
{
_logger.LogWarning("Stream closed before reading full message");
return;
}
totalRead += read;
}
var json = Encoding.UTF8.GetString(contentBuffer, 0, contentLength);
_logger.LogDebug("Received message: {Json}", json);
ProcessMessage(json);
}
}
catch (OperationCanceledException)
{
// Expected during disposal
_logger.LogInformation("Read loop exiting due to cancellation");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in read loop");
OnError?.Invoke(ex);
}
finally
{
_logger.LogInformation("Read loop ended");
OnDisconnected?.Invoke();
}
}
private async Task<int> ReadContentLengthAsync(Stream stream, byte[] buffer, CancellationToken ct)
{
var position = 0;
var headerComplete = false;
while (!headerComplete && position < buffer.Length)
{
var read = await stream.ReadAsync(buffer.AsMemory(position, 1), ct).ConfigureAwait(false);
if (read == 0)
{
return -1; // Stream closed
}
position++;
// Check for \r\n\r\n (end of headers)
if (position >= 4 &&
buffer[position - 4] == '\r' &&
buffer[position - 3] == '\n' &&
buffer[position - 2] == '\r' &&
buffer[position - 1] == '\n')
{
headerComplete = true;
}
}
if (!headerComplete)
{
_logger.LogError("Header too large or malformed");
return -1;
}
var headerText = Encoding.ASCII.GetString(buffer, 0, position);
var lines = headerText.Split(["\r\n"], StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
{
var valueStr = line.Substring(15).Trim();
if (int.TryParse(valueStr, out var length))
{
return length;
}
}
}
_logger.LogError("Content-Length header not found");
return -1;
}
private void ProcessMessage(string json)
{
try
{
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.TryGetProperty("id", out var idProp) && idProp.ValueKind == JsonValueKind.Number)
{
// This is a response
var id = idProp.GetInt32();
var response = JsonSerializer.Deserialize(json, JsonRpcSerializerContext.Default.JsonRpcResponse);
if (response != null && _pendingRequests.TryRemove(id, out var tcs))
{
tcs.SetResult(response);
}
else
{
_logger.LogWarning("Received response for unknown request ID: {Id}", id);
}
}
else if (root.TryGetProperty("method", out var methodProp))
{
// This is a notification
var method = methodProp.GetString() ?? string.Empty;
if (_notificationHandlers.TryGetValue(method, out var handler))
{
var paramsElement = root.TryGetProperty("params", out var p) ? p : default;
handler(paramsElement);
}
else
{
_logger.LogDebug("No handler registered for notification: {Method}", method);
}
}
else
{
_logger.LogWarning("Received message with neither id nor method");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing message: {Json}", json);
OnError?.Invoke(ex);
}
}
}

View File

@@ -0,0 +1,102 @@
// 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.
#pragma warning disable SA1402 // File may only contain a single type - closely related message types grouped together
#pragma warning disable SA1649 // File name should match first type name
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.UI.ViewModels.Services.JsonRpc;
/// <summary>
/// Represents a JSON-RPC 2.0 request message.
/// </summary>
public sealed class JsonRpcRequest
{
[JsonPropertyName("jsonrpc")]
public string JsonRpc { get; set; } = "2.0";
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("method")]
public string Method { get; set; } = string.Empty;
[JsonPropertyName("params")]
public JsonNode? Params { get; set; }
}
/// <summary>
/// Represents a JSON-RPC 2.0 response message.
/// </summary>
public sealed class JsonRpcResponse
{
[JsonPropertyName("jsonrpc")]
public string JsonRpc { get; set; } = "2.0";
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("result")]
public JsonElement? Result { get; set; }
[JsonPropertyName("error")]
public JsonRpcError? Error { get; set; }
}
/// <summary>
/// Represents a JSON-RPC 2.0 notification message (no id, no response expected).
/// </summary>
public sealed class JsonRpcNotification
{
[JsonPropertyName("jsonrpc")]
public string JsonRpc { get; set; } = "2.0";
[JsonPropertyName("method")]
public string Method { get; set; } = string.Empty;
[JsonPropertyName("params")]
public JsonNode? Params { get; set; }
}
/// <summary>
/// Represents a JSON-RPC 2.0 error object.
/// </summary>
public sealed class JsonRpcError
{
[JsonPropertyName("code")]
public int Code { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
[JsonPropertyName("data")]
public object? Data { get; set; }
// Standard JSON-RPC 2.0 error codes
public const int ParseError = -32700;
public const int InvalidRequest = -32600;
public const int MethodNotFound = -32601;
public const int InvalidParams = -32602;
public const int InternalError = -32603;
}
/// <summary>
/// JSON source generation context for AOT-safe serialization.
/// </summary>
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(JsonRpcRequest))]
[JsonSerializable(typeof(JsonRpcResponse))]
[JsonSerializable(typeof(JsonRpcNotification))]
[JsonSerializable(typeof(JsonRpcError))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(JsonNode))]
[JsonSerializable(typeof(JsonObject))]
internal partial class JsonRpcSerializerContext : JsonSerializerContext
{
}

View File

@@ -2,28 +2,34 @@
// 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.Immutable;
using System.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
public partial class ExtensionService : IExtensionService, IDisposable
/// <summary>
/// Extension service that manages out-of-process WinRT AppExtension-based command providers.
/// Handles package catalog monitoring, extension startup with timeouts, and background retries.
/// </summary>
public partial class WinRTExtensionService : IExtensionService, IDisposable
{
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionRemoved;
private static readonly TimeSpan ExtensionStartTimeout = TimeSpan.FromSeconds(10);
private static readonly TimeSpan BackgroundStartTimeout = TimeSpan.FromSeconds(60);
private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser();
private static readonly Lock _lock = new();
private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1);
private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1);
private readonly TaskScheduler _taskScheduler;
private readonly ICommandProviderCache _commandProviderCache;
// private readonly ILocalSettingsService _localSettingsService;
private bool _disposedValue;
private const string CreateInstanceProperty = "CreateInstance";
@@ -32,17 +38,119 @@ public partial class ExtensionService : IExtensionService, IDisposable
private static readonly List<IExtensionWrapper> _installedExtensions = [];
private static readonly List<IExtensionWrapper> _enabledExtensions = [];
public ExtensionService()
public event TypedEventHandler<IExtensionService, IEnumerable<CommandProviderWrapper>>? OnProviderAdded;
public event TypedEventHandler<IExtensionService, IEnumerable<CommandProviderWrapper>>? OnProviderRemoved;
public WinRTExtensionService(TaskScheduler taskScheduler, ICommandProviderCache commandProviderCache)
{
_taskScheduler = taskScheduler;
_commandProviderCache = commandProviderCache;
_catalog.PackageInstalling += Catalog_PackageInstalling;
_catalog.PackageUninstalling += Catalog_PackageUninstalling;
_catalog.PackageUpdating += Catalog_PackageUpdating;
}
//// These two were an investigation into getting updates when a package
//// gets redeployed from VS. Neither get raised (nor do the above)
//// _catalog.PackageStatusChanged += Catalog_PackageStatusChanged;
//// _catalog.PackageStaging += Catalog_PackageStaging;
// _localSettingsService = settingsService;
public async Task<IEnumerable<CommandProviderWrapper>> LoadProvidersAsync(CancellationToken ct)
{
var extensions = (await GetInstalledExtensionsAsync().ConfigureAwait(false)).ToImmutableList();
var timer = Stopwatch.StartNew();
// Start all extensions in parallel
var startResults = await Task.WhenAll(extensions.Select(ext => TryStartExtensionAsync(ext, ct))).ConfigureAwait(false);
var startedWrappers = new List<CommandProviderWrapper>();
foreach (var r in startResults)
{
if (r.IsStarted)
{
startedWrappers.Add(r.Wrapper);
}
else if (r.IsTimedOut)
{
_ = StartExtensionWhenReadyAsync(r.Extension, r.PendingStartTask, r.Stopwatch, ct);
}
}
timer.Stop();
Logger.LogInfo($"WinRTExtensionService: Started {startedWrappers.Count} extension(s) in {timer.ElapsedMilliseconds} ms");
return startedWrappers;
}
public async Task SignalStopAsync()
{
var installedExtensions = await GetInstalledExtensionsAsync().ConfigureAwait(false);
foreach (var installedExtension in installedExtensions)
{
Logger.LogDebug($"Signaling dispose to {installedExtension.ExtensionUniqueId}");
try
{
if (installedExtension.IsRunning())
{
installedExtension.SignalDispose();
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to send dispose signal to extension {installedExtension.ExtensionUniqueId}", ex);
}
}
}
private async Task<ExtensionStartResult> TryStartExtensionAsync(IExtensionWrapper extension, CancellationToken ct)
{
Logger.LogDebug($"Starting {extension.PackageFullName}");
var sw = Stopwatch.StartNew();
var startTask = extension.StartExtensionAsync();
try
{
await startTask.WaitAsync(ExtensionStartTimeout, ct).ConfigureAwait(false);
Logger.LogInfo($"Started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms");
return ExtensionStartResult.Started(extension, new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache));
}
catch (TimeoutException)
{
Logger.LogWarning($"Starting extension {extension.PackageFullName} timed out after {sw.ElapsedMilliseconds} ms, continuing in background");
return ExtensionStartResult.TimedOut(extension, startTask, sw);
}
catch (OperationCanceledException)
{
Logger.LogDebug($"Starting extension {extension.PackageFullName} was cancelled after {sw.ElapsedMilliseconds} ms");
return ExtensionStartResult.Failed(extension);
}
catch (Exception ex)
{
Logger.LogError($"Failed to start extension {extension.PackageFullName} after {sw.ElapsedMilliseconds} ms: {ex}");
return ExtensionStartResult.Failed(extension);
}
}
private async Task StartExtensionWhenReadyAsync(
IExtensionWrapper extension,
Task startTask,
Stopwatch sw,
CancellationToken ct)
{
try
{
await startTask.WaitAsync(BackgroundStartTimeout, ct).ConfigureAwait(false);
var wrapper = new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache);
Logger.LogInfo($"Late-started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms");
OnProviderAdded?.Invoke(this, [wrapper]);
}
catch (OperationCanceledException)
{
// Reload happened -- discard stale results
}
catch (Exception ex)
{
Logger.LogError($"Background start of extension {extension.PackageFullName} failed after {sw.ElapsedMilliseconds} ms: {ex}");
}
}
private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args)
@@ -73,10 +181,7 @@ public partial class ExtensionService : IExtensionService, IDisposable
{
lock (_lock)
{
// Get any extension providers that we previously had from this app
UninstallPackageUnderLock(args.TargetPackage);
// then add the new ones.
InstallPackageUnderLock(args.TargetPackage);
}
}
@@ -103,7 +208,25 @@ public partial class ExtensionService : IExtensionService, IDisposable
UpdateExtensionsListsFromWrappers(wrappers);
OnExtensionAdded?.Invoke(this, wrappers);
// Start extensions and notify via OnProviderAdded
var startedProviders = new List<CommandProviderWrapper>();
foreach (var wrapper in wrappers)
{
try
{
await wrapper.StartExtensionAsync().ConfigureAwait(false);
startedProviders.Add(new CommandProviderWrapper(wrapper, _taskScheduler, _commandProviderCache));
}
catch (Exception ex)
{
Logger.LogError($"Failed to start newly installed extension {wrapper.ExtensionUniqueId}: {ex}");
}
}
if (startedProviders.Count > 0)
{
OnProviderAdded?.Invoke(this, startedProviders);
}
}
finally
{
@@ -121,7 +244,6 @@ public partial class ExtensionService : IExtensionService, IDisposable
if (extension.PackageFullName == package.Id.FullName)
{
CommandPaletteHost.Instance.DebugLog($"Uninstalled extension app {extension.PackageDisplayName}");
removedExtensions.Add(extension);
}
}
@@ -134,7 +256,24 @@ public partial class ExtensionService : IExtensionService, IDisposable
_installedExtensions.RemoveAll(i => removedExtensions.Contains(i));
_enabledExtensions.RemoveAll(i => removedExtensions.Contains(i));
OnExtensionRemoved?.Invoke(this, removedExtensions);
// Build placeholder wrappers for removal notification.
// The TopLevelCommandManager matches by Extension reference, so we need to pass
// something that carries the IExtensionWrapper identity.
var removedProviders = new List<CommandProviderWrapper>();
foreach (var ext in removedExtensions)
{
try
{
removedProviders.Add(new CommandProviderWrapper(ext, _taskScheduler, _commandProviderCache));
}
catch
{
// Extension may not be in a runnable state if it was uninstalled;
// we still need to signal removal
}
}
OnProviderRemoved?.Invoke(this, removedProviders);
}
finally
{
@@ -143,52 +282,6 @@ public partial class ExtensionService : IExtensionService, IDisposable
});
}
private static async Task<IsExtensionResult> IsValidCmdPalExtension(Package package)
{
var extensions = await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
foreach (var extension in extensions)
{
if (package.Id?.FullName == extension.Package?.Id?.FullName)
{
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
return new(cmdPalProvider is not null && classId.Count != 0, extension);
}
}
return new(false, null);
}
private static async Task<(IPropertySet? CmdPalProvider, List<string> ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension)
{
var classIds = new List<string>();
var properties = await extension.GetExtensionPropertiesAsync();
if (properties is null)
{
return (null, classIds);
}
var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
if (cmdPalProvider is null)
{
return (null, classIds);
}
var activation = GetSubPropertySet(cmdPalProvider, "Activation");
if (activation is null)
{
return (cmdPalProvider, classIds);
}
// Handle case where extension creates multiple instances.
classIds.AddRange(GetCreateInstanceList(activation));
return (cmdPalProvider, classIds);
}
private static async Task<IEnumerable<AppExtension>> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
{
await _getInstalledExtensionsLock.WaitAsync();
@@ -215,25 +308,22 @@ public partial class ExtensionService : IExtensionService, IDisposable
}
}
private static void UpdateExtensionsListsFromWrappers(List<ExtensionWrapper> wrappers)
public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
{
foreach (var extensionWrapper in wrappers)
{
// var localSettingsService = Application.Current.GetService<ILocalSettingsService>();
var extensionUniqueId = extensionWrapper.ExtensionUniqueId;
var isExtensionDisabled = false; // await localSettingsService.ReadSettingAsync<bool>(extensionUniqueId + "-ExtensionDisabled");
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
return extension.FirstOrDefault();
}
_installedExtensions.Add(extensionWrapper);
if (!isExtensionDisabled)
{
_enabledExtensions.Add(extensionWrapper);
}
public void EnableExtension(string extensionUniqueId)
{
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
_enabledExtensions.Add(extension.First());
}
// TelemetryFactory.Get<ITelemetry>().Log(
// "Extension_ReportInstalled",
// LogLevel.Critical,
// new ReportInstalledExtensionEvent(extensionUniqueId, isEnabled: !isExtensionDisabled));
}
public void DisableExtension(string extensionUniqueId)
{
var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
_enabledExtensions.Remove(extension.First());
}
private static async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsyncUnderLock(bool includeDisabledExtensions, bool refresh)
@@ -295,6 +385,8 @@ public partial class ExtensionService : IExtensionService, IDisposable
}
}
private static async Task<IEnumerable<AppExtension>> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
private static async Task<List<ExtensionWrapper>> CreateWrappersForExtension(AppExtension extension)
{
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
@@ -337,7 +429,6 @@ public partial class ExtensionService : IExtensionService, IDisposable
}
else
{
// log warning that extension declared unsupported extension interface
CommandPaletteHost.Instance.DebugLog($"Extension {extension.DisplayName} declared an unsupported interface: {supportedInterface.Key}");
}
}
@@ -346,77 +437,68 @@ public partial class ExtensionService : IExtensionService, IDisposable
return extensionWrapper;
}
public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
private static void UpdateExtensionsListsFromWrappers(List<ExtensionWrapper> wrappers)
{
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
return extension.FirstOrDefault();
}
public async Task SignalStopExtensionsAsync()
{
var installedExtensions = await GetInstalledExtensionsAsync();
foreach (var installedExtension in installedExtensions)
foreach (var extensionWrapper in wrappers)
{
Logger.LogDebug($"Signaling dispose to {installedExtension.ExtensionUniqueId}");
try
var extensionUniqueId = extensionWrapper.ExtensionUniqueId;
var isExtensionDisabled = false;
_installedExtensions.Add(extensionWrapper);
if (!isExtensionDisabled)
{
if (installedExtension.IsRunning())
{
installedExtension.SignalDispose();
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to send dispose signal to extension {installedExtension.ExtensionUniqueId}", ex);
_enabledExtensions.Add(extensionWrapper);
}
}
}
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false)
private static async Task<IsExtensionResult> IsValidCmdPalExtension(Package package)
{
var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions);
List<IExtensionWrapper> filteredExtensions = [];
foreach (var installedExtension in installedExtensions)
var extensions = await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
foreach (var extension in extensions)
{
if (installedExtension.HasProviderType(providerType))
if (package.Id?.FullName == extension.Package?.Id?.FullName)
{
filteredExtensions.Add(installedExtension);
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
return new(cmdPalProvider is not null && classId.Count != 0, extension);
}
}
return filteredExtensions;
return new(false, null);
}
public void Dispose()
private static async Task<(IPropertySet? CmdPalProvider, List<string> ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension)
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
var classIds = new List<string>();
var properties = await extension.GetExtensionPropertiesAsync();
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
if (properties is null)
{
if (disposing)
{
_getInstalledExtensionsLock.Dispose();
_getInstalledWidgetsLock.Dispose();
}
_disposedValue = true;
return (null, classIds);
}
var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
if (cmdPalProvider is null)
{
return (null, classIds);
}
var activation = GetSubPropertySet(cmdPalProvider, "Activation");
if (activation is null)
{
return (cmdPalProvider, classIds);
}
classIds.AddRange(GetCreateInstanceList(activation));
return (cmdPalProvider, classIds);
}
private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as IPropertySet : null;
private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as object[] : null;
/// <summary>
/// There are cases where the extension creates multiple COM instances.
/// </summary>
/// <param name="activationPropSet">Activation property set object</param>
/// <returns>List of ClassId strings associated with the activation property</returns>
private static List<string> GetCreateInstanceList(IPropertySet activationPropSet)
{
var propSetList = new List<string>();
@@ -424,8 +506,6 @@ public partial class ExtensionService : IExtensionService, IDisposable
if (singlePropertySet is not null)
{
var classId = GetProperty(singlePropertySet, ClassIdProperty);
// If the instance has a classId as a single string, then it's only supporting a single instance.
if (classId is not null)
{
propSetList.Add(classId);
@@ -457,35 +537,24 @@ public partial class ExtensionService : IExtensionService, IDisposable
private static string? GetProperty(IPropertySet propSet, string name) => propSet[name] as string;
public void EnableExtension(string extensionUniqueId)
public void Dispose()
{
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
_enabledExtensions.Add(extension.First());
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public void DisableExtension(string extensionUniqueId)
private void Dispose(bool disposing)
{
var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
_enabledExtensions.Remove(extension.First());
}
if (!_disposedValue)
{
if (disposing)
{
_getInstalledExtensionsLock.Dispose();
}
/*
///// <inheritdoc cref="IExtensionService.DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper)"/>
//public async Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension)
//{
// // Only attempt to disable feature if its available.
// if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId))
// {
// return false;
// }
// _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown");
// // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension
// // for the rest of its process lifetime.
// DisableExtension(extension.ExtensionUniqueId);
// // Update the local settings so the next time the user launches Dev Home the extension will be disabled.
// await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true);
// return true;
//} */
_disposedValue = true;
}
}
}
internal record struct IsExtensionResult(bool IsExtension, AppExtension? Extension)

View File

@@ -489,6 +489,34 @@ public partial class ShellViewModel : ObservableObject,
UnsafeHandleCommandResult(a.Result, onBeforeShowConfirmation);
}
break;
}
case CommandResultKind.GoToPage:
{
if (result is JSGoToPageCommandResult jsGoToPage && jsGoToPage.Page != null)
{
// Handle NavigationMode before navigating to the target page
if (jsGoToPage.Args is IGoToPageArgs goToPageArgs)
{
switch (goToPageArgs.NavigationMode)
{
case NavigationMode.GoBack:
GoBack();
break;
case NavigationMode.GoHome:
GoHome(withAnimation: false, focusSearch: false);
break;
case NavigationMode.Push:
default:
break;
}
}
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(
new(new ExtensionObject<ICommand>(jsGoToPage.Page)));
}
break;
}
}

View File

@@ -2,7 +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.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -11,7 +10,6 @@ using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
@@ -22,22 +20,20 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class TopLevelCommandManager : ObservableObject,
IRecipient<ReloadCommandsMessage>,
IRecipient<ProviderEnabledStateChangedMessage>,
IRecipient<PinCommandItemMessage>,
IRecipient<UnpinCommandItemMessage>,
IRecipient<PinToDockMessage>,
IDisposable
{
private static readonly TimeSpan ExtensionStartTimeout = TimeSpan.FromSeconds(10);
private static readonly TimeSpan CommandLoadTimeout = TimeSpan.FromSeconds(10);
private static readonly TimeSpan BackgroundStartTimeout = TimeSpan.FromSeconds(60);
private static readonly TimeSpan BackgroundCommandLoadTimeout = TimeSpan.FromSeconds(60);
private readonly IServiceProvider _serviceProvider;
private readonly ICommandProviderCache _commandProviderCache;
private readonly IEnumerable<IExtensionService> _extensionServices;
private readonly TaskScheduler _taskScheduler;
private readonly List<CommandProviderWrapper> _builtInCommands = [];
private readonly List<CommandProviderWrapper> _extensionCommandProviders = [];
private readonly List<CommandProviderWrapper> _commandProviders = [];
private readonly Lock _commandProvidersLock = new();
// watch out: if you add code that locks CommandProviders, be sure to always
@@ -50,18 +46,25 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
private HashSet<(string ProviderId, string CommandId)> _pinnedCommandSet = [];
public TopLevelCommandManager(IServiceProvider serviceProvider, ICommandProviderCache commandProviderCache)
public TopLevelCommandManager(IServiceProvider serviceProvider, IEnumerable<IExtensionService> extensionServices)
{
_serviceProvider = serviceProvider;
_commandProviderCache = commandProviderCache;
_extensionServices = extensionServices;
_currentExtensionLoadCancellationToken = _extensionLoadCts.Token;
_taskScheduler = _serviceProvider.GetService<TaskScheduler>()!;
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
WeakReferenceMessenger.Default.Register<ProviderEnabledStateChangedMessage>(this);
WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(this);
WeakReferenceMessenger.Default.Register<UnpinCommandItemMessage>(this);
WeakReferenceMessenger.Default.Register<PinToDockMessage>(this);
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
RebuildPinnedCache();
foreach (var service in _extensionServices)
{
service.OnProviderAdded += ExtensionService_OnProviderAdded;
service.OnProviderRemoved += ExtensionService_OnProviderRemoved;
}
}
public ObservableCollection<PinnedCommandSettings> PinnedCommands { get; } = [];
@@ -84,7 +87,7 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
{
lock (_commandProvidersLock)
{
return _builtInCommands.Concat(_extensionCommandProviders).ToList();
return _commandProviders.ToList();
}
}
}
@@ -101,58 +104,6 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
ListHelpers.InPlaceUpdateList(PinnedCommands, settings.PinnedCommands);
}
public async Task<bool> LoadBuiltinsAsync()
{
var s = new Stopwatch();
s.Start();
lock (_commandProvidersLock)
{
_builtInCommands.Clear();
}
// Load built-In commands first. These are all in-proc, and
// owned by our ServiceProvider.
var builtInCommands = _serviceProvider.GetServices<ICommandProvider>();
foreach (var provider in builtInCommands)
{
CommandProviderWrapper wrapper = new(provider, _taskScheduler);
lock (_commandProvidersLock)
{
_builtInCommands.Add(wrapper);
}
var objects = await LoadTopLevelCommandsFromProvider(wrapper);
lock (TopLevelCommands)
{
if (objects.Commands is IEnumerable<TopLevelViewModel> commands)
{
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
}
lock (_dockBandsLock)
{
if (objects.DockBands is IEnumerable<TopLevelViewModel> bands)
{
foreach (var c in bands)
{
DockBands.Add(c);
}
}
}
}
s.Stop();
Logger.LogDebug($"Loading built-ins took {s.ElapsedMilliseconds}ms");
return true;
}
// May be called from a background thread
private async Task<TopLevelObjectSets> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
@@ -288,125 +239,197 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
{
// gate ensures that the reload is serialized and if multiple calls
// request a reload, only the first and the last one will be executed.
// this should be superseded with a cancellable version.
await _reloadCommandsGate.ExecuteAsync(CancellationToken.None);
}
/// <summary>
/// Loads only built-in (in-process) command providers. This is fast and
/// suitable for the initial pre-load phase so the UI appears immediately.
/// </summary>
public async Task LoadBuiltInProvidersAsync()
{
var ct = _currentExtensionLoadCancellationToken;
foreach (var service in _extensionServices.OfType<BuiltInExtensionService>())
{
var wrappers = await service.LoadProvidersAsync(ct).ConfigureAwait(false);
await RegisterAndLoadCommandsAsync(wrappers, ct).ConfigureAwait(false);
}
}
/// <summary>
/// Loads external (out-of-process WinRT) command providers. Call this after
/// the root page is displayed so the UI is not blocked by extension startup.
/// Commands appear progressively via <see cref="IExtensionService.OnProviderAdded"/>.
/// </summary>
[RelayCommand]
public async Task LoadExternalProvidersAsync()
{
IsLoading = true;
try
{
var ct = _currentExtensionLoadCancellationToken;
foreach (var service in _extensionServices.Where(s => s is not BuiltInExtensionService))
{
var wrappers = await service.LoadProvidersAsync(ct).ConfigureAwait(false);
await RegisterAndLoadCommandsAsync(wrappers, ct).ConfigureAwait(false);
}
}
finally
{
IsLoading = false;
}
}
private async Task ReloadAllCommandsAsyncCore(CancellationToken cancellationToken)
{
IsLoading = true;
// Invalidate any background continuations from the previous load cycle
await _extensionLoadCts.CancelAsync().ConfigureAwait(false);
_extensionLoadCts.Dispose();
_extensionLoadCts = new();
_currentExtensionLoadCancellationToken = _extensionLoadCts.Token;
var extensionService = _serviceProvider.GetService<IExtensionService>()!;
await extensionService.SignalStopExtensionsAsync().ConfigureAwait(false);
lock (TopLevelCommands)
try
{
TopLevelCommands.Clear();
}
// Invalidate any background continuations from the previous load cycle
await _extensionLoadCts.CancelAsync().ConfigureAwait(false);
_extensionLoadCts.Dispose();
_extensionLoadCts = new();
_currentExtensionLoadCancellationToken = _extensionLoadCts.Token;
lock (_dockBandsLock)
{
DockBands.Clear();
}
await LoadBuiltinsAsync().ConfigureAwait(false);
_ = Task.Run(LoadExtensionsAsync, cancellationToken);
}
// Load commands from our extensions. Called on a background thread.
// Currently, this
// * queries the package catalog,
// * starts all the extensions,
// * then fetches the top-level commands from them.
// TODO In the future, we'll probably abstract some of this away, to have
// separate extension tracking vs stub loading.
[RelayCommand]
public async Task<bool> LoadExtensionsAsync()
{
var extensionService = _serviceProvider.GetService<IExtensionService>()!;
extensionService.OnExtensionAdded -= ExtensionService_OnExtensionAdded;
extensionService.OnExtensionRemoved -= ExtensionService_OnExtensionRemoved;
var ct = _currentExtensionLoadCancellationToken;
var extensions = (await extensionService.GetInstalledExtensionsAsync().ConfigureAwait(false)).ToImmutableList();
lock (_commandProvidersLock)
{
_extensionCommandProviders.Clear();
}
await StartExtensionsAndGetCommands(extensions, ct).ConfigureAwait(false);
extensionService.OnExtensionAdded += ExtensionService_OnExtensionAdded;
extensionService.OnExtensionRemoved += ExtensionService_OnExtensionRemoved;
IsLoading = false;
// Send on the current thread; receivers should marshal to UI if needed
WeakReferenceMessenger.Default.Send<ReloadFinishedMessage>();
return true;
}
private void ExtensionService_OnExtensionAdded(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)
{
var ct = _currentExtensionLoadCancellationToken;
// When we get an extension install event, hop off to a BG thread
_ = Task.Run(
async () =>
// Signal all services to stop their running providers
foreach (var service in _extensionServices)
{
// for each newly installed extension, start it and get commands
// from it. One single package might have more than one
// IExtensionWrapper in it.
await StartExtensionsAndGetCommands(extensions, ct).ConfigureAwait(false);
},
ct);
}
private async Task StartExtensionsAndGetCommands(IEnumerable<IExtensionWrapper> extensions, CancellationToken ct)
{
var timer = Stopwatch.StartNew();
// Start all extensions in parallel
var startResults = await Task.WhenAll(extensions.Select(TryStartExtensionAsync)).ConfigureAwait(false);
var startedWrappers = new List<CommandProviderWrapper>();
foreach (var r in startResults)
{
if (r.IsStarted)
{
startedWrappers.Add(r.Wrapper);
await service.SignalStopAsync().ConfigureAwait(false);
}
else if (r.IsTimedOut)
lock (TopLevelCommands)
{
_ = StartExtensionWhenReadyAsync(r.Extension, r.PendingStartTask, r.Stopwatch, ct);
TopLevelCommands.Clear();
}
lock (_dockBandsLock)
{
DockBands.Clear();
}
lock (_commandProvidersLock)
{
_commandProviders.Clear();
}
var ct = _currentExtensionLoadCancellationToken;
// Load providers from each service sequentially (order matters: built-ins first)
foreach (var service in _extensionServices)
{
var wrappers = await service.LoadProvidersAsync(ct).ConfigureAwait(false);
await RegisterAndLoadCommandsAsync(wrappers, ct).ConfigureAwait(false);
}
}
// Register started extensions and load their commands
var loadSummary = await RegisterAndLoadCommandsAsync(startedWrappers, ct).ConfigureAwait(false);
timer.Stop();
Logger.LogInfo($"Loaded {loadSummary.CommandCount} command(s) and {loadSummary.DockBandCount} band(s) from {startedWrappers.Count} extension(s) in {timer.ElapsedMilliseconds} ms");
finally
{
IsLoading = false;
WeakReferenceMessenger.Default.Send<ReloadFinishedMessage>();
}
}
private async Task<RegisterAndLoadSummary> RegisterAndLoadCommandsAsync(ICollection<CommandProviderWrapper> wrappers, CancellationToken ct)
private async Task UpdateProviderEnabledStateAsyncCore(string providerId, bool isEnabled)
{
IsLoading = true;
try
{
// If disabled, we'll remove that providers commands from top level commands, dock bands, and pinned commands.
if (!isEnabled)
{
lock (TopLevelCommands)
{
var commandsToRemove = TopLevelCommands.Where(c => c.CommandProviderId == providerId).ToList();
foreach (var command in commandsToRemove)
{
TopLevelCommands.Remove(command);
}
}
lock (_dockBandsLock)
{
var dockBandsToRemove = DockBands.Where(b => b.CommandProviderId == providerId).ToList();
foreach (var band in dockBandsToRemove)
{
DockBands.Remove(band);
}
}
lock (PinnedCommands)
{
var pinnedToRemove = PinnedCommands.Where(p => p.ProviderId == providerId).ToList();
foreach (var command in pinnedToRemove)
{
PinnedCommands.Remove(command);
}
}
}
else
{
CommandProviderWrapper? provider;
lock (_commandProvidersLock)
{
provider = _commandProviders.FirstOrDefault(p => p.ProviderId == providerId);
}
if (provider != null)
{
await provider.LoadTopLevelCommands(_serviceProvider);
lock (TopLevelCommands)
{
foreach (var command in provider.TopLevelItems)
{
if (!TopLevelCommands.Any(a => a.Id == command.Id))
{
TopLevelCommands.Add(command);
}
}
foreach (var item in provider.FallbackItems)
{
if (!TopLevelCommands.Any(a => a.Id == item.Id) && item.IsEnabled)
{
TopLevelCommands.Add(item);
}
}
}
lock (_dockBandsLock)
{
foreach (var band in provider.DockBandItems)
{
if (!DockBands.Any(a => a.Id == band.Id))
{
DockBands.Add(band);
}
}
}
}
else
{
Logger.LogWarning($"Could not find provider with id '{providerId}' to update enabled state.");
return;
}
}
}
finally
{
IsLoading = false;
}
}
private async Task<RegisterAndLoadSummary> RegisterAndLoadCommandsAsync(IEnumerable<CommandProviderWrapper> wrappers, CancellationToken ct)
{
var wrapperList = wrappers.ToList();
lock (_commandProvidersLock)
{
_extensionCommandProviders.AddRange(wrappers);
_commandProviders.AddRange(wrapperList);
}
// Load the commands from the providers in parallel
var loadResults = await Task.WhenAll(wrappers.Select(w => TryLoadCommandsAsync(w, ct))).ConfigureAwait(false);
var loadResults = await Task.WhenAll(wrapperList.Select(w => TryLoadCommandsAsync(w, ct))).ConfigureAwait(false);
var totalCommands = 0;
var totalDockBands = 0;
@@ -463,7 +486,6 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
// Fire background continuations for timed-out loads outside the lock
foreach (var r in timedOut)
{
// It's weird to repeat the condition here, but it allows the compiler to track nullability of other properties
if (r.IsTimedOut)
{
_ = AppendCommandsWhenReadyAsync(r.Wrapper, r.PendingLoadTask, r.Stopwatch, ct);
@@ -473,60 +495,6 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
return new RegisterAndLoadSummary(totalCommands, totalDockBands);
}
private async Task<ExtensionStartResult> TryStartExtensionAsync(IExtensionWrapper extension)
{
Logger.LogDebug($"Starting {extension.PackageFullName}");
var sw = Stopwatch.StartNew();
var ct = _currentExtensionLoadCancellationToken;
var startTask = extension.StartExtensionAsync();
try
{
await startTask.WaitAsync(ExtensionStartTimeout, ct).ConfigureAwait(false);
Logger.LogInfo($"Started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms");
return ExtensionStartResult.Started(extension, new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache));
}
catch (TimeoutException)
{
Logger.LogWarning($"Starting extension {extension.PackageFullName} timed out after {sw.ElapsedMilliseconds} ms, continuing in background");
return ExtensionStartResult.TimedOut(extension, startTask, sw);
}
catch (OperationCanceledException)
{
Logger.LogDebug($"Starting extension {extension.PackageFullName} was cancelled after {sw.ElapsedMilliseconds} ms");
return ExtensionStartResult.Failed(extension);
}
catch (Exception ex)
{
Logger.LogError($"Failed to start extension {extension.PackageFullName} after {sw.ElapsedMilliseconds} ms: {ex}");
return ExtensionStartResult.Failed(extension);
}
}
private async Task StartExtensionWhenReadyAsync(
IExtensionWrapper extension,
Task startTask,
Stopwatch sw,
CancellationToken ct)
{
try
{
await startTask.WaitAsync(BackgroundStartTimeout, ct).ConfigureAwait(false);
var wrapper = new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache);
Logger.LogInfo($"Late-started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms, loading commands and bands");
await RegisterAndLoadCommandsAsync([wrapper], ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Reload happened -- discard stale results
}
catch (Exception ex)
{
Logger.LogError($"Background start/load of extension {extension.PackageFullName} failed after {sw.ElapsedMilliseconds} ms: {ex}");
}
}
private async Task<CommandLoadResult> TryLoadCommandsAsync(CommandProviderWrapper wrapper, CancellationToken ct)
{
var sw = Stopwatch.StartNew();
@@ -536,22 +504,22 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
var result = await loadTask.WaitAsync(CommandLoadTimeout, ct).ConfigureAwait(false);
var commandCount = result.Commands?.Count ?? 0;
var dockBandCount = result.DockBands?.Count ?? 0;
Logger.LogInfo($"Loaded {commandCount} command(s) and {dockBandCount} band(s) from {wrapper.ExtensionHost?.Extension?.PackageFullName} in {sw.ElapsedMilliseconds} ms");
Logger.LogInfo($"Loaded {commandCount} command(s) and {dockBandCount} band(s) from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} in {sw.ElapsedMilliseconds} ms");
return CommandLoadResult.Loaded(wrapper, result);
}
catch (TimeoutException)
{
Logger.LogWarning($"Loading commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName} timed out after {sw.ElapsedMilliseconds} ms, continuing in background");
Logger.LogWarning($"Loading commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} timed out after {sw.ElapsedMilliseconds} ms, continuing in background");
return CommandLoadResult.TimedOut(wrapper, loadTask, sw);
}
catch (OperationCanceledException)
{
Logger.LogDebug($"Loading commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName} was cancelled after {sw.ElapsedMilliseconds} ms");
Logger.LogDebug($"Loading commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} was cancelled after {sw.ElapsedMilliseconds} ms");
return CommandLoadResult.Failed(wrapper);
}
catch (Exception ex)
{
Logger.LogError($"Failed to load commands and bands for extension {wrapper.ExtensionHost?.Extension?.PackageFullName} after {sw.ElapsedMilliseconds} ms: {ex}");
Logger.LogError($"Failed to load commands and bands for {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} after {sw.ElapsedMilliseconds} ms: {ex}");
return CommandLoadResult.Failed(wrapper);
}
}
@@ -590,7 +558,7 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
}
}
Logger.LogInfo($"Late-loaded {commands?.Count ?? 0} command(s) and {dockBands?.Count ?? 0} band(s) from {wrapper.ExtensionHost?.Extension?.PackageFullName} in {sw.ElapsedMilliseconds} ms");
Logger.LogInfo($"Late-loaded {commands?.Count ?? 0} command(s) and {dockBands?.Count ?? 0} band(s) from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} in {sw.ElapsedMilliseconds} ms");
}
catch (OperationCanceledException)
{
@@ -598,52 +566,63 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
}
catch (Exception ex)
{
Logger.LogError($"Background loading of commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName} failed after {sw.ElapsedMilliseconds} ms: {ex}");
Logger.LogError($"Background loading of commands and bands from {wrapper.ExtensionHost?.Extension?.PackageFullName ?? wrapper.DisplayName} failed after {sw.ElapsedMilliseconds} ms: {ex}");
}
}
private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)
private void ExtensionService_OnProviderAdded(IExtensionService sender, IEnumerable<CommandProviderWrapper> wrappers)
{
// When we get an extension uninstall event, hop off to a BG thread
var ct = _currentExtensionLoadCancellationToken;
_ = Task.Run(
async () =>
{
// Then find all the top-level commands that belonged to that extension
await RegisterAndLoadCommandsAsync(wrappers, ct).ConfigureAwait(false);
},
ct);
}
private void ExtensionService_OnProviderRemoved(IExtensionService sender, IEnumerable<CommandProviderWrapper> removedWrappers)
{
// When we get a provider removal event, hop off to a BG thread
_ = Task.Run(
async () =>
{
var removedProviderIds = new HashSet<string>(removedWrappers.Select(w => w.ProviderId));
List<TopLevelViewModel> commandsToRemove = [];
List<TopLevelViewModel> bandsToRemove = [];
foreach (var extension in extensions)
{
lock (TopLevelCommands)
{
foreach (var command in TopLevelCommands)
{
var host = command.ExtensionHost;
if (host?.Extension == extension)
{
commandsToRemove.Add(command);
}
}
}
lock (_dockBandsLock)
lock (TopLevelCommands)
{
foreach (var command in TopLevelCommands)
{
foreach (var band in DockBands)
if (removedProviderIds.Contains(command.CommandProviderId))
{
var host = band.ExtensionHost;
if (host?.Extension == extension)
{
bandsToRemove.Add(band);
}
commandsToRemove.Add(command);
}
}
}
// Then back on the UI thread (remember, TopLevelCommands is
// Observable, so you can't touch it on the BG thread)...
lock (_dockBandsLock)
{
foreach (var band in DockBands)
{
if (removedProviderIds.Contains(band.CommandProviderId))
{
bandsToRemove.Add(band);
}
}
}
lock (_commandProvidersLock)
{
_commandProviders.RemoveAll(w => removedProviderIds.Contains(w.ProviderId));
}
await Task.Factory.StartNew(
() =>
{
// ... remove all the deleted commands.
lock (TopLevelCommands)
{
if (commandsToRemove.Count != 0)
@@ -715,6 +694,9 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
public void Receive(ReloadCommandsMessage message) =>
_ = ReloadAllCommandsAsync();
public void Receive(ProviderEnabledStateChangedMessage message) =>
_ = UpdateProviderEnabledStateAsyncCore(message.ProviderId, message.IsEnabled);
public void Receive(PinCommandItemMessage message)
{
var wrapper = LookupProvider(message.ProviderId);
@@ -752,8 +734,7 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
{
lock (_commandProvidersLock)
{
return _builtInCommands.FirstOrDefault(w => w.ProviderId == providerId)
?? _extensionCommandProviders.FirstOrDefault(w => w.ProviderId == providerId);
return _commandProviders.FirstOrDefault(w => w.ProviderId == providerId);
}
}
@@ -761,8 +742,7 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
{
lock (_commandProvidersLock)
{
return _builtInCommands.Any(wrapper => wrapper.Id == id && wrapper.IsActive)
|| _extensionCommandProviders.Any(wrapper => wrapper.Id == id && wrapper.IsActive);
return _commandProviders.Any(wrapper => wrapper.Id == id && wrapper.IsActive);
}
}
@@ -815,49 +795,18 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
public void Dispose()
{
foreach (var service in _extensionServices)
{
service.OnProviderAdded -= ExtensionService_OnProviderAdded;
service.OnProviderRemoved -= ExtensionService_OnProviderRemoved;
}
_extensionLoadCts.Cancel();
_extensionLoadCts.Dispose();
_reloadCommandsGate.Dispose();
GC.SuppressFinalize(this);
}
private sealed class ExtensionStartResult
{
public IExtensionWrapper Extension { get; }
public CommandProviderWrapper? Wrapper { get; private init; }
public Task? PendingStartTask { get; private init; }
public Stopwatch? Stopwatch { get; private init; }
[MemberNotNullWhen(true, nameof(Wrapper))]
public bool IsStarted => Wrapper is not null;
[MemberNotNullWhen(true, nameof(PendingStartTask), nameof(Stopwatch))]
public bool IsTimedOut => PendingStartTask is not null;
private ExtensionStartResult(IExtensionWrapper extension)
{
Extension = extension;
}
public static ExtensionStartResult Started(IExtensionWrapper extension, CommandProviderWrapper wrapper)
{
return new ExtensionStartResult(extension) { Wrapper = wrapper };
}
public static ExtensionStartResult TimedOut(IExtensionWrapper extension, Task pendingStartTask, Stopwatch sw)
{
return new ExtensionStartResult(extension) { PendingStartTask = pendingStartTask, Stopwatch = sw };
}
public static ExtensionStartResult Failed(IExtensionWrapper extension)
{
return new ExtensionStartResult(extension);
}
}
private sealed class CommandLoadResult
{
public TopLevelObjectSets? TopLevelObjectSets { get; private init; }

View File

@@ -264,7 +264,11 @@ public partial class App : Application, IDisposable
// Core services
services.AddSingleton(appInfoService);
services.AddSingleton<IExtensionService, ExtensionService>();
// Load IExtensionServices here
services.AddSingleton<IExtensionService, BuiltInExtensionService>();
services.AddSingleton<IExtensionService, WinRTExtensionService>();
services.AddSingleton<IExtensionService, JavaScriptExtensionService>();
services.AddSingleton<IRunHistoryService, RunHistoryService>();
services.AddSingleton<IRootPageService, PowerToysRootPageService>();

View File

@@ -922,8 +922,11 @@ public sealed partial class MainWindow : WindowEx,
}
}
var extensionService = serviceProvider.GetService<IExtensionService>()!;
extensionService.SignalStopExtensionsAsync();
var extensionServices = serviceProvider.GetServices<IExtensionService>();
foreach (var extensionService in extensionServices)
{
extensionService.SignalStopAsync();
}
App.Current.Services.GetService<TrayIconService>()!.Destroy();

View File

@@ -110,7 +110,6 @@
Visibility="{x:Bind IsText, Mode=OneWay}" />
<HyperlinkButton
Padding="0"
Command="{x:Bind NavigateCommand, Mode=OneWay}"
NavigateUri="{x:Bind Link, Mode=OneWay}"
UseSystemFocusVisuals="{StaticResource UseSystemFocusVisuals}"
Visibility="{x:Bind IsLink, Mode=OneWay}">

View File

@@ -36,7 +36,7 @@ internal sealed class PowerToysRootPageService : IRootPageService
public async Task PreLoadAsync()
{
await _tlcManager.LoadBuiltinsAsync();
await _tlcManager.LoadBuiltInProvidersAsync();
}
public Microsoft.CommandPalette.Extensions.IPage GetRootPage()
@@ -46,11 +46,11 @@ internal sealed class PowerToysRootPageService : IRootPageService
public async Task PostLoadRootPageAsync()
{
// After loading built-ins, and starting navigation, kick off a thread to load extensions.
_tlcManager.LoadExtensionsCommand.Execute(null);
// After loading built-ins, and starting navigation, kick off a thread to load external extensions.
_tlcManager.LoadExternalProvidersCommand.Execute(null);
await _tlcManager.LoadExtensionsCommand.ExecutionTask!;
if (_tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
await _tlcManager.LoadExternalProvidersCommand.ExecutionTask!;
if (_tlcManager.LoadExternalProvidersCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
// TODO: Handle failure case
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
// Mimic the JsonRpcResponse
public sealed class TestJsonRpcResponse
{
[JsonPropertyName("jsonrpc")]
public string JsonRpc { get; set; } = "2.0";
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("result")]
public JsonElement? Result { get; set; }
[JsonPropertyName("error")]
public object? Error { get; set; }
}
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(TestJsonRpcResponse))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(JsonNode))]
[JsonSerializable(typeof(JsonObject))]
internal partial class TestContext : JsonSerializerContext { }
var json = """{"jsonrpc":"2.0","id":3,"result":{"items":[{"title":"Say Hello","displayName":"Say Hello","subtitle":"Displays a toast message","icon":null,"section":"Commands","tags":[{"text":"Action"}],"command":{"id":"say-hello","name":"Say Hello","displayName":"Say Hello"}},{"title":"View Readme","displayName":"View Readme","subtitle":"Shows a markdown content page","icon":null,"section":"Pages","tags":[{"text":"Page"}],"command":{"id":"open-markdown","name":"View Readme","displayName":"View Readme"}},{"title":"Static Item","displayName":"Static Item","subtitle":"This item does not have an action","icon":null,"section":"Other","command":{"id":"item-3","name":"Static Item","displayName":"Static Item"}}]}}""";
Console.WriteLine($"Input JSON length: {json.Length}");
var response = JsonSerializer.Deserialize(json, TestContext.Default.TestJsonRpcResponse);
Console.WriteLine($"Response is null: {response == null}");
Console.WriteLine($"Response.Id: {response?.Id}");
Console.WriteLine($"Response.Result.HasValue: {response?.Result.HasValue}");
Console.WriteLine($"Response.Result.Value.ValueKind: {response?.Result?.ValueKind}");
if (response?.Result?.ValueKind == JsonValueKind.Object)
{
var result = response.Result.Value;
if (result.TryGetProperty("items", out var itemsProp))
{
Console.WriteLine($"items.ValueKind: {itemsProp.ValueKind}");
if (itemsProp.ValueKind == JsonValueKind.Array)
{
Console.WriteLine($"items count: {itemsProp.GetArrayLength()}");
foreach (var item in itemsProp.EnumerateArray())
{
if (item.TryGetProperty("title", out var t))
Console.WriteLine($" item title: {t.GetString()}");
}
}
}
else
{
Console.WriteLine("ERROR: 'items' property not found!");
Console.WriteLine($"Available properties:");
foreach (var prop in result.EnumerateObject())
{
Console.WriteLine($" {prop.Name}: {prop.Value.ValueKind}");
}
}
}
else
{
Console.WriteLine($"ERROR: Result is not an object! ValueKind = {response?.Result?.ValueKind}");
}

View File

@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Common.WinGet.Models;
using Microsoft.CmdPal.Common.WinGet.Services;
using Microsoft.CmdPal.UI.ViewModels.Gallery;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -61,7 +62,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -118,7 +119,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory(winGetService.Object, winGetStatusService.Object),
winGetService.Object,
@@ -202,7 +203,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory(winGetService.Object, winGetStatusService.Object),
winGetService.Object,
@@ -250,7 +251,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -292,7 +293,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -324,7 +325,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -359,7 +360,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -389,7 +390,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -421,7 +422,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory());
@@ -485,7 +486,7 @@ public class ExtensionGalleryViewModelTests
using var viewModel = new ExtensionGalleryViewModel(
galleryService.Object,
extensionService.Object,
new[] { extensionService.Object },
NullLogger<ExtensionGalleryViewModel>.Instance,
CreateGalleryExtensionViewModelFactory(winGetPackageStatusService: winGetStatusService.Object),
winGetPackageManagerService: null,

View File

@@ -0,0 +1,424 @@
# Building Command Palette Extensions with JavaScript/TypeScript
This guide explains how to create, develop, and publish JavaScript/TypeScript extensions for Command Palette using the JSONRPC extension system.
## Prerequisites
- Node.js 22+ (for development; end-users get it automatically)
- npm (comes with Node.js)
- TypeScript 5.8+ (recommended)
## Quick Start
### 1. Create a new extension project
```bash
mkdir my-cmdpal-extension
cd my-cmdpal-extension
npm init -y
npm install @microsoft/cmdpal-sdk
npm install -D typescript @types/node
```
### 2. Configure TypeScript
Create `tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*.ts"]
}
```
### 3. Configure package.json
Add the `cmdpal` field to your `package.json`:
```json
{
"name": "@yourname/my-extension",
"version": "1.0.0",
"main": "dist/index.js",
"cmdpal": {
"main": "dist/index.js",
"displayName": "My Extension",
"minVersion": "0.100.0"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
}
}
```
The `cmdpal.main` field specifies the entry point for your extension. If omitted, the standard `main` field is used as a fallback.
### 4. Write your extension
Create `src/index.ts`:
```typescript
import {
CommandProviderBase,
ListPageBase,
ListItemBase,
InvokableCommandBase,
ExtensionHost,
type ActivationContext,
type ICommandItem,
type IListItem,
type CommandResult,
} from '@microsoft/cmdpal-sdk';
// A command that does something when invoked
class GreetCommand extends InvokableCommandBase {
id = 'greet';
name = 'Greet';
invoke(): CommandResult {
ExtensionHost.log('User invoked greet!');
return { kind: 'showToast', args: { message: 'Hello from my extension! 👋' } };
}
}
// A list page showing items
class MyListPage extends ListPageBase {
id = 'my-list';
name = 'My Items';
title = 'My Extension';
placeholderText = 'Search items...';
getItems(): IListItem[] {
return [
new ListItemBase({
command: new GreetCommand(),
title: 'Say Hello',
subtitle: 'Shows a greeting toast',
tags: [{ text: 'Action' }],
}),
];
}
}
// The main provider — this is what CmdPal loads
class MyProvider extends CommandProviderBase {
id = 'my-extension';
displayName = 'My Extension';
private page = new MyListPage();
topLevelCommands(): ICommandItem[] {
return [{
command: this.page,
title: this.page.title,
subtitle: 'My awesome extension',
}];
}
getCommand(id: string) {
if (id === this.page.id) return this.page as any;
if (id === 'greet') return new GreetCommand();
return null;
}
}
// Entry point — CmdPal calls this to activate your extension
export function activate(context: ActivationContext) {
ExtensionHost.log(`Extension activated in ${context.extensionDirectory}`);
return new MyProvider();
}
```
### 5. Build and test
```bash
npm run build
```
## Extension Architecture
### How it works
```
┌─────────────────────┐ JSONRPC/stdio ┌──────────────────┐
│ Command Palette │ ◄───────────────────────► │ Node.js Host │
│ (C# / WinUI) │ │ │
│ │ │ ┌────────────┐ │
│ JSONRPCExtension- │ provider/getCommands ──► │ │ Extension1 │ │
│ Service │ ◄── { items: [...] } │ └────────────┘ │
│ │ │ ┌────────────┐ │
│ │ command/invoke ────────► │ │ Extension2 │ │
│ │ ◄── { kind: "toast" } │ └────────────┘ │
└─────────────────────┘ └──────────────────┘
```
1. CmdPal spawns a single Node.js host process at startup
2. The host loads all installed JS extensions
3. CmdPal communicates with extensions via JSONRPC over stdio
4. Your extension responds to requests and can send notifications back
### Entry Point
Your extension must export an `activate` function:
```typescript
export function activate(context: ActivationContext): CommandProvider {
return new MyProvider();
}
```
The `context` provides:
- `extensionId` — Your extension's unique identifier
- `extensionDirectory` — Absolute path to your extension's install directory
### CommandProvider
The `CommandProvider` is the root of your extension. It must implement:
| Method | Description |
|--------|-------------|
| `topLevelCommands()` | Returns items shown in the main palette |
| `fallbackCommands()` | (Optional) Returns fallback search handlers |
| `getCommand(id)` | (Optional) Returns a command/page by ID |
| `initializeWithHost(host)` | (Optional) Called with the extension host API |
| `dispose()` | (Optional) Cleanup when extension is unloaded |
## Page Types
### ListPage
Displays a searchable list of items:
```typescript
class MyList extends ListPageBase {
id = 'my-list';
name = 'My List';
title = 'Items';
placeholderText = 'Search...';
showDetails = true;
getItems(): IListItem[] {
return [/* items */];
}
}
```
### DynamicListPage
A list where your extension handles filtering:
```typescript
class SearchPage extends DynamicListPageBase {
id = 'search';
name = 'Search';
title = 'Search';
private query = '';
setSearchText(text: string) {
this.query = text;
// Items will be re-fetched automatically
}
async getItems() {
return await myApi.search(this.query);
}
}
```
### ContentPage
Displays rich content (markdown, forms, images):
```typescript
class AboutPage extends ContentPageBase {
id = 'about';
name = 'About';
title = 'About My Extension';
getContent(): Content[] {
return [
{ type: 'markdown', body: '# Hello\n\nThis is **markdown** content.' },
{ type: 'image', image: { light: { icon: 'https://...' } }, maxWidth: 300 },
];
}
}
```
## Commands
### InvokableCommand
A command that performs an action:
```typescript
class CopyCommand extends InvokableCommandBase {
id = 'copy';
name = 'Copy to Clipboard';
async invoke(): Promise<CommandResult> {
// Do your action here
return { kind: 'showToast', args: { message: 'Copied!' } };
}
}
```
### CommandResult
After invoking, return a result to control navigation:
| Kind | Description |
|------|-------------|
| `dismiss` | Close the palette |
| `goHome` | Return to the main page, keep palette open |
| `goBack` | Go back one page |
| `hide` | Hide palette but keep current page |
| `keepOpen` | Do nothing (stay on current page) |
| `goToPage` | Navigate to another page (`args: { pageId, navigationMode }`) |
| `showToast` | Show a toast message (`args: { message }`) |
| `confirm` | Show a confirmation dialog |
## Logging & Status
Use `ExtensionHost` to communicate with CmdPal:
```typescript
import { ExtensionHost } from '@microsoft/cmdpal-sdk';
// Logging (visible in CmdPal logs)
ExtensionHost.log('Something happened');
ExtensionHost.log('Something failed', 'error');
// Status bar messages
ExtensionHost.showStatus('Loading data...', 'info', { isIndeterminate: true });
ExtensionHost.hideStatus('loading-id');
```
## Publishing to the Gallery
### 1. Publish to npm
```bash
npm publish --access public
```
### 2. Submit to the gallery
Create a PR to [microsoft/CmdPal-Extensions](https://github.com/microsoft/CmdPal-Extensions) adding your extension to the gallery manifest:
```json
{
"id": "yourname.my-extension",
"title": "My Extension",
"shortDescription": "A brief description",
"description": "Full description of what your extension does...",
"author": { "name": "Your Name", "url": "https://github.com/yourname" },
"installSources": [
{ "type": "npm", "id": "@yourname/my-extension" }
],
"iconUrl": "https://raw.githubusercontent.com/.../icon.png",
"tags": ["productivity"],
"categories": ["utilities-and-tools"],
"addedAt": "2026-01-01"
}
```
### Install source types
| Type | Description | Required fields |
|------|-------------|-----------------|
| `npm` | npm package | `id` (package name) |
| `winget` | WinGet package | `id` (winget ID) |
| `msstore` | Microsoft Store | `id` (store product ID) |
## Extension Settings
Extensions can provide a settings page by implementing `settings` on the provider:
```typescript
class SettingsPage extends ContentPageBase {
id = 'settings';
name = 'Settings';
title = 'My Extension Settings';
getContent(): Content[] {
return [{
type: 'form',
templateJson: JSON.stringify({
type: 'AdaptiveCard',
body: [
{ type: 'Input.Text', id: 'apiKey', label: 'API Key' }
],
actions: [{ type: 'Action.Submit', title: 'Save' }]
}),
dataJson: JSON.stringify({ apiKey: '' }),
submitForm(inputs: string, data: string) {
// Save settings
return { kind: 'showToast', args: { message: 'Settings saved!' } };
}
}];
}
}
class MyProvider extends CommandProviderBase {
// ...
settings = { settingsPage: new SettingsPage() };
}
```
## Notifications (Push Updates)
Extensions can notify CmdPal when data changes, triggering a re-fetch:
```typescript
// In DynamicListPageBase subclasses:
this.notifyItemsChanged(); // triggers page/getItems re-call
// Or use the connection directly (advanced):
// The SDK wires this up through the JSONRPC bridge
```
## File Structure
A typical extension project:
```
my-extension/
├── package.json # npm config + cmdpal field
├── tsconfig.json # TypeScript config
├── src/
│ └── index.ts # Extension entry point
├── dist/ # Compiled output (git-ignored)
│ └── index.js
└── README.md
```
## Troubleshooting
### Extension not loading?
1. Check that `cmdpal.main` (or `main`) in package.json points to a valid JS file
2. Verify your `activate()` function returns a valid `CommandProvider`
3. Check CmdPal logs for error messages from your extension
### Commands not appearing?
1. Ensure `topLevelCommands()` returns at least one item
2. Verify the extension is enabled in CmdPal settings
3. Check that the `getCommand(id)` method returns pages/commands referenced by your items
### Logs
Extension logs are written to:
`%LOCALAPPDATA%\Microsoft\PowerToys\CmdPal\logs\`
## API Reference
See the [TypeScript SDK README](../ts-sdk/README.md) for the complete API surface.
See the [JSONRPC Protocol Specification](../docs/jsonrpc-protocol.md) for the wire protocol details.

View File

@@ -0,0 +1,652 @@
# Command Palette JSONRPC Extension Protocol
This document defines the JSONRPC 2.0 protocol used between the Command Palette host (C#) and the Node.js extension host process.
## Transport
- **Transport:** stdio (stdin/stdout)
- **Framing:** Header-delimited (Content-Length), matching the LSP/DAP convention
- **Direction:** Bidirectional — both host and extensions can send requests and notifications
## Lifecycle
1. CmdPal spawns the Node host process
2. Host sends `initialize` request with configuration
3. Node host responds with capabilities
4. Host sends `extensions/load` with list of extensions to activate
5. Node host loads each extension, responds with success/failure per extension
6. Normal operation: host sends requests, extensions respond; extensions send notifications
## Conventions
- All method names use `/` as namespace separator
- Request IDs are integers, auto-incremented by the sender
- Extension-specific requests include an `extensionId` field to route to the correct extension
- All property values use camelCase JSON naming
---
## Host → Node Requests
### `initialize`
Sent once after process spawn to configure the Node host.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"hostVersion": "0.100.0",
"extensionsDirectory": "C:\\Users\\...\\extensions",
"logsDirectory": "C:\\Users\\...\\logs"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "1.0.0",
"hostReady": true
}
}
```
### `extensions/load`
Instructs the Node host to load a set of extensions.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "extensions/load",
"params": {
"extensions": [
{
"id": "cooldev.weather",
"packageName": "@cooldev/weather-extension",
"entryPoint": "dist/index.js",
"directory": "C:\\Users\\...\\extensions\\@cooldev-weather-extension"
}
]
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"loaded": [
{ "id": "cooldev.weather", "success": true }
],
"failed": []
}
}
```
### `extensions/unload`
Instructs the Node host to unload a specific extension.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 3,
"method": "extensions/unload",
"params": {
"extensionId": "cooldev.weather"
}
}
```
### `provider/getTopLevelCommands`
Maps to `ICommandProvider.TopLevelCommands()`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 4,
"method": "provider/getTopLevelCommands",
"params": {
"extensionId": "cooldev.weather"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"items": [
{
"command": {
"id": "weather-now",
"name": "Current Weather",
"icon": { "light": { "icon": "\uE9CA" }, "dark": { "icon": "\uE9CA" } }
},
"title": "Current Weather",
"subtitle": "Get current weather for your location",
"icon": { "light": { "icon": "\uE9CA" }, "dark": { "icon": "\uE9CA" } }
}
]
}
}
```
### `provider/getFallbackCommands`
Maps to `ICommandProvider.FallbackCommands()`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 5,
"method": "provider/getFallbackCommands",
"params": {
"extensionId": "cooldev.weather"
}
}
```
**Response:** Same shape as `provider/getTopLevelCommands` but items include `fallbackHandler` field.
### `provider/getSettings`
Maps to `ICommandProvider.Settings`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 6,
"method": "provider/getSettings",
"params": {
"extensionId": "cooldev.weather"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"settingsPage": {
"id": "weather-settings",
"name": "Weather Settings",
"title": "Weather Settings",
"content": [...]
}
}
}
```
### `command/invoke`
Maps to `IInvokableCommand.Invoke()`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 7,
"method": "command/invoke",
"params": {
"extensionId": "cooldev.weather",
"commandId": "weather-now"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"kind": "goToPage",
"args": {
"pageId": "weather-detail",
"navigationMode": "push"
}
}
}
```
### `page/getItems`
Maps to `IListPage.GetItems()`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 8,
"method": "page/getItems",
"params": {
"extensionId": "cooldev.weather",
"pageId": "weather-list"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 8,
"result": {
"items": [
{
"command": { "id": "city-nyc", "name": "New York" },
"title": "New York",
"subtitle": "72°F, Sunny",
"tags": [{ "text": "Favorite", "icon": null }],
"section": "Favorites"
}
]
}
}
```
### `page/setSearchText`
Maps to `IDynamicListPage.SearchText { set }`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 9,
"method": "page/setSearchText",
"params": {
"extensionId": "cooldev.weather",
"pageId": "weather-list",
"searchText": "new york"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 9,
"result": { "accepted": true }
}
```
### `page/loadMore`
Maps to `IListPage.LoadMore()`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 10,
"method": "page/loadMore",
"params": {
"extensionId": "cooldev.weather",
"pageId": "weather-list"
}
}
```
### `page/getContent`
Maps to `IContentPage.GetContent()`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 11,
"method": "page/getContent",
"params": {
"extensionId": "cooldev.weather",
"pageId": "weather-detail"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"content": [
{ "type": "markdown", "body": "## Current Weather\n\n72°F and sunny" },
{ "type": "image", "image": { "light": { "icon": "https://..." } }, "maxWidth": 200, "maxHeight": 200 }
]
}
}
```
### `page/getProperties`
Gets page-level properties (title, isLoading, placeholderText, showDetails, filters, gridProperties, etc.).
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 12,
"method": "page/getProperties",
"params": {
"extensionId": "cooldev.weather",
"pageId": "weather-list"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 12,
"result": {
"title": "Weather",
"isLoading": false,
"placeholderText": "Search cities...",
"showDetails": true,
"hasMoreItems": false,
"filters": {
"currentFilterId": "all",
"filters": [
{ "id": "all", "name": "All" },
{ "id": "favorites", "name": "Favorites" }
]
}
}
}
```
### `form/submit`
Maps to `IFormContent.SubmitForm()`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 13,
"method": "form/submit",
"params": {
"extensionId": "cooldev.weather",
"pageId": "settings-form",
"inputs": "{ \"apiKey\": \"abc123\" }",
"data": "{ \"location\": \"NYC\" }"
}
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 13,
"result": {
"kind": "dismiss"
}
}
```
### `fallback/updateQuery`
Maps to `IFallbackHandler.UpdateQuery()`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 14,
"method": "fallback/updateQuery",
"params": {
"extensionId": "cooldev.weather",
"commandId": "weather-fallback",
"query": "weather in paris"
}
}
```
### `provider/getCommand`
Maps to `ICommandProvider.GetCommand(id)`.
**Request:**
```json
{
"jsonrpc": "2.0",
"id": 15,
"method": "provider/getCommand",
"params": {
"extensionId": "cooldev.weather",
"commandId": "weather-now"
}
}
```
---
## Node → Host Notifications
These are fire-and-forget messages from extensions to the host.
### `notify/itemsChanged`
Extension signals that its list items have changed. Host should re-fetch.
```json
{
"jsonrpc": "2.0",
"method": "notify/itemsChanged",
"params": {
"extensionId": "cooldev.weather",
"pageId": "weather-list",
"totalItems": 15
}
}
```
### `notify/propChanged`
Extension signals that a property has changed on a command/page.
```json
{
"jsonrpc": "2.0",
"method": "notify/propChanged",
"params": {
"extensionId": "cooldev.weather",
"objectId": "weather-now",
"propertyName": "title"
}
}
```
### `notify/commandsChanged`
Extension signals that its top-level commands have changed. Host should re-fetch.
```json
{
"jsonrpc": "2.0",
"method": "notify/commandsChanged",
"params": {
"extensionId": "cooldev.weather"
}
}
```
### `host/log`
Extension sends a log message.
```json
{
"jsonrpc": "2.0",
"method": "host/log",
"params": {
"extensionId": "cooldev.weather",
"state": "info",
"message": "Fetched weather data for 5 cities"
}
}
```
`state` is one of: `"info"`, `"success"`, `"warning"`, `"error"`.
### `host/showStatus`
Extension requests a status message be shown.
```json
{
"jsonrpc": "2.0",
"method": "host/showStatus",
"params": {
"extensionId": "cooldev.weather",
"context": "page",
"message": "Loading weather data...",
"state": "info",
"progress": { "isIndeterminate": true }
}
}
```
### `host/hideStatus`
Extension requests a status message be hidden.
```json
{
"jsonrpc": "2.0",
"method": "host/hideStatus",
"params": {
"extensionId": "cooldev.weather",
"messageId": "loading-weather"
}
}
```
---
## Data Types
### IconInfo
```json
{
"light": { "icon": "<glyph or URL>", "data": null },
"dark": { "icon": "<glyph or URL>", "data": null }
}
```
### CommandResultKind (enum string)
`"dismiss"` | `"goHome"` | `"goBack"` | `"hide"` | `"keepOpen"` | `"goToPage"` | `"showToast"` | `"confirm"`
### NavigationMode (enum string)
`"push"` | `"goBack"` | `"goHome"`
### MessageState (enum string)
`"info"` | `"success"` | `"warning"` | `"error"`
### StatusContext (enum string)
`"page"` | `"extension"`
### ContentType (discriminator for content array items)
`"markdown"` | `"form"` | `"tree"` | `"plainText"` | `"image"`
### Tag
```json
{
"icon": <IconInfo | null>,
"text": "string",
"foreground": { "hasValue": true, "color": { "r": 255, "g": 0, "b": 0, "a": 255 } },
"background": null,
"toolTip": "string"
}
```
### Details
```json
{
"heroImage": <IconInfo | null>,
"title": "string",
"body": "string",
"metadata": [
{ "key": "Author", "data": { "type": "tags", "tags": [...] } },
{ "key": "Link", "data": { "type": "link", "link": "https://...", "text": "Homepage" } }
]
}
```
### Filter
```json
{
"id": "string",
"name": "string",
"icon": <IconInfo | null>
}
```
### GridProperties
```json
{
"type": "small" | "medium" | "gallery",
"showTitle": true,
"showSubtitle": true
}
```
---
## Error Handling
JSONRPC errors use standard error codes:
- `-32700`: Parse error
- `-32600`: Invalid request
- `-32601`: Method not found
- `-32602`: Invalid params
- `-32603`: Internal error
Custom error codes (extension-specific):
- `-32000`: Extension not found
- `-32001`: Extension not loaded
- `-32002`: Command not found
- `-32003`: Page not found
- `-32004`: Extension threw an exception (details in `data` field)
```json
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32004,
"message": "Extension threw an exception",
"data": {
"extensionId": "cooldev.weather",
"stack": "Error: API key invalid\n at ..."
}
}
}
```

View File

@@ -0,0 +1,2 @@
import { type ActivationContext, type ICommandProvider } from '@microsoft/cmdpal-sdk';
export declare function activate(context: ActivationContext): ICommandProvider;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
{
"name": "@cmdpal/sample-js-extension",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@cmdpal/sample-js-extension",
"version": "0.1.0",
"dependencies": {
"@microsoft/cmdpal-sdk": "file:../../ts-sdk"
},
"devDependencies": {
"typescript": "^5.8.0"
}
},
"../../ts-sdk": {
"name": "@microsoft/cmdpal-sdk",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "^9.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.8.0"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@microsoft/cmdpal-sdk": {
"resolved": "../../ts-sdk",
"link": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

View File

@@ -0,0 +1,26 @@
{
"name": "@microsoft/cmdpal-sample-extension",
"version": "0.1.0",
"description": "Sample JavaScript extension for Command Palette demonstrating the JSONRPC SDK",
"main": "dist/index.js",
"cmdpal": {
"displayName": "Sample JS Extension",
"icon": "\uE943",
"publisher": "Microsoft",
"capabilities": ["commands"],
"debug": false
},
"engines": {
"node": ">=22"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"@microsoft/cmdpal-sdk": "file:../../ts-sdk"
},
"devDependencies": {
"typescript": "^5.8.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,68 @@
# setup-dev-extensions.ps1
# Sets up the JavaScript extension development environment for local testing.
# Run from: src/modules/cmdpal/
#
# What it does:
# 1. Builds the TypeScript SDK (ts-sdk)
# 2. Builds the sample JS extension
# 3. Installs the sample extension into the JSExtensions directory
param(
[string]$Configuration = "Debug",
[string]$Platform = "ARM64"
)
$ErrorActionPreference = "Stop"
$scriptDir = $PSScriptRoot
# Paths
$sdkDir = Join-Path $scriptDir "ts-sdk"
$sampleExtDir = Join-Path $scriptDir "ext\SampleJSExtension"
$extensionsDir = Join-Path $env:LOCALAPPDATA "Microsoft\PowerToys\CommandPalette\JSExtensions"
$sampleInstallDir = Join-Path $extensionsDir "sample-js-extension"
Write-Host "=== Step 1: Building TypeScript SDK ===" -ForegroundColor Cyan
Push-Location $sdkDir
npm install --quiet 2>&1 | Out-Null
npm run build
if ($LASTEXITCODE -ne 0) { Pop-Location; throw "SDK build failed" }
Pop-Location
Write-Host " SDK built successfully." -ForegroundColor Green
Write-Host ""
Write-Host "=== Step 2: Building Sample Extension ===" -ForegroundColor Cyan
Push-Location $sampleExtDir
npm install --quiet 2>&1 | Out-Null
npm run build
if ($LASTEXITCODE -ne 0) { Pop-Location; throw "Sample extension build failed" }
Pop-Location
Write-Host " Sample extension built successfully." -ForegroundColor Green
Write-Host ""
Write-Host "=== Step 3: Installing Sample Extension ===" -ForegroundColor Cyan
# Create the extensions directory structure
New-Item -ItemType Directory -Path $sampleInstallDir -Force | Out-Null
# Copy the built sample extension (dist/, node_modules/, package.json)
Copy-Item -Path "$sampleExtDir\package.json" -Destination $sampleInstallDir -Force
if (Test-Path "$sampleExtDir\dist") {
Copy-Item -Path "$sampleExtDir\dist" -Destination $sampleInstallDir -Recurse -Force
}
if (Test-Path "$sampleExtDir\node_modules") {
Copy-Item -Path "$sampleExtDir\node_modules" -Destination $sampleInstallDir -Recurse -Force
}
Write-Host " Installed to: $sampleInstallDir" -ForegroundColor Green
Write-Host ""
Write-Host "=== Done! ===" -ForegroundColor Green
Write-Host ""
Write-Host "Extension installed at: $sampleInstallDir"
Write-Host "The JavaScriptExtensionService will discover it on next CmdPal launch."
Write-Host ""
Write-Host "Requirements:"
Write-Host " - Node.js v22+ must be on your PATH"
Write-Host " - Each JS extension runs as its own 'node dist/index.js' process"
Write-Host ""
Write-Host "Hot-reload: Edit .js files in the extension directory and CmdPal will"
Write-Host "automatically restart the extension process (500ms debounce)."

View File

@@ -0,0 +1,115 @@
# @microsoft/cmdpal-sdk
TypeScript SDK for building Command Palette extensions.
## Quick Start
```bash
npm install @microsoft/cmdpal-sdk
```
```typescript
import {
CommandProviderBase,
ListPageBase,
ListItemBase,
type ActivationContext,
} from '@microsoft/cmdpal-sdk';
class MyListPage extends ListPageBase {
id = 'my-list';
name = 'My List';
title = 'My Extension';
placeholderText = 'Search...';
getItems() {
return [
new ListItemBase({
command: { id: 'item-1', name: 'Hello World' },
title: 'Hello World',
subtitle: 'My first extension item',
}),
];
}
}
class MyProvider extends CommandProviderBase {
id = 'my-extension';
displayName = 'My Extension';
private page = new MyListPage();
topLevelCommands() {
return [{ command: this.page, title: this.page.title }];
}
getCommand(id: string) {
if (id === this.page.id) return this.page;
return null;
}
}
export function activate(context: ActivationContext) {
return new MyProvider();
}
```
## Package.json Configuration
Add a `cmdpal` field to your `package.json`:
```json
{
"name": "@yourorg/my-extension",
"version": "1.0.0",
"cmdpal": {
"main": "dist/index.js",
"displayName": "My Extension",
"minVersion": "0.100.0"
},
"main": "dist/index.js"
}
```
## API Overview
### Base Classes
| Class | Description |
|-------|-------------|
| `CommandProviderBase` | Main extension entry point. Provides commands to the palette. |
| `ListPageBase` | A page displaying a searchable list of items. |
| `DynamicListPageBase` | A list page where the extension handles search filtering. |
| `ContentPageBase` | A page displaying rich content (markdown, forms, images). |
| `InvokableCommandBase` | A command that performs an action when invoked. |
| `ListItemBase` | A single item in a list page. |
| `CommandItemBase` | A command item displayed in menus. |
### Runtime
| Export | Description |
|--------|-------------|
| `ExtensionHost` | Static class for logging and status messages. |
| `activate()` | Helper for extension activation. |
### Types
The SDK exports all interfaces matching the Command Palette extension contract:
`ICommand`, `IListPage`, `IDynamicListPage`, `IContentPage`, `ICommandProvider`, etc.
## Extension Lifecycle
1. CmdPal discovers your package in the extensions directory
2. The Node host calls your `activate()` export
3. Your provider's `topLevelCommands()` is called to populate the palette
4. When a user interacts with your commands, corresponding methods are called via JSONRPC
## Logging
```typescript
import { ExtensionHost } from '@microsoft/cmdpal-sdk';
ExtensionHost.log('Something happened');
ExtensionHost.log('Something went wrong', 'error');
ExtensionHost.showStatus('Loading...', 'info', { isIndeterminate: true });
```

View File

@@ -0,0 +1,19 @@
import type { ICommandItem, ICommand, ContextItem, IconInfo } from '../types';
/**
* Base class for command items displayed in lists and menus.
*/
export declare class CommandItemBase implements ICommandItem {
command: ICommand;
title: string;
subtitle?: string;
icon?: IconInfo | null;
moreCommands?: ContextItem[];
constructor(options: {
command: ICommand;
title: string;
subtitle?: string;
icon?: IconInfo | null;
moreCommands?: ContextItem[];
});
}
//# sourceMappingURL=CommandItemBase.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CommandItemBase.d.ts","sourceRoot":"","sources":["../../src/base/CommandItemBase.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE9E;;GAEG;AACH,qBAAa,eAAgB,YAAW,YAAY;IAClD,OAAO,EAAE,QAAQ,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;gBAEjB,OAAO,EAAE;QACnB,OAAO,EAAE,QAAQ,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;QACvB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;KAC9B;CAOF"}

View File

@@ -0,0 +1,25 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandItemBase = void 0;
/**
* Base class for command items displayed in lists and menus.
*/
class CommandItemBase {
command;
title;
subtitle;
icon;
moreCommands;
constructor(options) {
this.command = options.command;
this.title = options.title;
this.subtitle = options.subtitle;
this.icon = options.icon ?? null;
this.moreCommands = options.moreCommands;
}
}
exports.CommandItemBase = CommandItemBase;
//# sourceMappingURL=CommandItemBase.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CommandItemBase.js","sourceRoot":"","sources":["../../src/base/CommandItemBase.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAIjE;;GAEG;AACH,MAAa,eAAe;IAC1B,OAAO,CAAW;IAClB,KAAK,CAAS;IACd,QAAQ,CAAU;IAClB,IAAI,CAAmB;IACvB,YAAY,CAAiB;IAE7B,YAAY,OAMX;QACC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC3C,CAAC;CACF;AApBD,0CAoBC"}

View File

@@ -0,0 +1,33 @@
import type { ICommandProvider, ICommandItem, IFallbackCommandItem, ICommand, ICommandSettings, IconInfo, IExtensionHost } from '../types';
/**
* Base class for Command Palette extension providers.
* Extend this class to create an extension that provides commands to the palette.
*
* @example
* ```typescript
* import { CommandProviderBase, ListPageBase } from '@microsoft/cmdpal-sdk';
*
* class MyProvider extends CommandProviderBase {
* id = 'my-extension';
* displayName = 'My Extension';
*
* topLevelCommands() {
* return [{ command: new MyPage(), title: 'My Command', icon: null }];
* }
* }
* ```
*/
export declare abstract class CommandProviderBase implements ICommandProvider {
abstract id: string;
abstract displayName: string;
icon?: IconInfo | null;
frozen?: boolean;
settings?: ICommandSettings | null;
protected host?: IExtensionHost;
abstract topLevelCommands(): Promise<ICommandItem[]> | ICommandItem[];
fallbackCommands?(): Promise<IFallbackCommandItem[]> | IFallbackCommandItem[];
getCommand?(id: string): Promise<ICommand | null> | ICommand | null;
initializeWithHost(host: IExtensionHost): void;
dispose(): void;
}
//# sourceMappingURL=CommandProviderBase.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CommandProviderBase.d.ts","sourceRoot":"","sources":["../../src/base/CommandProviderBase.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACf,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;;;;;GAiBG;AACH,8BAAsB,mBAAoB,YAAW,gBAAgB;IACnE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAQ;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAS;IACzB,QAAQ,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IAE1C,SAAS,CAAC,IAAI,CAAC,EAAE,cAAc,CAAC;IAEhC,QAAQ,CAAC,gBAAgB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,GAAG,YAAY,EAAE;IAErE,gBAAgB,CAAC,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,GAAG,oBAAoB,EAAE;IAI7E,UAAU,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI;IAInE,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAI9C,OAAO,IAAI,IAAI;CAGhB"}

View File

@@ -0,0 +1,44 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandProviderBase = void 0;
/**
* Base class for Command Palette extension providers.
* Extend this class to create an extension that provides commands to the palette.
*
* @example
* ```typescript
* import { CommandProviderBase, ListPageBase } from '@microsoft/cmdpal-sdk';
*
* class MyProvider extends CommandProviderBase {
* id = 'my-extension';
* displayName = 'My Extension';
*
* topLevelCommands() {
* return [{ command: new MyPage(), title: 'My Command', icon: null }];
* }
* }
* ```
*/
class CommandProviderBase {
icon = null;
frozen = false;
settings = null;
host;
fallbackCommands() {
return [];
}
getCommand(id) {
return null;
}
initializeWithHost(host) {
this.host = host;
}
dispose() {
// Override to clean up resources
}
}
exports.CommandProviderBase = CommandProviderBase;
//# sourceMappingURL=CommandProviderBase.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CommandProviderBase.js","sourceRoot":"","sources":["../../src/base/CommandProviderBase.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAYjE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAsB,mBAAmB;IAIvC,IAAI,GAAqB,IAAI,CAAC;IAC9B,MAAM,GAAa,KAAK,CAAC;IACzB,QAAQ,GAA6B,IAAI,CAAC;IAEhC,IAAI,CAAkB;IAIhC,gBAAgB;QACd,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,UAAU,CAAE,EAAU;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kBAAkB,CAAC,IAAoB;QACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,OAAO;QACL,iCAAiC;IACnC,CAAC;CACF;AA3BD,kDA2BC"}

View File

@@ -0,0 +1,33 @@
import type { IContentPage, Content, Details, ContextItem, IconInfo, OptionalColor } from '../types';
/**
* Base class for content pages that display rich content (markdown, forms, images, etc.).
*
* @example
* ```typescript
* import { ContentPageBase } from '@microsoft/cmdpal-sdk';
*
* class ReadmePage extends ContentPageBase {
* id = 'readme';
* name = 'README';
* title = 'About This Extension';
*
* getContent() {
* return [
* { type: 'markdown', body: '# Hello World\n\nThis is my extension.' }
* ];
* }
* }
* ```
*/
export declare abstract class ContentPageBase implements IContentPage {
abstract id: string;
abstract name: string;
abstract title: string;
icon?: IconInfo | null;
isLoading?: boolean;
accentColor?: OptionalColor | null;
details?: Details | null;
commands?: ContextItem[];
abstract getContent(): Promise<Content[]> | Content[];
}
//# sourceMappingURL=ContentPageBase.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ContentPageBase.d.ts","sourceRoot":"","sources":["../../src/base/ContentPageBase.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,OAAO,EACP,OAAO,EACP,WAAW,EACX,QAAQ,EACR,aAAa,EACd,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,8BAAsB,eAAgB,YAAW,YAAY;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAQ;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAS;IAC5B,WAAW,CAAC,EAAE,aAAa,GAAG,IAAI,CAAQ;IAC1C,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAQ;IAChC,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAM;IAE9B,QAAQ,CAAC,UAAU,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE;CACtD"}

View File

@@ -0,0 +1,35 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContentPageBase = void 0;
/**
* Base class for content pages that display rich content (markdown, forms, images, etc.).
*
* @example
* ```typescript
* import { ContentPageBase } from '@microsoft/cmdpal-sdk';
*
* class ReadmePage extends ContentPageBase {
* id = 'readme';
* name = 'README';
* title = 'About This Extension';
*
* getContent() {
* return [
* { type: 'markdown', body: '# Hello World\n\nThis is my extension.' }
* ];
* }
* }
* ```
*/
class ContentPageBase {
icon = null;
isLoading = false;
accentColor = null;
details = null;
commands = [];
}
exports.ContentPageBase = ContentPageBase;
//# sourceMappingURL=ContentPageBase.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ContentPageBase.js","sourceRoot":"","sources":["../../src/base/ContentPageBase.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAWjE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAsB,eAAe;IAKnC,IAAI,GAAqB,IAAI,CAAC;IAC9B,SAAS,GAAa,KAAK,CAAC;IAC5B,WAAW,GAA0B,IAAI,CAAC;IAC1C,OAAO,GAAoB,IAAI,CAAC;IAChC,QAAQ,GAAmB,EAAE,CAAC;CAG/B;AAZD,0CAYC"}

View File

@@ -0,0 +1,36 @@
import type { IDynamicListPage } from '../types';
import { ListPageBase } from './ListPageBase';
/**
* Base class for dynamic list pages where the host sends search text
* and the extension filters/fetches results dynamically.
*
* @example
* ```typescript
* import { DynamicListPageBase } from '@microsoft/cmdpal-sdk';
*
* class SearchPage extends DynamicListPageBase {
* id = 'search';
* name = 'Search';
* title = 'Search';
* private query = '';
*
* setSearchText(text: string) {
* this.query = text;
* this.notifyItemsChanged();
* }
*
* async getItems() {
* return await fetchResults(this.query);
* }
* }
* ```
*/
export declare abstract class DynamicListPageBase extends ListPageBase implements IDynamicListPage {
abstract setSearchText(text: string): void;
/**
* Call this when items have changed and the host should re-fetch.
* The SDK bridge will send a notify/itemsChanged notification.
*/
protected notifyItemsChanged(): void;
}
//# sourceMappingURL=DynamicListPageBase.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"DynamicListPageBase.d.ts","sourceRoot":"","sources":["../../src/base/DynamicListPageBase.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,8BAAsB,mBAAoB,SAAQ,YAAa,YAAW,gBAAgB;IACxF,QAAQ,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAE1C;;;OAGG;IACH,SAAS,CAAC,kBAAkB,IAAI,IAAI;CAIrC"}

View File

@@ -0,0 +1,44 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.DynamicListPageBase = void 0;
const ListPageBase_1 = require("./ListPageBase");
/**
* Base class for dynamic list pages where the host sends search text
* and the extension filters/fetches results dynamically.
*
* @example
* ```typescript
* import { DynamicListPageBase } from '@microsoft/cmdpal-sdk';
*
* class SearchPage extends DynamicListPageBase {
* id = 'search';
* name = 'Search';
* title = 'Search';
* private query = '';
*
* setSearchText(text: string) {
* this.query = text;
* this.notifyItemsChanged();
* }
*
* async getItems() {
* return await fetchResults(this.query);
* }
* }
* ```
*/
class DynamicListPageBase extends ListPageBase_1.ListPageBase {
/**
* Call this when items have changed and the host should re-fetch.
* The SDK bridge will send a notify/itemsChanged notification.
*/
notifyItemsChanged() {
// This will be wired up by the runtime bridge
// The bridge patches this method to send JSONRPC notifications
}
}
exports.DynamicListPageBase = DynamicListPageBase;
//# sourceMappingURL=DynamicListPageBase.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"DynamicListPageBase.js","sourceRoot":"","sources":["../../src/base/DynamicListPageBase.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAGjE,iDAA8C;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAsB,mBAAoB,SAAQ,2BAAY;IAG5D;;;OAGG;IACO,kBAAkB;QAC1B,8CAA8C;QAC9C,+DAA+D;IACjE,CAAC;CACF;AAXD,kDAWC"}

View File

@@ -0,0 +1,28 @@
import type { IFallbackCommandItem, IFallbackHandler, ICommand, ContextItem, IconInfo } from '../types';
/**
* Base class for fallback command items that appear when the user types
* from the home page. These provide search/filter results as the user types.
*
* @example
* ```typescript
* class WebSearchFallback extends FallbackCommandItemBase {
* command = new WebSearchCommand()
* title = 'Search the web'
*
* updateQuery(query: string) {
* this.displayTitle = `Search for "${query}"`
* }
* }
* ```
*/
export declare abstract class FallbackCommandItemBase implements IFallbackCommandItem, IFallbackHandler {
abstract command: ICommand;
abstract title: string;
subtitle?: string;
icon?: IconInfo | null;
moreCommands?: ContextItem[];
displayTitle?: string;
get fallbackHandler(): IFallbackHandler;
abstract updateQuery(query: string): void;
}
//# sourceMappingURL=FallbackCommandItemBase.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FallbackCommandItemBase.d.ts","sourceRoot":"","sources":["../../src/base/FallbackCommandItemBase.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAEvG;;;;;;;;;;;;;;;GAeG;AACH,8BAAsB,uBAAwB,YAAW,oBAAoB,EAAE,gBAAgB;IAC7F,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IAEtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IACtB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAA;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,IAAI,eAAe,IAAI,gBAAgB,CAEtC;IAED,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAC1C"}

View File

@@ -0,0 +1,33 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.FallbackCommandItemBase = void 0;
/**
* Base class for fallback command items that appear when the user types
* from the home page. These provide search/filter results as the user types.
*
* @example
* ```typescript
* class WebSearchFallback extends FallbackCommandItemBase {
* command = new WebSearchCommand()
* title = 'Search the web'
*
* updateQuery(query: string) {
* this.displayTitle = `Search for "${query}"`
* }
* }
* ```
*/
class FallbackCommandItemBase {
subtitle;
icon;
moreCommands;
displayTitle;
get fallbackHandler() {
return this;
}
}
exports.FallbackCommandItemBase = FallbackCommandItemBase;
//# sourceMappingURL=FallbackCommandItemBase.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FallbackCommandItemBase.js","sourceRoot":"","sources":["../../src/base/FallbackCommandItemBase.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAIjE;;;;;;;;;;;;;;;GAeG;AACH,MAAsB,uBAAuB;IAI3C,QAAQ,CAAS;IACjB,IAAI,CAAkB;IACtB,YAAY,CAAgB;IAC5B,YAAY,CAAS;IAErB,IAAI,eAAe;QACjB,OAAO,IAAI,CAAA;IACb,CAAC;CAGF;AAdD,0DAcC"}

View File

@@ -0,0 +1,26 @@
import type { IInvokableCommand, CommandResult, IconInfo } from '../types';
/**
* Base class for commands that can be invoked (executed) by the user.
*
* @example
* ```typescript
* import { InvokableCommandBase } from '@microsoft/cmdpal-sdk';
*
* class CopyCommand extends InvokableCommandBase {
* id = 'copy-text';
* name = 'Copy to Clipboard';
*
* async invoke() {
* // Perform the action
* return { kind: 'showToast', args: { message: 'Copied!' } };
* }
* }
* ```
*/
export declare abstract class InvokableCommandBase implements IInvokableCommand {
abstract id: string;
abstract name: string;
icon?: IconInfo | null;
abstract invoke(): Promise<CommandResult> | CommandResult;
}
//# sourceMappingURL=InvokableCommandBase.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"InvokableCommandBase.d.ts","sourceRoot":"","sources":["../../src/base/InvokableCommandBase.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE3E;;;;;;;;;;;;;;;;;GAiBG;AACH,8BAAsB,oBAAqB,YAAW,iBAAiB;IACrE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAQ;IAE9B,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa;CAC1D"}

View File

@@ -0,0 +1,29 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.InvokableCommandBase = void 0;
/**
* Base class for commands that can be invoked (executed) by the user.
*
* @example
* ```typescript
* import { InvokableCommandBase } from '@microsoft/cmdpal-sdk';
*
* class CopyCommand extends InvokableCommandBase {
* id = 'copy-text';
* name = 'Copy to Clipboard';
*
* async invoke() {
* // Perform the action
* return { kind: 'showToast', args: { message: 'Copied!' } };
* }
* }
* ```
*/
class InvokableCommandBase {
icon = null;
}
exports.InvokableCommandBase = InvokableCommandBase;
//# sourceMappingURL=InvokableCommandBase.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"InvokableCommandBase.js","sourceRoot":"","sources":["../../src/base/InvokableCommandBase.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAIjE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAsB,oBAAoB;IAIxC,IAAI,GAAqB,IAAI,CAAC;CAG/B;AAPD,oDAOC"}

View File

@@ -0,0 +1,40 @@
import type { IListItem, ICommand, ContextItem, IconInfo, Tag, Details } from '../types';
/**
* Base class for list items displayed in list pages.
*
* @example
* ```typescript
* import { ListItemBase } from '@microsoft/cmdpal-sdk';
*
* const item = new ListItemBase({
* command: { id: 'open-file', name: 'Open File' },
* title: 'document.txt',
* subtitle: 'Modified 2 hours ago',
* tags: [{ text: 'Recent' }],
* section: 'Documents',
* });
* ```
*/
export declare class ListItemBase implements IListItem {
command: ICommand;
title: string;
subtitle?: string;
icon?: IconInfo | null;
moreCommands?: ContextItem[];
tags?: Tag[];
details?: Details;
section?: string;
textToSuggest?: string;
constructor(options: {
command: ICommand;
title: string;
subtitle?: string;
icon?: IconInfo | null;
moreCommands?: ContextItem[];
tags?: Tag[];
details?: Details;
section?: string;
textToSuggest?: string;
});
}
//# sourceMappingURL=ListItemBase.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ListItemBase.d.ts","sourceRoot":"","sources":["../../src/base/ListItemBase.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEzF;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,YAAa,YAAW,SAAS;IAC5C,OAAO,EAAE,QAAQ,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;gBAEX,OAAO,EAAE;QACnB,OAAO,EAAE,QAAQ,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;QACvB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;QAC7B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB;CAWF"}

View File

@@ -0,0 +1,46 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ListItemBase = void 0;
/**
* Base class for list items displayed in list pages.
*
* @example
* ```typescript
* import { ListItemBase } from '@microsoft/cmdpal-sdk';
*
* const item = new ListItemBase({
* command: { id: 'open-file', name: 'Open File' },
* title: 'document.txt',
* subtitle: 'Modified 2 hours ago',
* tags: [{ text: 'Recent' }],
* section: 'Documents',
* });
* ```
*/
class ListItemBase {
command;
title;
subtitle;
icon;
moreCommands;
tags;
details;
section;
textToSuggest;
constructor(options) {
this.command = options.command;
this.title = options.title;
this.subtitle = options.subtitle;
this.icon = options.icon;
this.moreCommands = options.moreCommands;
this.tags = options.tags;
this.details = options.details;
this.section = options.section;
this.textToSuggest = options.textToSuggest;
}
}
exports.ListItemBase = ListItemBase;
//# sourceMappingURL=ListItemBase.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ListItemBase.js","sourceRoot":"","sources":["../../src/base/ListItemBase.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAIjE;;;;;;;;;;;;;;;GAeG;AACH,MAAa,YAAY;IACvB,OAAO,CAAW;IAClB,KAAK,CAAS;IACd,QAAQ,CAAU;IAClB,IAAI,CAAmB;IACvB,YAAY,CAAiB;IAC7B,IAAI,CAAS;IACb,OAAO,CAAW;IAClB,OAAO,CAAU;IACjB,aAAa,CAAU;IAEvB,YAAY,OAUX;QACC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAC7C,CAAC;CACF;AAhCD,oCAgCC"}

View File

@@ -0,0 +1,40 @@
import type { IListPage, IListItem, IconInfo, OptionalColor, Filters, GridProperties, ICommandItem } from '../types';
/**
* Base class for list pages that display a filterable/searchable list of items.
*
* @example
* ```typescript
* import { ListPageBase, ListItemBase } from '@microsoft/cmdpal-sdk';
*
* class MyListPage extends ListPageBase {
* id = 'my-list';
* name = 'My List';
* title = 'My List Page';
* placeholderText = 'Search items...';
*
* getItems() {
* return [
* { command: { id: 'item-1', name: 'Item 1' }, title: 'Item 1', subtitle: 'Description' }
* ];
* }
* }
* ```
*/
export declare abstract class ListPageBase implements IListPage {
abstract id: string;
abstract name: string;
abstract title: string;
icon?: IconInfo | null;
isLoading?: boolean;
accentColor?: OptionalColor | null;
searchText?: string;
placeholderText?: string;
showDetails?: boolean;
filters?: Filters | null;
gridProperties?: GridProperties | null;
hasMoreItems?: boolean;
emptyContent?: ICommandItem | null;
abstract getItems(): Promise<IListItem[]> | IListItem[];
loadMore?(): Promise<void> | void;
}
//# sourceMappingURL=ListPageBase.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ListPageBase.d.ts","sourceRoot":"","sources":["../../src/base/ListPageBase.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,SAAS,EACT,QAAQ,EACR,aAAa,EACb,OAAO,EACP,cAAc,EACd,YAAY,EACb,MAAM,UAAU,CAAC;AAElB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,8BAAsB,YAAa,YAAW,SAAS;IACrD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAQ;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAS;IAC5B,WAAW,CAAC,EAAE,aAAa,GAAG,IAAI,CAAQ;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAM;IACzB,eAAe,CAAC,EAAE,MAAM,CAAM;IAC9B,WAAW,CAAC,EAAE,OAAO,CAAS;IAC9B,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAQ;IAChC,cAAc,CAAC,EAAE,cAAc,GAAG,IAAI,CAAQ;IAC9C,YAAY,CAAC,EAAE,OAAO,CAAS;IAC/B,YAAY,CAAC,EAAE,YAAY,GAAG,IAAI,CAAQ;IAE1C,QAAQ,CAAC,QAAQ,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,GAAG,SAAS,EAAE;IAEvD,QAAQ,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;CAGlC"}

View File

@@ -0,0 +1,44 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ListPageBase = void 0;
/**
* Base class for list pages that display a filterable/searchable list of items.
*
* @example
* ```typescript
* import { ListPageBase, ListItemBase } from '@microsoft/cmdpal-sdk';
*
* class MyListPage extends ListPageBase {
* id = 'my-list';
* name = 'My List';
* title = 'My List Page';
* placeholderText = 'Search items...';
*
* getItems() {
* return [
* { command: { id: 'item-1', name: 'Item 1' }, title: 'Item 1', subtitle: 'Description' }
* ];
* }
* }
* ```
*/
class ListPageBase {
icon = null;
isLoading = false;
accentColor = null;
searchText = '';
placeholderText = '';
showDetails = false;
filters = null;
gridProperties = null;
hasMoreItems = false;
emptyContent = null;
loadMore() {
// Override if pagination is supported
}
}
exports.ListPageBase = ListPageBase;
//# sourceMappingURL=ListPageBase.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ListPageBase.js","sourceRoot":"","sources":["../../src/base/ListPageBase.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAYjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAsB,YAAY;IAKhC,IAAI,GAAqB,IAAI,CAAC;IAC9B,SAAS,GAAa,KAAK,CAAC;IAC5B,WAAW,GAA0B,IAAI,CAAC;IAC1C,UAAU,GAAY,EAAE,CAAC;IACzB,eAAe,GAAY,EAAE,CAAC;IAC9B,WAAW,GAAa,KAAK,CAAC;IAC9B,OAAO,GAAoB,IAAI,CAAC;IAChC,cAAc,GAA2B,IAAI,CAAC;IAC9C,YAAY,GAAa,KAAK,CAAC;IAC/B,YAAY,GAAyB,IAAI,CAAC;IAI1C,QAAQ;QACN,sCAAsC;IACxC,CAAC;CACF;AArBD,oCAqBC"}

View File

@@ -0,0 +1,20 @@
import type { IListItem, ICommand, ContextItem, IconInfo, Tag, Details } from '../types';
/**
* A separator item that can be used in list pages as a visual divider.
* When placed in a list, it appears as a section header or divider line.
*/
export declare class Separator implements IListItem {
command: ICommand;
title: string;
subtitle?: string;
icon?: IconInfo | null;
moreCommands?: ContextItem[];
tags?: Tag[];
details?: Details;
section?: string;
textToSuggest?: string;
/** Marker for serialization to identify this as a separator */
readonly _isSeparator = true;
constructor(title?: string);
}
//# sourceMappingURL=Separator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"Separator.d.ts","sourceRoot":"","sources":["../../src/base/Separator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAExF;;;GAGG;AACH,qBAAa,SAAU,YAAW,SAAS;IACzC,OAAO,EAAE,QAAQ,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IACtB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAA;IAC5B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;IACZ,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IAEtB,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,QAAO;gBAEhB,KAAK,GAAE,MAAW;CAI/B"}

View File

@@ -0,0 +1,29 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.Separator = void 0;
/**
* A separator item that can be used in list pages as a visual divider.
* When placed in a list, it appears as a section header or divider line.
*/
class Separator {
command;
title;
subtitle;
icon;
moreCommands;
tags;
details;
section;
textToSuggest;
/** Marker for serialization to identify this as a separator */
_isSeparator = true;
constructor(title = '') {
this.title = title;
this.command = { id: `separator-${title}`, name: '' };
}
}
exports.Separator = Separator;
//# sourceMappingURL=Separator.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"Separator.js","sourceRoot":"","sources":["../../src/base/Separator.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAIjE;;;GAGG;AACH,MAAa,SAAS;IACpB,OAAO,CAAU;IACjB,KAAK,CAAQ;IACb,QAAQ,CAAS;IACjB,IAAI,CAAkB;IACtB,YAAY,CAAgB;IAC5B,IAAI,CAAQ;IACZ,OAAO,CAAU;IACjB,OAAO,CAAS;IAChB,aAAa,CAAS;IAEtB,+DAA+D;IACtD,YAAY,GAAG,IAAI,CAAA;IAE5B,YAAY,QAAgB,EAAE;QAC5B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,OAAO,GAAG,EAAE,EAAE,EAAE,aAAa,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;IACvD,CAAC;CACF;AAlBD,8BAkBC"}

View File

@@ -0,0 +1,69 @@
import type { ICommandSettings, IContentPage } from '../types';
/**
* A toggle (boolean) setting.
*/
export declare class ToggleSetting {
readonly key: string;
label: string;
description?: string;
value: boolean;
isRequired?: boolean;
constructor(key: string, label: string, defaultValue?: boolean, description?: string);
}
/**
* A text input setting.
*/
export declare class TextSetting {
readonly key: string;
label: string;
description?: string;
value: string;
placeholder?: string;
multiline?: boolean;
isRequired?: boolean;
constructor(key: string, label: string, defaultValue?: string, description?: string);
}
/**
* A choice set (dropdown) setting.
*/
export declare class ChoiceSetSetting {
readonly key: string;
label: string;
description?: string;
value: string;
choices: Array<{
title: string;
value: string;
}>;
isRequired?: boolean;
constructor(key: string, label: string, choices: Array<{
title: string;
value: string;
}>, defaultValue?: string, description?: string);
}
type AnySetting = ToggleSetting | TextSetting | ChoiceSetSetting;
/**
* Container for extension settings. Generates an Adaptive Card form for the settings page.
*/
export declare class Settings implements ICommandSettings {
private readonly _settings;
private _settingsPage?;
get settingsPage(): IContentPage;
add(setting: AnySetting): this;
getSetting<T extends AnySetting>(key: string): T | undefined;
getAllSettings(): AnySetting[];
/**
* Update settings from a form submission (key-value map from Adaptive Card inputs).
*/
update(inputs: Record<string, string>): void;
/**
* Generate the Adaptive Card template JSON for the settings form.
*/
toTemplateJson(): string;
/**
* Generate the data JSON (current values).
*/
toDataJson(): string;
}
export {};
//# sourceMappingURL=Settings.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"Settings.d.ts","sourceRoot":"","sources":["../../src/base/Settings.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EAQb,MAAM,UAAU,CAAA;AAEjB;;GAEG;AACH,qBAAa,aAAa;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,CAAC,EAAE,OAAO,CAAA;gBAER,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAE,OAAe,EAAE,WAAW,CAAC,EAAE,MAAM;CAM5F;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;gBAER,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAE,MAAW,EAAE,WAAW,CAAC,EAAE,MAAM;CAMxF;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAChD,UAAU,CAAC,EAAE,OAAO,CAAA;gBAGlB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,EAChD,YAAY,CAAC,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,MAAM;CAQvB;AAED,KAAK,UAAU,GAAG,aAAa,GAAG,WAAW,GAAG,gBAAgB,CAAA;AAEhE;;GAEG;AACH,qBAAa,QAAS,YAAW,gBAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,aAAa,CAAC,CAAc;IAEpC,IAAI,YAAY,IAAI,YAAY,CAK/B;IAED,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAK9B,UAAU,CAAC,CAAC,SAAS,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI5D,cAAc,IAAI,UAAU,EAAE;IAI9B;;OAEG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAY5C;;OAEG;IACH,cAAc,IAAI,MAAM;IAyDxB;;OAEG;IACH,UAAU,IAAI,MAAM;CAOrB"}

View File

@@ -0,0 +1,207 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.Settings = exports.ChoiceSetSetting = exports.TextSetting = exports.ToggleSetting = void 0;
/**
* A toggle (boolean) setting.
*/
class ToggleSetting {
key;
label;
description;
value;
isRequired;
constructor(key, label, defaultValue = false, description) {
this.key = key;
this.label = label;
this.value = defaultValue;
this.description = description;
}
}
exports.ToggleSetting = ToggleSetting;
/**
* A text input setting.
*/
class TextSetting {
key;
label;
description;
value;
placeholder;
multiline;
isRequired;
constructor(key, label, defaultValue = '', description) {
this.key = key;
this.label = label;
this.value = defaultValue;
this.description = description;
}
}
exports.TextSetting = TextSetting;
/**
* A choice set (dropdown) setting.
*/
class ChoiceSetSetting {
key;
label;
description;
value;
choices;
isRequired;
constructor(key, label, choices, defaultValue, description) {
this.key = key;
this.label = label;
this.choices = choices;
this.value = defaultValue ?? (choices.length > 0 ? choices[0].value : '');
this.description = description;
}
}
exports.ChoiceSetSetting = ChoiceSetSetting;
/**
* Container for extension settings. Generates an Adaptive Card form for the settings page.
*/
class Settings {
_settings = [];
_settingsPage;
get settingsPage() {
if (!this._settingsPage) {
this._settingsPage = new SettingsPage(this);
}
return this._settingsPage;
}
add(setting) {
this._settings.push(setting);
return this;
}
getSetting(key) {
return this._settings.find(s => s.key === key);
}
getAllSettings() {
return [...this._settings];
}
/**
* Update settings from a form submission (key-value map from Adaptive Card inputs).
*/
update(inputs) {
for (const setting of this._settings) {
if (inputs[setting.key] !== undefined) {
if (setting instanceof ToggleSetting) {
setting.value = inputs[setting.key] === 'true';
}
else {
setting.value = inputs[setting.key];
}
}
}
}
/**
* Generate the Adaptive Card template JSON for the settings form.
*/
toTemplateJson() {
const body = [];
for (const setting of this._settings) {
if (setting.label) {
body.push({
type: 'TextBlock',
text: setting.label,
weight: 'bolder',
spacing: 'medium',
});
}
if (setting.description) {
body.push({
type: 'TextBlock',
text: setting.description,
isSubtle: true,
wrap: true,
spacing: 'none',
});
}
if (setting instanceof ToggleSetting) {
body.push({
type: 'Input.Toggle',
id: setting.key,
title: '',
value: String(setting.value),
valueOn: 'true',
valueOff: 'false',
});
}
else if (setting instanceof TextSetting) {
body.push({
type: 'Input.Text',
id: setting.key,
placeholder: setting.placeholder ?? '',
value: setting.value,
isMultiline: setting.multiline ?? false,
});
}
else if (setting instanceof ChoiceSetSetting) {
body.push({
type: 'Input.ChoiceSet',
id: setting.key,
value: setting.value,
choices: setting.choices.map(c => ({ title: c.title, value: c.value })),
});
}
}
body.push({
type: 'ActionSet',
actions: [{ type: 'Action.Submit', title: 'Save' }],
});
return JSON.stringify({ type: 'AdaptiveCard', version: '1.5', body });
}
/**
* Generate the data JSON (current values).
*/
toDataJson() {
const data = {};
for (const setting of this._settings) {
data[setting.key] = String(setting.value);
}
return JSON.stringify(data);
}
}
exports.Settings = Settings;
/**
* Internal settings page that renders the settings form.
*/
class SettingsPage {
id = '__settings__';
name = 'Settings';
title = 'Extension Settings';
icon = null;
isLoading = false;
accentColor = null;
details = null;
commands = [];
settings;
constructor(settings) {
this.settings = settings;
}
getContent() {
const form = {
type: 'form',
templateJson: this.settings.toTemplateJson(),
dataJson: this.settings.toDataJson(),
submitForm: (inputs, _data) => {
let parsed = {};
try {
const p = JSON.parse(inputs);
if (p && typeof p === 'object') {
parsed = p;
}
}
catch {
// inputs was not valid JSON; keep empty
}
this.settings.update(parsed);
return { kind: 'showToast', args: { message: 'Settings saved' } };
},
};
return [form];
}
}
//# sourceMappingURL=Settings.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
import type { IInvokableCommand, CommandResult, IconInfo } from '../types';
/**
* A command that does nothing when invoked — returns KeepOpen.
*/
export declare class NoOpCommand implements IInvokableCommand {
id: string;
name: string;
icon?: IconInfo | null;
constructor(id?: string, name?: string);
invoke(): CommandResult;
}
/**
* A command that opens a URL when invoked.
*/
export declare class OpenUrlCommand implements IInvokableCommand {
id: string;
name: string;
icon?: IconInfo | null;
private readonly url;
constructor(url: string, name?: string);
invoke(): CommandResult;
getUrl(): string;
}
/**
* A command that copies text to clipboard and shows a toast notification.
*/
export declare class CopyTextCommand implements IInvokableCommand {
id: string;
name: string;
icon?: IconInfo | null;
private readonly text;
private readonly toastMessage;
constructor(text: string, name?: string, toastMessage?: string);
invoke(): CommandResult;
}
/**
* A command that wraps another command with a confirmation dialog.
*/
export declare class ConfirmableCommand implements IInvokableCommand {
id: string;
name: string;
icon?: IconInfo | null;
private readonly title;
private readonly description;
private readonly primaryCommand;
private readonly isCritical;
constructor(options: {
id: string;
name: string;
title: string;
description: string;
primaryCommand: IInvokableCommand;
isCritical?: boolean;
icon?: IconInfo | null;
});
invoke(): CommandResult;
}
//# sourceMappingURL=commands.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/base/commands.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAG1E;;GAEG;AACH,qBAAa,WAAY,YAAW,iBAAiB;IACnD,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;gBAEV,EAAE,GAAE,MAAe,EAAE,IAAI,GAAE,MAAW;IAKlD,MAAM,IAAI,aAAa;CAGxB;AAED;;GAEG;AACH,qBAAa,cAAe,YAAW,iBAAiB;IACtD,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;gBAEhB,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;IAMtC,MAAM,IAAI,aAAa;IAIvB,MAAM,IAAI,MAAM;CAGjB;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,iBAAiB;IACvD,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM;IAO9D,MAAM,IAAI,aAAa;CAIxB;AAED;;GAEG;AACH,qBAAa,kBAAmB,YAAW,iBAAiB;IAC1D,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IAEtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAmB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,OAAO,EAAE;QACnB,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;QACb,WAAW,EAAE,MAAM,CAAA;QACnB,cAAc,EAAE,iBAAiB,CAAA;QACjC,UAAU,CAAC,EAAE,OAAO,CAAA;QACpB,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;KACvB;IAUD,MAAM,IAAI,aAAa;CAWxB"}

View File

@@ -0,0 +1,99 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfirmableCommand = exports.CopyTextCommand = exports.OpenUrlCommand = exports.NoOpCommand = void 0;
const ExtensionHost_1 = require("../runtime/ExtensionHost");
/**
* A command that does nothing when invoked — returns KeepOpen.
*/
class NoOpCommand {
id;
name;
icon;
constructor(id = 'noop', name = '') {
this.id = id;
this.name = name;
}
invoke() {
return { kind: 'keepOpen' };
}
}
exports.NoOpCommand = NoOpCommand;
/**
* A command that opens a URL when invoked.
*/
class OpenUrlCommand {
id;
name;
icon;
url;
constructor(url, name) {
this.id = `open-url-${url}`;
this.name = name ?? url;
this.url = url;
}
invoke() {
return { kind: 'dismiss' };
}
getUrl() {
return this.url;
}
}
exports.OpenUrlCommand = OpenUrlCommand;
/**
* A command that copies text to clipboard and shows a toast notification.
*/
class CopyTextCommand {
id;
name;
icon;
text;
toastMessage;
constructor(text, name, toastMessage) {
this.id = `copy-text-${text.substring(0, 20)}`;
this.name = name ?? 'Copy';
this.text = text;
this.toastMessage = toastMessage ?? 'Copied to clipboard';
}
invoke() {
ExtensionHost_1.ExtensionHost.copyToClipboard(this.text);
return { kind: 'showToast', args: { message: this.toastMessage } };
}
}
exports.CopyTextCommand = CopyTextCommand;
/**
* A command that wraps another command with a confirmation dialog.
*/
class ConfirmableCommand {
id;
name;
icon;
title;
description;
primaryCommand;
isCritical;
constructor(options) {
this.id = options.id;
this.name = options.name;
this.title = options.title;
this.description = options.description;
this.primaryCommand = options.primaryCommand;
this.isCritical = options.isCritical ?? false;
this.icon = options.icon;
}
invoke() {
return {
kind: 'confirm',
args: {
title: this.title,
description: this.description,
primaryCommand: this.primaryCommand,
isPrimaryCommandCritical: this.isCritical,
},
};
}
}
exports.ConfirmableCommand = ConfirmableCommand;
//# sourceMappingURL=commands.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/base/commands.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAGjE,4DAAwD;AAExD;;GAEG;AACH,MAAa,WAAW;IACtB,EAAE,CAAQ;IACV,IAAI,CAAQ;IACZ,IAAI,CAAkB;IAEtB,YAAY,KAAa,MAAM,EAAE,OAAe,EAAE;QAChD,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;QACZ,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAA;IAC7B,CAAC;CACF;AAbD,kCAaC;AAED;;GAEG;AACH,MAAa,cAAc;IACzB,EAAE,CAAQ;IACV,IAAI,CAAQ;IACZ,IAAI,CAAkB;IACL,GAAG,CAAQ;IAE5B,YAAY,GAAW,EAAE,IAAa;QACpC,IAAI,CAAC,EAAE,GAAG,YAAY,GAAG,EAAE,CAAA;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,GAAG,CAAA;QACvB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;IAChB,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IAC5B,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,GAAG,CAAA;IACjB,CAAC;CACF;AAnBD,wCAmBC;AAED;;GAEG;AACH,MAAa,eAAe;IAC1B,EAAE,CAAQ;IACV,IAAI,CAAQ;IACZ,IAAI,CAAkB;IACL,IAAI,CAAQ;IACZ,YAAY,CAAQ;IAErC,YAAY,IAAY,EAAE,IAAa,EAAE,YAAqB;QAC5D,IAAI,CAAC,EAAE,GAAG,aAAa,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAA;QAC9C,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,MAAM,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,YAAY,GAAG,YAAY,IAAI,qBAAqB,CAAA;IAC3D,CAAC;IAED,MAAM;QACJ,6BAAa,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACxC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,EAAE,CAAA;IACpE,CAAC;CACF;AAlBD,0CAkBC;AAED;;GAEG;AACH,MAAa,kBAAkB;IAC7B,EAAE,CAAQ;IACV,IAAI,CAAQ;IACZ,IAAI,CAAkB;IAEL,KAAK,CAAQ;IACb,WAAW,CAAQ;IACnB,cAAc,CAAmB;IACjC,UAAU,CAAS;IAEpC,YAAY,OAQX;QACC,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAA;QACxB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;QAC1B,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAA;QACtC,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAA;QAC5C,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,KAAK,CAAA;QAC7C,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;IAED,MAAM;QACJ,OAAO;YACL,IAAI,EAAE,SAAS;YACf,IAAI,EAAE;gBACJ,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,cAAc,EAAE,IAAI,CAAC,cAAc;gBACnC,wBAAwB,EAAE,IAAI,CAAC,UAAU;aAC1C;SACF,CAAA;IACH,CAAC;CACF;AAvCD,gDAuCC"}

View File

@@ -0,0 +1,24 @@
/**
* Helper functions for creating icon data from various sources.
*/
import type { IconInfo } from './types';
/**
* Creates an IconInfo from a base64-encoded image string.
* The base64 string should be the raw base64 data (without data URI prefix).
*/
export declare function iconFromBase64(base64Data: string): IconInfo;
/**
* Creates an IconInfo from a font glyph character (e.g., '\uE91B' for Segoe MDL2/Fluent Icons).
*/
export declare function iconFromGlyph(glyph: string): IconInfo;
/**
* Creates an IconInfo by reading a local file and encoding it as base64.
* Supports common image formats: PNG, JPEG, BMP, GIF, ICO.
*/
export declare function iconFromFile(filePath: string): Promise<IconInfo>;
/**
* Creates an IconInfo by fetching an image from a URL and encoding it as base64.
* Uses Node.js built-in fetch (Node 18+).
*/
export declare function iconFromUrl(url: string): Promise<IconInfo>;
//# sourceMappingURL=helpers.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,SAAS,CAAC;AAElD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,CAG3D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAGrD;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAKtE;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAQhE"}

View File

@@ -0,0 +1,83 @@
"use strict";
/**
* Helper functions for creating icon data from various sources.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.iconFromBase64 = iconFromBase64;
exports.iconFromGlyph = iconFromGlyph;
exports.iconFromFile = iconFromFile;
exports.iconFromUrl = iconFromUrl;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* Creates an IconInfo from a base64-encoded image string.
* The base64 string should be the raw base64 data (without data URI prefix).
*/
function iconFromBase64(base64Data) {
const iconData = { data: base64Data };
return { light: iconData, dark: iconData };
}
/**
* Creates an IconInfo from a font glyph character (e.g., '\uE91B' for Segoe MDL2/Fluent Icons).
*/
function iconFromGlyph(glyph) {
const iconData = { icon: glyph };
return { light: iconData, dark: iconData };
}
/**
* Creates an IconInfo by reading a local file and encoding it as base64.
* Supports common image formats: PNG, JPEG, BMP, GIF, ICO.
*/
async function iconFromFile(filePath) {
const absolutePath = path.resolve(filePath);
const buffer = await fs.promises.readFile(absolutePath);
const base64Data = buffer.toString('base64');
return iconFromBase64(base64Data);
}
/**
* Creates an IconInfo by fetching an image from a URL and encoding it as base64.
* Uses Node.js built-in fetch (Node 18+).
*/
async function iconFromUrl(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch icon from ${url}: ${response.status} ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const base64Data = Buffer.from(arrayBuffer).toString('base64');
return iconFromBase64(base64Data);
}
//# sourceMappingURL=helpers.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":";AAAA;;GAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUH,wCAGC;AAKD,sCAGC;AAMD,oCAKC;AAMD,kCAQC;AA5CD,uCAAyB;AACzB,2CAA6B;AAG7B;;;GAGG;AACH,SAAgB,cAAc,CAAC,UAAkB;IAC/C,MAAM,QAAQ,GAAa,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAChD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC7C,CAAC;AAED;;GAEG;AACH,SAAgB,aAAa,CAAC,KAAa;IACzC,MAAM,QAAQ,GAAa,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC3C,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACxD,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7C,OAAO,cAAc,CAAC,UAAU,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,WAAW,CAAC,GAAW;IAC3C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,KAAK,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACjG,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;IACjD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC/D,OAAO,cAAc,CAAC,UAAU,CAAC,CAAC;AACpC,CAAC"}

View File

@@ -0,0 +1,24 @@
/**
* @microsoft/cmdpal-sdk
*
* TypeScript SDK for building Command Palette extensions.
* Provides base classes, type definitions, and JSONRPC runtime bridge.
*/
export type { IconData, IconInfo, Color, OptionalColor, Tag, KeyChord, Details, DetailsElement, DetailsData, DetailsTags, DetailsLink, DetailsCommands, DetailsSeparator, Filter, Filters, GridProperties, CommandResult, CommandResultArgs, GoToPageArgs, ToastArgs, ConfirmationArgs, ContextItem, Content, MarkdownContent, FormContent, TreeContent, PlainTextContent, ImageContent, ProgressState, StatusMessage, ActivationContext, ICommand, IInvokableCommand, ICommandItem, IListItem, IFallbackCommandItem, IFallbackHandler, ICommandProvider, ICommandSettings, IPage, IListPage, IDynamicListPage, IContentPage, IExtensionHost, } from './types';
export { type CommandResultKind, type NavigationMode, type MessageState, type StatusContext, type ContentType, type GridLayoutType, type FontFamily, } from './types';
export { CommandProviderBase } from './base/CommandProviderBase';
export { ListPageBase } from './base/ListPageBase';
export { DynamicListPageBase } from './base/DynamicListPageBase';
export { ContentPageBase } from './base/ContentPageBase';
export { InvokableCommandBase } from './base/InvokableCommandBase';
export { CommandItemBase } from './base/CommandItemBase';
export { ListItemBase } from './base/ListItemBase';
export { FallbackCommandItemBase } from './base/FallbackCommandItemBase';
export { Separator } from './base/Separator';
export { NoOpCommand, OpenUrlCommand, CopyTextCommand, ConfirmableCommand } from './base/commands';
export { Settings, ToggleSetting, TextSetting, ChoiceSetSetting } from './base/Settings';
export { ExtensionHost } from './runtime/ExtensionHost';
export { activate } from './runtime/activate';
export { startJsonRpcServer, sendNotification } from './runtime/stdio-server';
export { iconFromBase64, iconFromGlyph, iconFromFile, iconFromUrl } from './helpers';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAGH,YAAY,EACV,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,aAAa,EACb,GAAG,EACH,QAAQ,EACR,OAAO,EACP,cAAc,EACd,WAAW,EACX,WAAW,EACX,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,MAAM,EACN,OAAO,EACP,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,eAAe,EACf,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,QAAQ,EACR,iBAAiB,EACjB,YAAY,EACZ,SAAS,EACT,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,EACL,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,cAAc,GACf,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,GAChB,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACnG,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGzF,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG9E,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC"}

50
src/modules/cmdpal/ts-sdk/dist/index.js vendored Normal file
View File

@@ -0,0 +1,50 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.iconFromUrl = exports.iconFromFile = exports.iconFromGlyph = exports.iconFromBase64 = exports.sendNotification = exports.startJsonRpcServer = exports.activate = exports.ExtensionHost = exports.ChoiceSetSetting = exports.TextSetting = exports.ToggleSetting = exports.Settings = exports.ConfirmableCommand = exports.CopyTextCommand = exports.OpenUrlCommand = exports.NoOpCommand = exports.Separator = exports.FallbackCommandItemBase = exports.ListItemBase = exports.CommandItemBase = exports.InvokableCommandBase = exports.ContentPageBase = exports.DynamicListPageBase = exports.ListPageBase = exports.CommandProviderBase = void 0;
// Base classes
var CommandProviderBase_1 = require("./base/CommandProviderBase");
Object.defineProperty(exports, "CommandProviderBase", { enumerable: true, get: function () { return CommandProviderBase_1.CommandProviderBase; } });
var ListPageBase_1 = require("./base/ListPageBase");
Object.defineProperty(exports, "ListPageBase", { enumerable: true, get: function () { return ListPageBase_1.ListPageBase; } });
var DynamicListPageBase_1 = require("./base/DynamicListPageBase");
Object.defineProperty(exports, "DynamicListPageBase", { enumerable: true, get: function () { return DynamicListPageBase_1.DynamicListPageBase; } });
var ContentPageBase_1 = require("./base/ContentPageBase");
Object.defineProperty(exports, "ContentPageBase", { enumerable: true, get: function () { return ContentPageBase_1.ContentPageBase; } });
var InvokableCommandBase_1 = require("./base/InvokableCommandBase");
Object.defineProperty(exports, "InvokableCommandBase", { enumerable: true, get: function () { return InvokableCommandBase_1.InvokableCommandBase; } });
var CommandItemBase_1 = require("./base/CommandItemBase");
Object.defineProperty(exports, "CommandItemBase", { enumerable: true, get: function () { return CommandItemBase_1.CommandItemBase; } });
var ListItemBase_1 = require("./base/ListItemBase");
Object.defineProperty(exports, "ListItemBase", { enumerable: true, get: function () { return ListItemBase_1.ListItemBase; } });
var FallbackCommandItemBase_1 = require("./base/FallbackCommandItemBase");
Object.defineProperty(exports, "FallbackCommandItemBase", { enumerable: true, get: function () { return FallbackCommandItemBase_1.FallbackCommandItemBase; } });
var Separator_1 = require("./base/Separator");
Object.defineProperty(exports, "Separator", { enumerable: true, get: function () { return Separator_1.Separator; } });
var commands_1 = require("./base/commands");
Object.defineProperty(exports, "NoOpCommand", { enumerable: true, get: function () { return commands_1.NoOpCommand; } });
Object.defineProperty(exports, "OpenUrlCommand", { enumerable: true, get: function () { return commands_1.OpenUrlCommand; } });
Object.defineProperty(exports, "CopyTextCommand", { enumerable: true, get: function () { return commands_1.CopyTextCommand; } });
Object.defineProperty(exports, "ConfirmableCommand", { enumerable: true, get: function () { return commands_1.ConfirmableCommand; } });
var Settings_1 = require("./base/Settings");
Object.defineProperty(exports, "Settings", { enumerable: true, get: function () { return Settings_1.Settings; } });
Object.defineProperty(exports, "ToggleSetting", { enumerable: true, get: function () { return Settings_1.ToggleSetting; } });
Object.defineProperty(exports, "TextSetting", { enumerable: true, get: function () { return Settings_1.TextSetting; } });
Object.defineProperty(exports, "ChoiceSetSetting", { enumerable: true, get: function () { return Settings_1.ChoiceSetSetting; } });
// Runtime
var ExtensionHost_1 = require("./runtime/ExtensionHost");
Object.defineProperty(exports, "ExtensionHost", { enumerable: true, get: function () { return ExtensionHost_1.ExtensionHost; } });
var activate_1 = require("./runtime/activate");
Object.defineProperty(exports, "activate", { enumerable: true, get: function () { return activate_1.activate; } });
var stdio_server_1 = require("./runtime/stdio-server");
Object.defineProperty(exports, "startJsonRpcServer", { enumerable: true, get: function () { return stdio_server_1.startJsonRpcServer; } });
Object.defineProperty(exports, "sendNotification", { enumerable: true, get: function () { return stdio_server_1.sendNotification; } });
// Icon helpers
var helpers_1 = require("./helpers");
Object.defineProperty(exports, "iconFromBase64", { enumerable: true, get: function () { return helpers_1.iconFromBase64; } });
Object.defineProperty(exports, "iconFromGlyph", { enumerable: true, get: function () { return helpers_1.iconFromGlyph; } });
Object.defineProperty(exports, "iconFromFile", { enumerable: true, get: function () { return helpers_1.iconFromFile; } });
Object.defineProperty(exports, "iconFromUrl", { enumerable: true, get: function () { return helpers_1.iconFromUrl; } });
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAmEjE,eAAe;AACf,kEAAiE;AAAxD,0HAAA,mBAAmB,OAAA;AAC5B,oDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,kEAAiE;AAAxD,0HAAA,mBAAmB,OAAA;AAC5B,0DAAyD;AAAhD,kHAAA,eAAe,OAAA;AACxB,oEAAmE;AAA1D,4HAAA,oBAAoB,OAAA;AAC7B,0DAAyD;AAAhD,kHAAA,eAAe,OAAA;AACxB,oDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,0EAAyE;AAAhE,kIAAA,uBAAuB,OAAA;AAChC,8CAA6C;AAApC,sGAAA,SAAS,OAAA;AAClB,4CAAmG;AAA1F,uGAAA,WAAW,OAAA;AAAE,0GAAA,cAAc,OAAA;AAAE,2GAAA,eAAe,OAAA;AAAE,8GAAA,kBAAkB,OAAA;AACzE,4CAAyF;AAAhF,oGAAA,QAAQ,OAAA;AAAE,yGAAA,aAAa,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,4GAAA,gBAAgB,OAAA;AAE/D,UAAU;AACV,yDAAwD;AAA/C,8GAAA,aAAa,OAAA;AACtB,+CAA8C;AAArC,oGAAA,QAAQ,OAAA;AACjB,uDAA8E;AAArE,kHAAA,kBAAkB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAE7C,eAAe;AACf,qCAAqF;AAA5E,yGAAA,cAAc,OAAA;AAAE,wGAAA,aAAa,OAAA;AAAE,uGAAA,YAAY,OAAA;AAAE,sGAAA,WAAW,OAAA"}

View File

@@ -0,0 +1,44 @@
import type { IExtensionHost, MessageState, ProgressState } from '../types';
/**
* Provides access to the Command Palette host from within an extension.
* Use this to send log messages, show/hide status indicators, copy text, etc.
*
* @example
* ```typescript
* import { ExtensionHost } from '@microsoft/cmdpal-sdk';
*
* ExtensionHost.log('Extension loaded successfully');
* ExtensionHost.showStatus('Loading data...', 'info', { isIndeterminate: true });
* ExtensionHost.copyToClipboard('Hello, world!');
* ```
*/
export declare class ExtensionHost {
private static _instance;
/**
* Initializes the ExtensionHost with the host provided during activation.
* This is called automatically by the runtime bridge — do not call directly.
* @internal
*/
static initialize(host: IExtensionHost): void;
/**
* Sends a log message to the Command Palette host.
*/
static log(message: string, state?: MessageState): void;
/**
* Shows a status message in the Command Palette UI.
*/
static showStatus(message: string, state?: MessageState, progress?: ProgressState): void;
/**
* Hides a previously shown status message.
*/
static hideStatus(messageId: string): void;
/**
* Copies text to the system clipboard via the host.
*/
static copyToClipboard(text: string): void;
/**
* Gets whether the host has been initialized.
*/
static get isInitialized(): boolean;
}
//# sourceMappingURL=ExtensionHost.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExtensionHost.d.ts","sourceRoot":"","sources":["../../src/runtime/ExtensionHost.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE5E;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAC,SAAS,CAA+B;IAEvD;;;;OAIG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAI7C;;OAEG;IACH,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,YAAqB,GAAG,IAAI;IAI/D;;OAEG;IACH,MAAM,CAAC,UAAU,CACf,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,YAAqB,EAC5B,QAAQ,CAAC,EAAE,aAAa,GACvB,IAAI;IAIP;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI1C;;OAEG;IACH,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1C;;OAEG;IACH,MAAM,KAAK,aAAa,IAAI,OAAO,CAElC;CACF"}

View File

@@ -0,0 +1,62 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExtensionHost = void 0;
/**
* Provides access to the Command Palette host from within an extension.
* Use this to send log messages, show/hide status indicators, copy text, etc.
*
* @example
* ```typescript
* import { ExtensionHost } from '@microsoft/cmdpal-sdk';
*
* ExtensionHost.log('Extension loaded successfully');
* ExtensionHost.showStatus('Loading data...', 'info', { isIndeterminate: true });
* ExtensionHost.copyToClipboard('Hello, world!');
* ```
*/
class ExtensionHost {
static _instance = null;
/**
* Initializes the ExtensionHost with the host provided during activation.
* This is called automatically by the runtime bridge — do not call directly.
* @internal
*/
static initialize(host) {
ExtensionHost._instance = host;
}
/**
* Sends a log message to the Command Palette host.
*/
static log(message, state = 'info') {
ExtensionHost._instance?.log(message, state);
}
/**
* Shows a status message in the Command Palette UI.
*/
static showStatus(message, state = 'info', progress) {
ExtensionHost._instance?.showStatus(message, state, progress);
}
/**
* Hides a previously shown status message.
*/
static hideStatus(messageId) {
ExtensionHost._instance?.hideStatus(messageId);
}
/**
* Copies text to the system clipboard via the host.
*/
static copyToClipboard(text) {
ExtensionHost._instance?.copyToClipboard(text);
}
/**
* Gets whether the host has been initialized.
*/
static get isInitialized() {
return ExtensionHost._instance !== null;
}
}
exports.ExtensionHost = ExtensionHost;
//# sourceMappingURL=ExtensionHost.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExtensionHost.js","sourceRoot":"","sources":["../../src/runtime/ExtensionHost.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;;AAIjE;;;;;;;;;;;;GAYG;AACH,MAAa,aAAa;IAChB,MAAM,CAAC,SAAS,GAA0B,IAAI,CAAC;IAEvD;;;;OAIG;IACH,MAAM,CAAC,UAAU,CAAC,IAAoB;QACpC,aAAa,CAAC,SAAS,GAAG,IAAI,CAAC;IACjC,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,GAAG,CAAC,OAAe,EAAE,QAAsB,MAAM;QACtD,aAAa,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,UAAU,CACf,OAAe,EACf,QAAsB,MAAM,EAC5B,QAAwB;QAExB,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IAChE,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,UAAU,CAAC,SAAiB;QACjC,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,eAAe,CAAC,IAAY;QACjC,aAAa,CAAC,SAAS,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC;IAED;;OAEG;IACH,MAAM,KAAK,aAAa;QACtB,OAAO,aAAa,CAAC,SAAS,KAAK,IAAI,CAAC;IAC1C,CAAC;;AAjDH,sCAkDC"}

View File

@@ -0,0 +1,23 @@
import type { ICommandProvider, ActivationContext } from '../types';
/**
* Helper function for extension activation.
* Wraps a provider factory with automatic ExtensionHost initialization.
*
* @example
* ```typescript
* // In your extension's index.ts:
* import { activate as sdkActivate, CommandProviderBase } from '@microsoft/cmdpal-sdk';
*
* class MyProvider extends CommandProviderBase {
* id = 'my-ext';
* displayName = 'My Extension';
* topLevelCommands() { return []; }
* }
*
* export function activate(context: ActivationContext) {
* return sdkActivate(context, () => new MyProvider());
* }
* ```
*/
export declare function activate(context: ActivationContext, providerFactory: () => ICommandProvider | Promise<ICommandProvider>): ICommandProvider | Promise<ICommandProvider>;
//# sourceMappingURL=activate.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"activate.d.ts","sourceRoot":"","sources":["../../src/runtime/activate.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAGpE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,iBAAiB,EAC1B,eAAe,EAAE,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,GAClE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAI9C"}

View File

@@ -0,0 +1,32 @@
"use strict";
// 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.
Object.defineProperty(exports, "__esModule", { value: true });
exports.activate = activate;
/**
* Helper function for extension activation.
* Wraps a provider factory with automatic ExtensionHost initialization.
*
* @example
* ```typescript
* // In your extension's index.ts:
* import { activate as sdkActivate, CommandProviderBase } from '@microsoft/cmdpal-sdk';
*
* class MyProvider extends CommandProviderBase {
* id = 'my-ext';
* displayName = 'My Extension';
* topLevelCommands() { return []; }
* }
*
* export function activate(context: ActivationContext) {
* return sdkActivate(context, () => new MyProvider());
* }
* ```
*/
function activate(context, providerFactory) {
// The host will be initialized by the Node host process when it calls
// initializeWithHost on the provider. This is a convenience wrapper.
return providerFactory();
}
//# sourceMappingURL=activate.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"activate.js","sourceRoot":"","sources":["../../src/runtime/activate.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,6EAA6E;AAC7E,iEAAiE;;AAyBjE,4BAOC;AA3BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,QAAQ,CACtB,OAA0B,EAC1B,eAAmE;IAEnE,sEAAsE;IACtE,qEAAqE;IACrE,OAAO,eAAe,EAAE,CAAC;AAC3B,CAAC"}

Some files were not shown because too many files have changed in this diff Show More