mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-01 16:09:46 +02:00
Compare commits
12 Commits
main
...
dev/mjolle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3924e04578 | ||
|
|
34bdb12cc5 | ||
|
|
734a50ee97 | ||
|
|
c953f84fa3 | ||
|
|
a8d699b145 | ||
|
|
d2cee3a497 | ||
|
|
757f60d719 | ||
|
|
438507cdd4 | ||
|
|
89cf4e3115 | ||
|
|
59bc58afe2 | ||
|
|
e22bbd05a5 | ||
|
|
6b0779a7c5 |
@@ -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);
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
67
src/modules/cmdpal/TestDeserialize.cs
Normal file
67
src/modules/cmdpal/TestDeserialize.cs
Normal 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}");
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
424
src/modules/cmdpal/doc/js-extensions/extension-authoring-js.md
Normal file
424
src/modules/cmdpal/doc/js-extensions/extension-authoring-js.md
Normal 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.
|
||||
652
src/modules/cmdpal/doc/js-extensions/jsonrpc-protocol.md
Normal file
652
src/modules/cmdpal/doc/js-extensions/jsonrpc-protocol.md
Normal 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 ..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
2
src/modules/cmdpal/ext/SampleJSExtension/dist/index.d.ts
vendored
Normal file
2
src/modules/cmdpal/ext/SampleJSExtension/dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { type ActivationContext, type ICommandProvider } from '@microsoft/cmdpal-sdk';
|
||||
export declare function activate(context: ActivationContext): ICommandProvider;
|
||||
1031
src/modules/cmdpal/ext/SampleJSExtension/dist/index.js
vendored
Normal file
1031
src/modules/cmdpal/ext/SampleJSExtension/dist/index.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
51
src/modules/cmdpal/ext/SampleJSExtension/package-lock.json
generated
Normal file
51
src/modules/cmdpal/ext/SampleJSExtension/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/modules/cmdpal/ext/SampleJSExtension/package.json
Normal file
26
src/modules/cmdpal/ext/SampleJSExtension/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1282
src/modules/cmdpal/ext/SampleJSExtension/src/index.ts
Normal file
1282
src/modules/cmdpal/ext/SampleJSExtension/src/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
16
src/modules/cmdpal/ext/SampleJSExtension/tsconfig.json
Normal file
16
src/modules/cmdpal/ext/SampleJSExtension/tsconfig.json
Normal 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"]
|
||||
}
|
||||
68
src/modules/cmdpal/setup-dev-extensions.ps1
Normal file
68
src/modules/cmdpal/setup-dev-extensions.ps1
Normal 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)."
|
||||
115
src/modules/cmdpal/ts-sdk/README.md
Normal file
115
src/modules/cmdpal/ts-sdk/README.md
Normal 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 });
|
||||
```
|
||||
19
src/modules/cmdpal/ts-sdk/dist/base/CommandItemBase.d.ts
vendored
Normal file
19
src/modules/cmdpal/ts-sdk/dist/base/CommandItemBase.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/CommandItemBase.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/CommandItemBase.d.ts.map
vendored
Normal 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"}
|
||||
25
src/modules/cmdpal/ts-sdk/dist/base/CommandItemBase.js
vendored
Normal file
25
src/modules/cmdpal/ts-sdk/dist/base/CommandItemBase.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/CommandItemBase.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/CommandItemBase.js.map
vendored
Normal 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"}
|
||||
33
src/modules/cmdpal/ts-sdk/dist/base/CommandProviderBase.d.ts
vendored
Normal file
33
src/modules/cmdpal/ts-sdk/dist/base/CommandProviderBase.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/CommandProviderBase.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/CommandProviderBase.d.ts.map
vendored
Normal 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"}
|
||||
44
src/modules/cmdpal/ts-sdk/dist/base/CommandProviderBase.js
vendored
Normal file
44
src/modules/cmdpal/ts-sdk/dist/base/CommandProviderBase.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/CommandProviderBase.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/CommandProviderBase.js.map
vendored
Normal 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"}
|
||||
33
src/modules/cmdpal/ts-sdk/dist/base/ContentPageBase.d.ts
vendored
Normal file
33
src/modules/cmdpal/ts-sdk/dist/base/ContentPageBase.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/ContentPageBase.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/ContentPageBase.d.ts.map
vendored
Normal 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"}
|
||||
35
src/modules/cmdpal/ts-sdk/dist/base/ContentPageBase.js
vendored
Normal file
35
src/modules/cmdpal/ts-sdk/dist/base/ContentPageBase.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/ContentPageBase.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/ContentPageBase.js.map
vendored
Normal 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"}
|
||||
36
src/modules/cmdpal/ts-sdk/dist/base/DynamicListPageBase.d.ts
vendored
Normal file
36
src/modules/cmdpal/ts-sdk/dist/base/DynamicListPageBase.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/DynamicListPageBase.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/DynamicListPageBase.d.ts.map
vendored
Normal 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"}
|
||||
44
src/modules/cmdpal/ts-sdk/dist/base/DynamicListPageBase.js
vendored
Normal file
44
src/modules/cmdpal/ts-sdk/dist/base/DynamicListPageBase.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/DynamicListPageBase.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/DynamicListPageBase.js.map
vendored
Normal 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"}
|
||||
28
src/modules/cmdpal/ts-sdk/dist/base/FallbackCommandItemBase.d.ts
vendored
Normal file
28
src/modules/cmdpal/ts-sdk/dist/base/FallbackCommandItemBase.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/FallbackCommandItemBase.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/FallbackCommandItemBase.d.ts.map
vendored
Normal 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"}
|
||||
33
src/modules/cmdpal/ts-sdk/dist/base/FallbackCommandItemBase.js
vendored
Normal file
33
src/modules/cmdpal/ts-sdk/dist/base/FallbackCommandItemBase.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/FallbackCommandItemBase.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/FallbackCommandItemBase.js.map
vendored
Normal 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"}
|
||||
26
src/modules/cmdpal/ts-sdk/dist/base/InvokableCommandBase.d.ts
vendored
Normal file
26
src/modules/cmdpal/ts-sdk/dist/base/InvokableCommandBase.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/InvokableCommandBase.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/InvokableCommandBase.d.ts.map
vendored
Normal 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"}
|
||||
29
src/modules/cmdpal/ts-sdk/dist/base/InvokableCommandBase.js
vendored
Normal file
29
src/modules/cmdpal/ts-sdk/dist/base/InvokableCommandBase.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/InvokableCommandBase.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/InvokableCommandBase.js.map
vendored
Normal 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"}
|
||||
40
src/modules/cmdpal/ts-sdk/dist/base/ListItemBase.d.ts
vendored
Normal file
40
src/modules/cmdpal/ts-sdk/dist/base/ListItemBase.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/ListItemBase.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/ListItemBase.d.ts.map
vendored
Normal 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"}
|
||||
46
src/modules/cmdpal/ts-sdk/dist/base/ListItemBase.js
vendored
Normal file
46
src/modules/cmdpal/ts-sdk/dist/base/ListItemBase.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/ListItemBase.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/ListItemBase.js.map
vendored
Normal 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"}
|
||||
40
src/modules/cmdpal/ts-sdk/dist/base/ListPageBase.d.ts
vendored
Normal file
40
src/modules/cmdpal/ts-sdk/dist/base/ListPageBase.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/ListPageBase.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/ListPageBase.d.ts.map
vendored
Normal 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"}
|
||||
44
src/modules/cmdpal/ts-sdk/dist/base/ListPageBase.js
vendored
Normal file
44
src/modules/cmdpal/ts-sdk/dist/base/ListPageBase.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/ListPageBase.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/ListPageBase.js.map
vendored
Normal 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"}
|
||||
20
src/modules/cmdpal/ts-sdk/dist/base/Separator.d.ts
vendored
Normal file
20
src/modules/cmdpal/ts-sdk/dist/base/Separator.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/Separator.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/Separator.d.ts.map
vendored
Normal 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"}
|
||||
29
src/modules/cmdpal/ts-sdk/dist/base/Separator.js
vendored
Normal file
29
src/modules/cmdpal/ts-sdk/dist/base/Separator.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/Separator.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/Separator.js.map
vendored
Normal 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"}
|
||||
69
src/modules/cmdpal/ts-sdk/dist/base/Settings.d.ts
vendored
Normal file
69
src/modules/cmdpal/ts-sdk/dist/base/Settings.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/Settings.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/Settings.d.ts.map
vendored
Normal 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"}
|
||||
207
src/modules/cmdpal/ts-sdk/dist/base/Settings.js
vendored
Normal file
207
src/modules/cmdpal/ts-sdk/dist/base/Settings.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/Settings.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/Settings.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
58
src/modules/cmdpal/ts-sdk/dist/base/commands.d.ts
vendored
Normal file
58
src/modules/cmdpal/ts-sdk/dist/base/commands.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/commands.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/commands.d.ts.map
vendored
Normal 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"}
|
||||
99
src/modules/cmdpal/ts-sdk/dist/base/commands.js
vendored
Normal file
99
src/modules/cmdpal/ts-sdk/dist/base/commands.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/base/commands.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/base/commands.js.map
vendored
Normal 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"}
|
||||
24
src/modules/cmdpal/ts-sdk/dist/helpers.d.ts
vendored
Normal file
24
src/modules/cmdpal/ts-sdk/dist/helpers.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/helpers.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/helpers.d.ts.map
vendored
Normal 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"}
|
||||
83
src/modules/cmdpal/ts-sdk/dist/helpers.js
vendored
Normal file
83
src/modules/cmdpal/ts-sdk/dist/helpers.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/helpers.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/helpers.js.map
vendored
Normal 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"}
|
||||
24
src/modules/cmdpal/ts-sdk/dist/index.d.ts
vendored
Normal file
24
src/modules/cmdpal/ts-sdk/dist/index.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/index.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/index.d.ts.map
vendored
Normal 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
50
src/modules/cmdpal/ts-sdk/dist/index.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/index.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/index.js.map
vendored
Normal 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"}
|
||||
44
src/modules/cmdpal/ts-sdk/dist/runtime/ExtensionHost.d.ts
vendored
Normal file
44
src/modules/cmdpal/ts-sdk/dist/runtime/ExtensionHost.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/runtime/ExtensionHost.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/runtime/ExtensionHost.d.ts.map
vendored
Normal 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"}
|
||||
62
src/modules/cmdpal/ts-sdk/dist/runtime/ExtensionHost.js
vendored
Normal file
62
src/modules/cmdpal/ts-sdk/dist/runtime/ExtensionHost.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/runtime/ExtensionHost.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/runtime/ExtensionHost.js.map
vendored
Normal 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"}
|
||||
23
src/modules/cmdpal/ts-sdk/dist/runtime/activate.d.ts
vendored
Normal file
23
src/modules/cmdpal/ts-sdk/dist/runtime/activate.d.ts
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/runtime/activate.d.ts.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/runtime/activate.d.ts.map
vendored
Normal 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"}
|
||||
32
src/modules/cmdpal/ts-sdk/dist/runtime/activate.js
vendored
Normal file
32
src/modules/cmdpal/ts-sdk/dist/runtime/activate.js
vendored
Normal 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
|
||||
1
src/modules/cmdpal/ts-sdk/dist/runtime/activate.js.map
vendored
Normal file
1
src/modules/cmdpal/ts-sdk/dist/runtime/activate.js.map
vendored
Normal 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
Reference in New Issue
Block a user