diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Helpers/PathHelper.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Helpers/PathHelper.cs
new file mode 100644
index 0000000000..75cfcac444
--- /dev/null
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Helpers/PathHelper.cs
@@ -0,0 +1,153 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+using Windows.Win32;
+using Windows.Win32.Storage.FileSystem;
+
+namespace Microsoft.CmdPal.Core.Common.Helpers;
+
+public static class PathHelper
+{
+ public static bool Exists(string path, out bool isDirectory)
+ {
+ isDirectory = false;
+ if (string.IsNullOrEmpty(path))
+ {
+ return false;
+ }
+
+ string? fullPath;
+ try
+ {
+ fullPath = Path.GetFullPath(path);
+ }
+ catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException)
+ {
+ return false;
+ }
+
+ var result = ExistsCore(fullPath, out isDirectory);
+ if (result && IsDirectorySeparator(fullPath[^1]))
+ {
+ // Some sys-calls remove all trailing slashes and may give false positives for existing files.
+ // We want to make sure that if the path ends in a trailing slash, it's truly a directory.
+ return isDirectory;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Normalize potential local/UNC file path text input: trim whitespace and surrounding quotes.
+ /// Windows file paths cannot contain quotes, but user input can include them.
+ ///
+ public static string Unquote(string? text)
+ {
+ return string.IsNullOrWhiteSpace(text) ? (text ?? string.Empty) : text.Trim().Trim('"');
+ }
+
+ ///
+ /// Quick heuristic to determine if the string looks like a Windows file path (UNC or drive-letter based).
+ ///
+ public static bool LooksLikeFilePath(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return false;
+ }
+
+ // UNC path
+ if (path.StartsWith(@"\\", StringComparison.Ordinal))
+ {
+ // Win32 File Namespaces \\?\
+ if (path.StartsWith(@"\\?\", StringComparison.Ordinal))
+ {
+ return IsSlow(path[4..]);
+ }
+
+ // Basic UNC path validation: \\server\share or \\server\share\path
+ var parts = path[2..].Split('\\', StringSplitOptions.RemoveEmptyEntries);
+
+ return parts.Length >= 2; // At minimum: server and share
+ }
+
+ // Drive letter path (e.g., C:\ or C:)
+ return path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':';
+ }
+
+ ///
+ /// Validates path syntax without performing any I/O by using Path.GetFullPath.
+ ///
+ public static bool HasValidPathSyntax(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return false;
+ }
+
+ try
+ {
+ _ = Path.GetFullPath(path);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Checks if a string represents a valid Windows file path (local or network)
+ /// using fast syntax validation only. Reuses LooksLikeFilePath and HasValidPathSyntax.
+ ///
+ public static bool IsValidFilePath(string? path)
+ {
+ return LooksLikeFilePath(path) && HasValidPathSyntax(path);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsDirectorySeparator(char c)
+ {
+ return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
+ }
+
+ private static bool ExistsCore(string fullPath, out bool isDirectory)
+ {
+ var attributes = PInvoke.GetFileAttributes(fullPath);
+ var result = attributes != PInvoke.INVALID_FILE_ATTRIBUTES;
+ isDirectory = result && (attributes & (uint)FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY) != 0;
+ return result;
+ }
+
+ public static bool IsSlow(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ return false;
+ }
+
+ try
+ {
+ var root = Path.GetPathRoot(path);
+ if (!string.IsNullOrEmpty(root))
+ {
+ if (root.Length > 2 && char.IsLetter(root[0]) && root[1] == ':')
+ {
+ return new DriveInfo(root).DriveType is not (DriveType.Fixed or DriveType.Ram);
+ }
+ else if (root.StartsWith(@"\\", StringComparison.Ordinal))
+ {
+ return !root.StartsWith(@"\\?\", StringComparison.Ordinal) || IsSlow(root[4..]);
+ }
+ }
+
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/NativeMethods.txt b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/NativeMethods.txt
index 61e89b68c4..03318381a6 100644
--- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/NativeMethods.txt
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/NativeMethods.txt
@@ -12,4 +12,8 @@ MonitorFromWindow
SHOW_WINDOW_CMD
ShellExecuteEx
-SEE_MASK_INVOKEIDLIST
\ No newline at end of file
+SEE_MASK_INVOKEIDLIST
+
+GetFileAttributes
+FILE_FLAGS_AND_ATTRIBUTES
+INVALID_FILE_ATTRIBUTES
\ No newline at end of file
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs
index 427fcd170e..81fec6e363 100644
--- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs
@@ -2,8 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using CommunityToolkit.Mvvm.Input;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
@@ -11,6 +13,13 @@ public partial class DetailsLinkViewModel(
IDetailsElement _detailsElement,
WeakReference context) : DetailsElementViewModel(_detailsElement, context)
{
+ private static readonly string[] _initProperties = [
+ nameof(Text),
+ nameof(Link),
+ nameof(IsLink),
+ nameof(IsText),
+ nameof(NavigateCommand)];
+
private readonly ExtensionObject _dataModel =
new(_detailsElement.Data as IDetailsLink);
@@ -22,6 +31,8 @@ public partial class DetailsLinkViewModel(
public bool IsText => !IsLink;
+ public RelayCommand? NavigateCommand { get; private set; }
+
public override void InitializeProperties()
{
base.InitializeProperties();
@@ -38,9 +49,18 @@ public partial class DetailsLinkViewModel(
Text = Link.ToString();
}
- UpdateProperty(nameof(Text));
- UpdateProperty(nameof(Link));
- UpdateProperty(nameof(IsLink));
- UpdateProperty(nameof(IsText));
+ if (Link is not null)
+ {
+ // Custom command to open a link in the default browser or app,
+ // depending on the link type.
+ // Binding Link to a Hyperlink(Button).NavigateUri works only for
+ // certain URI schemes (e.g., http, https) and cannot open file:
+ // scheme URIs or local files.
+ NavigateCommand = new RelayCommand(
+ () => ShellHelpers.OpenInShell(Link.ToString()),
+ () => Link is not null);
+ }
+
+ UpdateProperty(_initProperties);
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
index 597072241a..fe1a29dd97 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
@@ -108,6 +108,7 @@
Visibility="{x:Bind IsText, Mode=OneWay}" />
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/IClipboardMetadataProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/IClipboardMetadataProvider.cs
new file mode 100644
index 0000000000..9b73ade32b
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/IClipboardMetadataProvider.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+///
+/// Abstraction for providers that can extract metadata and offer actions for a clipboard context.
+///
+internal interface IClipboardMetadataProvider
+{
+ ///
+ /// Gets the section title to show in the UI for this provider's metadata.
+ ///
+ string SectionTitle { get; }
+
+ ///
+ /// Returns true if this provider can produce metadata for the given item.
+ ///
+ bool CanHandle(ClipboardItem item);
+
+ ///
+ /// Returns metadata elements for the UI. Caller decides section grouping.
+ ///
+ IEnumerable GetDetails(ClipboardItem item);
+
+ ///
+ /// Returns context actions to be appended to MoreCommands. Use unique IDs for de-duplication.
+ ///
+ IEnumerable GetActions(ClipboardItem item);
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadata.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadata.cs
new file mode 100644
index 0000000000..429f6341f3
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadata.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+internal sealed record ImageMetadata(
+ uint Width,
+ uint Height,
+ double DpiX,
+ double DpiY,
+ ulong? StorageSize);
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadataAnalyzer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadataAnalyzer.cs
new file mode 100644
index 0000000000..e69a7d3d9c
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadataAnalyzer.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading.Tasks;
+using Windows.Graphics.Imaging;
+using Windows.Storage.Streams;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+internal static class ImageMetadataAnalyzer
+{
+ ///
+ /// Reads image metadata from a RandomAccessStreamReference without decoding pixels.
+ /// Returns oriented dimensions (EXIF rotation applied).
+ ///
+ public static async Task GetAsync(RandomAccessStreamReference reference)
+ {
+ ArgumentNullException.ThrowIfNull(reference);
+
+ using IRandomAccessStream ras = await reference.OpenReadAsync().AsTask().ConfigureAwait(false);
+ var sizeBytes = TryGetSize(ras);
+
+ // BitmapDecoder does not decode pixel data unless you ask it to,
+ // so this is fast and memory-friendly.
+ var decoder = await BitmapDecoder.CreateAsync(ras).AsTask().ConfigureAwait(false);
+
+ // OrientedPixelWidth/Height account for EXIF orientation
+ var width = decoder.OrientedPixelWidth;
+ var height = decoder.OrientedPixelHeight;
+
+ return new ImageMetadata(
+ Width: width,
+ Height: height,
+ DpiX: decoder.DpiX,
+ DpiY: decoder.DpiY,
+ StorageSize: sizeBytes);
+ }
+
+ private static ulong? TryGetSize(IRandomAccessStream s)
+ {
+ try
+ {
+ // On file-backed streams this is accurate.
+ // On some URI/virtual streams this may be unsupported or 0.
+ var size = s.Size;
+ return size == 0 ? (ulong?)0 : size;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadataProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadataProvider.cs
new file mode 100644
index 0000000000..09a3f33f2e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ImageMetadataProvider.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using ManagedCommon;
+using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+internal sealed class ImageMetadataProvider : IClipboardMetadataProvider
+{
+ public string SectionTitle => "Image metadata";
+
+ public bool CanHandle(ClipboardItem item) => item.IsImage;
+
+ public IEnumerable GetDetails(ClipboardItem item)
+ {
+ var result = new List();
+ if (!CanHandle(item) || item.ImageData is null)
+ {
+ return result;
+ }
+
+ try
+ {
+ var metadata = ImageMetadataAnalyzer.GetAsync(item.ImageData).GetAwaiter().GetResult();
+
+ result.Add(new DetailsElement
+ {
+ Key = "Dimensions",
+ Data = new DetailsLink($"{metadata.Width} x {metadata.Height}"),
+ });
+ result.Add(new DetailsElement
+ {
+ Key = "DPI",
+ Data = new DetailsLink($"{metadata.DpiX:0.###} x {metadata.DpiY:0.###}"),
+ });
+
+ if (metadata.StorageSize != null)
+ {
+ result.Add(new DetailsElement
+ {
+ Key = "Storage size",
+ Data = new DetailsLink(SizeFormatter.FormatSize(metadata.StorageSize.Value)),
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogDebug("Failed to retrieve image metadata:" + ex);
+ }
+
+ return result;
+ }
+
+ public IEnumerable GetActions(ClipboardItem item) => [];
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/LineEndingType.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/LineEndingType.cs
new file mode 100644
index 0000000000..1274d1ace9
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/LineEndingType.cs
@@ -0,0 +1,14 @@
+// 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.Ext.ClipboardHistory.Helpers.Analyzers;
+
+internal enum LineEndingType
+{
+ None,
+ Windows, // \r\n (CRLF)
+ Unix, // \n (LF)
+ Mac, // \r (CR)
+ Mixed,
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ProviderAction.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ProviderAction.cs
new file mode 100644
index 0000000000..1827fa8744
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/ProviderAction.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+///
+/// Represents an action exposed by a metadata provider.
+///
+/// Unique identifier for de-duplication (case-insensitive).
+/// The actual context menu item to be shown.
+internal readonly record struct ProviderAction(string Id, CommandContextItem Action);
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/SizeFormatter.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/SizeFormatter.cs
new file mode 100644
index 0000000000..a08ab32bc2
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/SizeFormatter.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Globalization;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+///
+/// Utility for formatting byte sizes to a human-readable string.
+///
+internal static class SizeFormatter
+{
+ private const long KB = 1024;
+ private const long MB = 1024 * KB;
+ private const long GB = 1024 * MB;
+
+ public static string FormatSize(long bytes)
+ {
+ return bytes switch
+ {
+ >= GB => string.Format(CultureInfo.CurrentCulture, "{0:F2} GB", (double)bytes / GB),
+ >= MB => string.Format(CultureInfo.CurrentCulture, "{0:F2} MB", (double)bytes / MB),
+ >= KB => string.Format(CultureInfo.CurrentCulture, "{0:F2} KB", (double)bytes / KB),
+ _ => string.Format(CultureInfo.CurrentCulture, "{0} B", bytes),
+ };
+ }
+
+ public static string FormatSize(ulong bytes)
+ {
+ // Use double for division to avoid overflow; thresholds mirror long version
+ if (bytes >= (ulong)GB)
+ {
+ return string.Format(CultureInfo.CurrentCulture, "{0:F2} GB", bytes / (double)GB);
+ }
+
+ if (bytes >= (ulong)MB)
+ {
+ return string.Format(CultureInfo.CurrentCulture, "{0:F2} MB", bytes / (double)MB);
+ }
+
+ if (bytes >= (ulong)KB)
+ {
+ return string.Format(CultureInfo.CurrentCulture, "{0:F2} KB", bytes / (double)KB);
+ }
+
+ return string.Format(CultureInfo.CurrentCulture, "{0} B", bytes);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextFileSystemMetadataProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextFileSystemMetadataProvider.cs
new file mode 100644
index 0000000000..a51444a3af
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextFileSystemMetadataProvider.cs
@@ -0,0 +1,138 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using ManagedCommon;
+using Microsoft.CmdPal.Core.Common.Helpers;
+using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+///
+/// Detects when text content is a valid existing file or directory path and exposes basic metadata.
+///
+internal sealed class TextFileSystemMetadataProvider : IClipboardMetadataProvider
+{
+ public string SectionTitle => "File";
+
+ public bool CanHandle(ClipboardItem item)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+
+ if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
+ {
+ return false;
+ }
+
+ var text = PathHelper.Unquote(item.Content);
+ return PathHelper.IsValidFilePath(text);
+ }
+
+ public IEnumerable GetDetails(ClipboardItem item)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+
+ var result = new List();
+ if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
+ {
+ return result;
+ }
+
+ var path = PathHelper.Unquote(item.Content);
+
+ if (PathHelper.IsSlow(path) || !PathHelper.Exists(path, out var isDirectory))
+ {
+ result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(Path.GetFileName(path)) });
+ result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(path), path) });
+ return result;
+ }
+
+ try
+ {
+ if (!isDirectory)
+ {
+ var fi = new FileInfo(path);
+ result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(fi.Name) });
+ result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(fi.FullName), fi.FullName) });
+ result.Add(new DetailsElement { Key = "Type", Data = new DetailsLink(fi.Extension) });
+ result.Add(new DetailsElement { Key = "Size", Data = new DetailsLink(SizeFormatter.FormatSize(fi.Length)) });
+ result.Add(new DetailsElement { Key = "Modified", Data = new DetailsLink(fi.LastWriteTime.ToString(CultureInfo.CurrentCulture)) });
+ result.Add(new DetailsElement { Key = "Created", Data = new DetailsLink(fi.CreationTime.ToString(CultureInfo.CurrentCulture)) });
+ }
+ else
+ {
+ var di = new DirectoryInfo(path);
+ result.Add(new DetailsElement { Key = "Name", Data = new DetailsLink(di.Name) });
+ result.Add(new DetailsElement { Key = "Location", Data = new DetailsLink(UrlHelper.NormalizeUrl(di.FullName), di.FullName) });
+ result.Add(new DetailsElement { Key = "Type", Data = new DetailsLink("Folder") });
+ result.Add(new DetailsElement { Key = "Modified", Data = new DetailsLink(di.LastWriteTime.ToString(CultureInfo.CurrentCulture)) });
+ result.Add(new DetailsElement { Key = "Created", Data = new DetailsLink(di.CreationTime.ToString(CultureInfo.CurrentCulture)) });
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to retrieve file system metadata.", ex);
+ }
+
+ return result;
+ }
+
+ public IEnumerable GetActions(ClipboardItem item)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+
+ if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
+ {
+ yield break;
+ }
+
+ var path = PathHelper.Unquote(item.Content);
+
+ if (PathHelper.IsSlow(path) || !PathHelper.Exists(path, out var isDirectory))
+ {
+ // One anything
+ var open = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
+ yield return new ProviderAction(WellKnownActionIds.Open, open);
+
+ yield break;
+ }
+
+ if (!isDirectory)
+ {
+ // Open file
+ var open = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
+ yield return new ProviderAction(WellKnownActionIds.Open, open);
+
+ // Show in folder (select)
+ var show = new CommandContextItem(new ShowFileInFolderCommand(path)) { RequestedShortcut = WellKnownKeyChords.OpenFileLocation };
+ yield return new ProviderAction(WellKnownActionIds.OpenLocation, show);
+
+ // Copy path
+ var copy = new CommandContextItem(new CopyPathCommand(path)) { RequestedShortcut = WellKnownKeyChords.CopyFilePath };
+ yield return new ProviderAction(WellKnownActionIds.CopyPath, copy);
+
+ // Open in console at file location
+ var openConsole = new CommandContextItem(OpenInConsoleCommand.FromFile(path)) { RequestedShortcut = WellKnownKeyChords.OpenInConsole };
+ yield return new ProviderAction(WellKnownActionIds.OpenConsole, openConsole);
+ }
+ else
+ {
+ // Open folder
+ var openFolder = new CommandContextItem(new OpenFileCommand(path)) { RequestedShortcut = KeyChords.OpenUrl };
+ yield return new ProviderAction(WellKnownActionIds.Open, openFolder);
+
+ // Open in console
+ var openConsole = new CommandContextItem(OpenInConsoleCommand.FromDirectory(path)) { RequestedShortcut = WellKnownKeyChords.OpenInConsole };
+ yield return new ProviderAction(WellKnownActionIds.OpenConsole, openConsole);
+
+ // Copy path
+ var copy = new CommandContextItem(new CopyPathCommand(path)) { RequestedShortcut = WellKnownKeyChords.CopyFilePath };
+ yield return new ProviderAction(WellKnownActionIds.CopyPath, copy);
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadata.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadata.cs
new file mode 100644
index 0000000000..726a15c37e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadata.cs
@@ -0,0 +1,25 @@
+// 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.Ext.ClipboardHistory.Helpers.Analyzers;
+
+internal sealed record TextMetadata
+{
+ public int CharacterCount { get; init; }
+
+ public int WordCount { get; init; }
+
+ public int SentenceCount { get; init; }
+
+ public int LineCount { get; init; }
+
+ public int ParagraphCount { get; init; }
+
+ public LineEndingType LineEnding { get; init; }
+
+ public override string ToString()
+ {
+ return $"Characters: {CharacterCount}, Words: {WordCount}, Sentences: {SentenceCount}, Lines: {LineCount}, Paragraphs: {ParagraphCount}, Line Ending: {LineEnding}";
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadataAnalyzer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadataAnalyzer.cs
new file mode 100644
index 0000000000..83992f6428
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadataAnalyzer.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+internal partial class TextMetadataAnalyzer
+{
+ public TextMetadata Analyze(string input)
+ {
+ ArgumentNullException.ThrowIfNull(input);
+
+ return new TextMetadata
+ {
+ CharacterCount = input.Length,
+ WordCount = CountWords(input),
+ SentenceCount = CountSentences(input),
+ LineCount = CountLines(input),
+ ParagraphCount = CountParagraphs(input),
+ LineEnding = DetectLineEnding(input),
+ };
+ }
+
+ private LineEndingType DetectLineEnding(string text)
+ {
+ var crlfCount = Regex.Matches(text, "\r\n").Count;
+ var lfCount = Regex.Matches(text, "(? 0 ? 1 : 0) + (lfCount > 0 ? 1 : 0) + (crCount > 0 ? 1 : 0);
+
+ if (endingTypes > 1)
+ {
+ return LineEndingType.Mixed;
+ }
+
+ if (crlfCount > 0)
+ {
+ return LineEndingType.Windows;
+ }
+
+ if (lfCount > 0)
+ {
+ return LineEndingType.Unix;
+ }
+
+ if (crCount > 0)
+ {
+ return LineEndingType.Mac;
+ }
+
+ return LineEndingType.None;
+ }
+
+ private int CountLines(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return 0;
+ }
+
+ return text.Count(c => c == '\n') + 1;
+ }
+
+ private int CountParagraphs(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return 0;
+ }
+
+ var paragraphs = ParagraphsRegex()
+ .Split(text)
+ .Count(static p => !string.IsNullOrWhiteSpace(p));
+
+ return paragraphs > 0 ? paragraphs : 1;
+ }
+
+ private int CountWords(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return 0;
+ }
+
+ return Regex.Matches(text, @"\b\w+\b").Count;
+ }
+
+ private int CountSentences(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return 0;
+ }
+
+ var matches = SentencesRegex().Matches(text);
+ return matches.Count > 0 ? matches.Count : (text.Trim().Length > 0 ? 1 : 0);
+ }
+
+ [GeneratedRegex(@"(\r?\n){2,}")]
+ private static partial Regex ParagraphsRegex();
+
+ [GeneratedRegex(@"[.!?]+(?=\s|$)")]
+ private static partial Regex SentencesRegex();
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadataProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadataProvider.cs
new file mode 100644
index 0000000000..86e2a32270
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/TextMetadataProvider.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+internal sealed class TextMetadataProvider : IClipboardMetadataProvider
+{
+ public string SectionTitle => "Text statistics";
+
+ public bool CanHandle(ClipboardItem item) => item.IsText;
+
+ public IEnumerable GetDetails(ClipboardItem item)
+ {
+ var result = new List();
+ if (!CanHandle(item) || string.IsNullOrEmpty(item.Content))
+ {
+ return result;
+ }
+
+ var r = new TextMetadataAnalyzer().Analyze(item.Content);
+
+ result.Add(new DetailsElement
+ {
+ Key = "Characters",
+ Data = new DetailsLink(r.CharacterCount.ToString(CultureInfo.CurrentCulture)),
+ });
+ result.Add(new DetailsElement
+ {
+ Key = "Words",
+ Data = new DetailsLink(r.WordCount.ToString(CultureInfo.CurrentCulture)),
+ });
+ result.Add(new DetailsElement
+ {
+ Key = "Sentences",
+ Data = new DetailsLink(r.SentenceCount.ToString(CultureInfo.CurrentCulture)),
+ });
+ result.Add(new DetailsElement
+ {
+ Key = "Lines",
+ Data = new DetailsLink(r.LineCount.ToString(CultureInfo.CurrentCulture)),
+ });
+ result.Add(new DetailsElement
+ {
+ Key = "Paragraphs",
+ Data = new DetailsLink(r.ParagraphCount.ToString(CultureInfo.CurrentCulture)),
+ });
+ result.Add(new DetailsElement
+ {
+ Key = "Line Ending",
+ Data = new DetailsLink(r.LineEnding.ToString()),
+ });
+
+ return result;
+ }
+
+ public IEnumerable GetActions(ClipboardItem item) => [];
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/WebLinkMetadataProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/WebLinkMetadataProvider.cs
new file mode 100644
index 0000000000..0a2afc3e01
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/WebLinkMetadataProvider.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
+
+///
+/// Detects web links in text and shows normalized URL and key parts.
+///
+internal sealed class WebLinkMetadataProvider : IClipboardMetadataProvider
+{
+ public string SectionTitle => "Link";
+
+ public bool CanHandle(ClipboardItem item)
+ {
+ if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
+ {
+ return false;
+ }
+
+ if (!UrlHelper.IsValidUrl(item.Content))
+ {
+ return false;
+ }
+
+ var normalized = UrlHelper.NormalizeUrl(item.Content);
+ if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
+ {
+ return false;
+ }
+
+ // Exclude file: scheme; it's handled by TextFileSystemMetadataProvider
+ return !uri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public IEnumerable GetDetails(ClipboardItem item)
+ {
+ var result = new List();
+ if (!item.IsText || string.IsNullOrWhiteSpace(item.Content))
+ {
+ return result;
+ }
+
+ try
+ {
+ var normalized = UrlHelper.NormalizeUrl(item.Content);
+ if (!Uri.TryCreate(normalized, UriKind.Absolute, out var uri))
+ {
+ return result;
+ }
+
+ // Skip file: at runtime as well (defensive)
+ if (uri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
+ {
+ return result;
+ }
+
+ result.Add(new DetailsElement { Key = "URL", Data = new DetailsLink(normalized) });
+ result.Add(new DetailsElement { Key = "Host", Data = new DetailsLink(uri.Host) });
+
+ if (!uri.IsDefaultPort)
+ {
+ result.Add(new DetailsElement { Key = "Port", Data = new DetailsLink(uri.Port.ToString(CultureInfo.CurrentCulture)) });
+ }
+
+ if (!string.IsNullOrEmpty(uri.AbsolutePath) && uri.AbsolutePath != "/")
+ {
+ result.Add(new DetailsElement { Key = "Path", Data = new DetailsLink(uri.AbsolutePath) });
+ }
+
+ if (!string.IsNullOrEmpty(uri.Query))
+ {
+ var q = uri.Query;
+ var count = q.Count(static c => c == '&') + (q.Length > 1 ? 1 : 0);
+ result.Add(new DetailsElement { Key = "Query params", Data = new DetailsLink(count.ToString(CultureInfo.CurrentCulture)) });
+ }
+
+ if (!string.IsNullOrEmpty(uri.Fragment))
+ {
+ result.Add(new DetailsElement { Key = "Fragment", Data = new DetailsLink(uri.Fragment) });
+ }
+ }
+ catch
+ {
+ // ignore malformed inputs
+ }
+
+ return result;
+ }
+
+ public IEnumerable GetActions(ClipboardItem item)
+ {
+ if (!CanHandle(item))
+ {
+ yield break;
+ }
+
+ var normalized = UrlHelper.NormalizeUrl(item.Content!);
+
+ var open = new CommandContextItem(new OpenUrlCommand(normalized))
+ {
+ RequestedShortcut = KeyChords.OpenUrl,
+ };
+ yield return new ProviderAction(WellKnownActionIds.Open, open);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/WellKnownActionIds.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/WellKnownActionIds.cs
new file mode 100644
index 0000000000..7fa2a74aea
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/Analyzers/WellKnownActionIds.cs
@@ -0,0 +1,16 @@
+// 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.Ext.ClipboardHistory.Helpers.Analyzers;
+
+///
+/// Well-known action id constants used to de-duplicate provider actions.
+///
+internal static class WellKnownActionIds
+{
+ public const string Open = "open";
+ public const string OpenLocation = "openLocation";
+ public const string CopyPath = "copyPath";
+ public const string OpenConsole = "openConsole";
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs
index 60e7851761..fe160e4c1b 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/UrlHelper.cs
@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.IO;
+using Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
@@ -31,7 +31,7 @@ internal static class UrlHelper
}
// Check if it's a valid file path (local or network)
- if (IsValidFilePath(url))
+ if (PathHelper.IsValidFilePath(url))
{
return true;
}
@@ -78,7 +78,7 @@ internal static class UrlHelper
url = url.Trim();
// If it's a valid file path, convert to file:// URI
- if (IsValidFilePath(url) && !url.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
+ if (!url.StartsWith("file://", StringComparison.OrdinalIgnoreCase) && PathHelper.IsValidFilePath(url))
{
try
{
@@ -105,40 +105,4 @@ internal static class UrlHelper
return url;
}
-
- ///
- /// Checks if a string represents a valid file path (local or network)
- ///
- /// The string to check
- /// True if the string is a valid file path, false otherwise
- private static bool IsValidFilePath(string path)
- {
- if (string.IsNullOrWhiteSpace(path))
- {
- return false;
- }
-
- try
- {
- // Check for UNC paths (network paths starting with \\)
- if (path.StartsWith(@"\\", StringComparison.Ordinal))
- {
- // Basic UNC path validation: \\server\share or \\server\share\path
- var parts = path.Substring(2).Split('\\', StringSplitOptions.RemoveEmptyEntries);
- return parts.Length >= 2; // At minimum: server and share
- }
-
- // Check for drive letters (C:\ or C:)
- if (path.Length >= 2 && char.IsLetter(path[0]) && path[1] == ':')
- {
- return true;
- }
-
- return false;
- }
- catch
- {
- return false;
- }
- }
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
index e3d17fb500..b0c0617c34 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
@@ -10,6 +10,7 @@
enable
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs
index 9b5aae6f7d..865d8f6b91 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs
@@ -9,6 +9,7 @@ using System.Linq;
using Microsoft.CmdPal.Common.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
+using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -16,13 +17,20 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
internal sealed partial class ClipboardListItem : ListItem
{
+ private static readonly IClipboardMetadataProvider[] MetadataProviders =
+ [
+ new ImageMetadataProvider(),
+ new TextFileSystemMetadataProvider(),
+ new WebLinkMetadataProvider(),
+ new TextMetadataProvider(),
+ ];
+
private readonly SettingsManager _settingsManager;
private readonly ClipboardItem _item;
private readonly CommandContextItem _deleteContextMenuItem;
private readonly CommandContextItem? _pasteCommand;
private readonly CommandContextItem? _copyCommand;
- private readonly CommandContextItem? _openUrlCommand;
private readonly Lazy _lazyDetails;
public override IDetails? Details
@@ -73,26 +81,11 @@ internal sealed partial class ClipboardListItem : ListItem
_pasteCommand = new CommandContextItem(new PasteCommand(_item, ClipboardFormat.Text, _settingsManager));
_copyCommand = new CommandContextItem(new CopyCommand(_item, ClipboardFormat.Text));
-
- // Check if the text content is a valid URL and add OpenUrl command
- if (UrlHelper.IsValidUrl(_item.Content ?? string.Empty))
- {
- var normalizedUrl = UrlHelper.NormalizeUrl(_item.Content ?? string.Empty);
- _openUrlCommand = new CommandContextItem(new OpenUrlCommand(normalizedUrl))
- {
- RequestedShortcut = KeyChords.OpenUrl,
- };
- }
- else
- {
- _openUrlCommand = null;
- }
}
else
{
_pasteCommand = null;
_copyCommand = null;
- _openUrlCommand = null;
}
RefreshCommands();
@@ -163,27 +156,74 @@ internal sealed partial class ClipboardListItem : ListItem
commands.Add(firstCommand);
}
- if (_openUrlCommand != null)
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var temp = new List();
+ foreach (var provider in MetadataProviders)
{
- commands.Add(_openUrlCommand);
+ if (!provider.CanHandle(_item))
+ {
+ continue;
+ }
+
+ foreach (var action in provider.GetActions(_item))
+ {
+ if (string.IsNullOrEmpty(action.Id) || !seen.Add(action.Id))
+ {
+ continue;
+ }
+
+ temp.Add(action.Action);
+ }
+ }
+
+ if (temp.Count > 0)
+ {
+ if (commands.Count > 0)
+ {
+ commands.Add(new Separator());
+ }
+
+ commands.AddRange(temp);
}
commands.Add(new Separator());
commands.Add(_deleteContextMenuItem);
- return commands.ToArray();
+ return [.. commands];
}
private Details CreateDetails()
{
- IDetailsElement[] metadata =
- [
- new DetailsElement
+ List metadata = [];
+
+ foreach (var provider in MetadataProviders)
+ {
+ if (provider.CanHandle(_item))
{
- Key = "Copied on",
- Data = new DetailsLink(_item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
+ var details = provider.GetDetails(_item);
+ if (details.Any())
+ {
+ metadata.Add(new DetailsElement
+ {
+ Key = provider.SectionTitle,
+ Data = new DetailsSeparator(),
+ });
+
+ metadata.AddRange(details);
+ }
}
- ];
+ }
+
+ metadata.Add(new DetailsElement
+ {
+ Key = "General",
+ Data = new DetailsSeparator(),
+ });
+ metadata.Add(new DetailsElement
+ {
+ Key = "Copied",
+ Data = new DetailsLink(_item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
+ });
if (_item.IsImage)
{
@@ -193,7 +233,7 @@ internal sealed partial class ClipboardListItem : ListItem
{
Title = _item.GetDataType(),
HeroImage = heroImage,
- Metadata = metadata,
+ Metadata = [.. metadata],
};
}
@@ -203,7 +243,7 @@ internal sealed partial class ClipboardListItem : ListItem
{
Title = _item.GetDataType(),
Body = $"```text\n{_item.Content}\n```",
- Metadata = metadata,
+ Metadata = [.. metadata],
};
}