Compare commits
88 Commits
dev/vanzue
...
dev/demo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f056343df | ||
|
|
dc00095173 | ||
|
|
4b8ca7982e | ||
|
|
5693e6f776 | ||
|
|
e126840d83 | ||
|
|
5b3699b17e | ||
|
|
f47317f056 | ||
|
|
9bbc2fff49 | ||
|
|
4d90e0d7a8 | ||
|
|
b5f593ac44 | ||
|
|
fcc0304f8a | ||
|
|
eb65888421 | ||
|
|
8dc65d2bb3 | ||
|
|
f85141c3ae | ||
|
|
c1bbfa6035 | ||
|
|
bcbacb6101 | ||
|
|
0b108efccf | ||
|
|
a2b4318540 | ||
|
|
f798e3ff7a | ||
|
|
a4548dd32f | ||
|
|
603dfcaa90 | ||
|
|
8c3649b399 | ||
|
|
0e0a831b06 | ||
|
|
5c95ceedc8 | ||
|
|
3541d1f6eb | ||
|
|
22f28c40a8 | ||
|
|
567c63e81b | ||
|
|
860bb8cba2 | ||
|
|
254927fec0 | ||
|
|
66f8d68a8a | ||
|
|
f32b2534ce | ||
|
|
cbc05b4cfc | ||
|
|
cd06b73431 | ||
|
|
1c3b09d6a0 | ||
|
|
89864af06e | ||
|
|
58e4a6c83c | ||
|
|
8eb9ef2c14 | ||
|
|
d89198d26e | ||
|
|
cceee12583 | ||
|
|
f56293763c | ||
|
|
86e013df7e | ||
|
|
0243f57ea3 | ||
|
|
00cd91344d | ||
|
|
72a6057859 | ||
|
|
6d2b982181 | ||
|
|
be07e97cf6 | ||
|
|
02a60bd098 | ||
|
|
250f51fe9b | ||
|
|
84d86b7b9d | ||
|
|
b462b35a43 | ||
|
|
207f294e1e | ||
|
|
03f3206a44 | ||
|
|
5171b78eae | ||
|
|
f36e8747fd | ||
|
|
211cbc6438 | ||
|
|
0e30d2705c | ||
|
|
4ece86c4d1 | ||
|
|
55d8e726a0 | ||
|
|
b3c1170040 | ||
|
|
ef1717c97f | ||
|
|
bfff31e657 | ||
|
|
b761f95ba3 | ||
|
|
1c73f0c518 | ||
|
|
dda9cd8e18 | ||
|
|
14282b9df1 | ||
|
|
81a6e3bfdc | ||
|
|
06279a2102 | ||
|
|
6fb3ee5e3a | ||
|
|
4df18234a7 | ||
|
|
571cb3cb22 | ||
|
|
9c76d8a667 | ||
|
|
e8cbb1bd66 | ||
|
|
5734afcf89 | ||
|
|
c63b29a777 | ||
|
|
4309006a92 | ||
|
|
d24a1d99ad | ||
|
|
439023af68 | ||
|
|
4e5a2db985 | ||
|
|
8a7c944ec9 | ||
|
|
49cfcb1349 | ||
|
|
320b7eca7c | ||
|
|
d60923bc9a | ||
|
|
a5097c7525 | ||
|
|
7ccbef0298 | ||
|
|
665d7ca535 | ||
|
|
debbc72825 | ||
|
|
46a4e32fb6 | ||
|
|
c832862b9a |
1
.github/actions/spell-check/expect.txt
vendored
@@ -947,6 +947,7 @@ maxversiontested
|
||||
mber
|
||||
MBM
|
||||
MBR
|
||||
mcp
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
|
||||
@@ -237,7 +237,9 @@
|
||||
"PowerToys.DSC.dll",
|
||||
"PowerToys.DSC.exe",
|
||||
|
||||
"PowerToysSparse.msix"
|
||||
"PowerToysSparse.msix",
|
||||
"PowerToys.McpServer.dll",
|
||||
"PowerToys.McpServer.exe"
|
||||
],
|
||||
"SigningInfo": {
|
||||
"Operations": [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
@@ -9,7 +9,6 @@
|
||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="1.0.0-beta.17" />
|
||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
@@ -40,12 +39,21 @@
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Amazon" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.66.0-beta" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.HuggingFace" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
@@ -65,6 +73,7 @@
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.2" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@@ -74,7 +83,7 @@
|
||||
<PackageVersion Include="NLog" Version="5.2.8" />
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.0.0" />
|
||||
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
@@ -95,6 +104,7 @@
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.8" />
|
||||
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.8" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.7.0" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.8" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
|
||||
@@ -1495,7 +1495,6 @@ SOFTWARE.
|
||||
- AdaptiveCards.Rendering.WinUI3
|
||||
- AdaptiveCards.Templating
|
||||
- Appium.WebDriver
|
||||
- Azure.AI.OpenAI
|
||||
- CoenM.ImageSharp.ImageHash
|
||||
- CommunityToolkit.Common
|
||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
||||
|
||||
@@ -330,6 +330,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AwakeModuleInterface", "src
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Awake", "src\modules\awake\Awake\Awake.csproj", "{D940E07F-532C-4FF3-883F-790DA014F19A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.McpServer", "src\McpServer\PowerToys.McpServer.csproj", "{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.UnitConverter", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.csproj", "{BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.UnitConverter.UnitTest", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj", "{3E424AD2-19E5-4AE6-B833-F53963EB5FC1}"
|
||||
@@ -828,6 +830,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageModelProvider", "src\common\LanguageModelProvider\LanguageModelProvider.csproj", "{45354F4F-1414-45CE-B600-51CD1209FD19}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
|
||||
EndProject
|
||||
Global
|
||||
@@ -1454,6 +1458,14 @@ Global
|
||||
{D940E07F-532C-4FF3-883F-790DA014F19A}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.ActiveCfg = Release|x64
|
||||
{D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.Build.0 = Release|x64
|
||||
{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}.Debug|x64.Build.0 = Debug|x64
|
||||
{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}.Release|x64.ActiveCfg = Release|x64
|
||||
{8A6F5D3B-F59E-4F34-A1D3-3F69D3FDBD9D}.Release|x64.Build.0 = Release|x64
|
||||
{BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -3008,6 +3020,14 @@ Global
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|x64.Build.0 = Debug|x64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|x64.ActiveCfg = Release|x64
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|x64.Build.0 = Release|x64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -3344,7 +3364,9 @@ Global
|
||||
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
|
||||
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
|
||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
{45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
@@ -1493,11 +1493,12 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
}
|
||||
processes.resize(bytes / sizeof(processes[0]));
|
||||
|
||||
std::array<std::wstring_view, 42> processesToTerminate = {
|
||||
std::array<std::wstring_view, 43> processesToTerminate = {
|
||||
L"PowerToys.PowerLauncher.exe",
|
||||
L"PowerToys.Settings.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
L"PowerToys.Awake.exe",
|
||||
L"PowerToys.McpServer.exe",
|
||||
L"PowerToys.FancyZones.exe",
|
||||
L"PowerToys.FancyZonesEditor.exe",
|
||||
L"PowerToys.FileLocksmithUI.exe",
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="PowerToysPublicDependencies" value="https://pkgs.dev.azure.com/shine-oss/PowerToys/_packaging/PowerToysPublicDependencies/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="Microsoft.SemanticKernel*" />
|
||||
<package pattern="Microsoft.Extensions.*" />
|
||||
<package pattern="System.*" />
|
||||
<package pattern="OpenAI" />
|
||||
<package pattern="Azure.*" />
|
||||
</packageSource>
|
||||
<packageSource key="PowerToysPublicDependencies">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
|
||||
32
src/McpServer/PowerToys.McpServer.csproj
Normal file
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\Common.SelfContained.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>PowerToys.McpServer</RootNamespace>
|
||||
<AssemblyName>PowerToys.McpServer</AssemblyName>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputPath>..\..\$(Platform)\$(Configuration)</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>PowerToys.McpServer.dev.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ApplicationManifest>PowerToys.McpServer.prod.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="ModelContextProtocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
8
src/McpServer/PowerToys.McpServer.dev.manifest
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.McpServer.app" />
|
||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
||||
publisher="CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US"
|
||||
packageName="Microsoft.PowerToys.SparseApp"
|
||||
applicationId="PowerToys.McpServer" />
|
||||
</assembly>
|
||||
8
src/McpServer/PowerToys.McpServer.prod.manifest
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="PowerToys.McpServer.app" />
|
||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
||||
publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
packageName="Microsoft.PowerToys.SparseApp"
|
||||
applicationId="PowerToys.McpServer" />
|
||||
</assembly>
|
||||
57
src/McpServer/Program.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Server;
|
||||
using PowerToys.McpServer.Tools;
|
||||
|
||||
namespace PowerToys.McpServer
|
||||
{
|
||||
internal sealed class Program
|
||||
{
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Initialize PowerToys logger
|
||||
// Logger.InitializeLogger expects path relative to Constants.AppDataPath()
|
||||
// which already points to LocalAppData\Microsoft\PowerToys
|
||||
string logPath = Path.Combine("\\McpServer", "Logs");
|
||||
Logger.InitializeLogger(logPath);
|
||||
Logger.LogInfo("Starting PowerToys MCP Server with official SDK");
|
||||
|
||||
try
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Configure all logs to go to stderr (required for MCP protocol)
|
||||
builder.Logging.AddConsole(consoleLogOptions =>
|
||||
{
|
||||
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
|
||||
});
|
||||
|
||||
// Register MCP server with stdio transport and tools
|
||||
builder.Services
|
||||
.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithToolsFromAssembly();
|
||||
|
||||
Logger.LogInfo("Building and running MCP host...");
|
||||
await builder.Build().RunAsync();
|
||||
Logger.LogInfo("MCP server shutdown complete");
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Fatal error in MCP server", ex);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/McpServer/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# PowerToys Model Context Protocol Server
|
||||
|
||||
This module hosts a standalone Model Context Protocol (MCP) server that exposes PowerToys functionality to MCP-compliant AI agents. The server is built as a .NET 9 console application that implements the MCP specification using the official ModelContextProtocol SDK.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- **Program.cs**: Main entry point that configures the MCP server with stdio transport
|
||||
- **Tools/AwakeTools.cs**: Implementation of Awake-related MCP tools
|
||||
- **PowerToys.McpServer.csproj**: .NET 9 project configuration with MCP dependencies
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Microsoft.Extensions.Hosting**: For hosting infrastructure and dependency injection
|
||||
- **ModelContextProtocol**: Official MCP SDK for .NET
|
||||
- **PowerToys Settings Library**: Integration with PowerToys settings system
|
||||
- **ManagedCommon**: PowerToys logging and utilities
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool Name | Description | Parameters | Module |
|
||||
|-----------|-------------|------------|--------|
|
||||
| `GetAwakeStatus` | Returns the current Awake configuration (mode, timers, display policy) | None | Awake |
|
||||
| `SetAwakePassive` | Set Awake to passive mode (allow system to sleep normally) | None | Awake |
|
||||
| `SetAwakeIndefinite` | Set Awake to indefinite mode (keep system awake until manually changed) | `keepDisplayOn` (bool), `force` (bool) | Awake |
|
||||
| `SetAwakeTimed` | Set Awake to timed mode (keep system awake for a specific duration) | `durationSeconds` (int), `keepDisplayOn` (bool), `force` (bool) | Awake |
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
- .NET 9 SDK
|
||||
- Visual Studio 2022 (recommended) or VS Code with C# extension
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# From PowerToys root directory
|
||||
msbuild src/McpServer/PowerToys.McpServer.csproj /p:Platform=x64 /p:Configuration=Debug
|
||||
|
||||
# Or using dotnet CLI
|
||||
cd src/McpServer
|
||||
dotnet build -c Debug
|
||||
```
|
||||
|
||||
### Run the Server
|
||||
The executable is built to `x64\Debug\PowerToys.McpServer.exe`. The server communicates over standard input/output using MCP framing (`Content-Length` header followed by JSON).
|
||||
|
||||
**Example MCP Client Session:**
|
||||
1. Client sends `initialize` request with MCP version and capabilities
|
||||
2. Client calls `tools/list` to discover available PowerToys tools
|
||||
3. Client invokes `tools/call` with the desired tool name and arguments
|
||||
4. Server responds with tool execution results or errors
|
||||
|
||||
The server will remain active until the process is terminated or a `shutdown` request is received.
|
||||
|
||||
### Logging
|
||||
- Application logs are written to `%LOCALAPPDATA%\Microsoft\PowerToys\McpServer\Logs\`
|
||||
- MCP protocol logs are sent to stderr (required by MCP specification)
|
||||
|
||||
## Architecture
|
||||
|
||||
The server uses the official ModelContextProtocol .NET SDK and follows these patterns:
|
||||
|
||||
- **Tool Discovery**: Tools are automatically discovered using `WithToolsFromAssembly()`
|
||||
- **Tool Attributes**: Methods marked with `[McpServerTool]` and `[Description]` are exposed as MCP tools
|
||||
- **Parameter Binding**: Method parameters are automatically bound from MCP tool call arguments
|
||||
- **Error Handling**: Exceptions are caught and returned as MCP error responses
|
||||
- **Settings Integration**: Uses PowerToys settings system for configuration persistence
|
||||
|
||||
## Adding New Module Tools
|
||||
|
||||
1. Create a new static class in the `Tools/` directory (e.g., `FancyZonesTools.cs`)
|
||||
2. Mark the class with `[McpServerToolType]` attribute
|
||||
3. Implement static methods with `[McpServerTool]` and `[Description]` attributes:
|
||||
```csharp
|
||||
[McpServerToolType]
|
||||
public static class MyModuleTools
|
||||
{
|
||||
[McpServerTool]
|
||||
[Description("Description of what this tool does")]
|
||||
public static JsonObject MyTool(
|
||||
[Description("Parameter description")] string parameter)
|
||||
{
|
||||
// Implementation here
|
||||
return new JsonObject();
|
||||
}
|
||||
}
|
||||
```
|
||||
4. Follow existing patterns in `AwakeTools.cs` for:
|
||||
- Settings integration using `SettingsUtils`
|
||||
- Logging using `Logger.LogInfo/LogError`
|
||||
- Error handling and response formatting
|
||||
- PowerToys process detection and module status checks
|
||||
|
||||
## Integration with PowerToys
|
||||
|
||||
The MCP server integrates with PowerToys through:
|
||||
|
||||
- **Settings System**: Uses the same settings files as the main PowerToys application
|
||||
- **Process Management**: Detects and interacts with running PowerToys processes
|
||||
- **Module Status**: Checks if specific PowerToys modules are enabled
|
||||
- **Logging**: Uses PowerToys logging infrastructure for troubleshooting
|
||||
|
||||
Refer to the PowerToys developer documentation for build and packaging instructions.
|
||||
694
src/McpServer/Tools/AwakeTools.cs
Normal file
@@ -0,0 +1,694 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json.Nodes;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using ModelContextProtocol.Server;
|
||||
using Lock = System.Threading.Lock;
|
||||
|
||||
namespace PowerToys.McpServer.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// MCP tools for PowerToys Awake module.
|
||||
/// </summary>
|
||||
[McpServerToolType]
|
||||
public static class AwakeTools
|
||||
{
|
||||
private static readonly SettingsUtils SettingsUtils = new SettingsUtils();
|
||||
private const string PowerToysProcessName = "PowerToys";
|
||||
private const string AwakeExecutableName = "PowerToys.Awake.exe";
|
||||
private static readonly string[] AwakeRelativeSearchPaths =
|
||||
[
|
||||
AwakeExecutableName,
|
||||
Path.Combine("modules", "Awake", AwakeExecutableName),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current Awake mode and configuration.
|
||||
/// </summary>
|
||||
/// <returns>JSON object with current Awake status.</returns>
|
||||
[McpServerTool]
|
||||
[Description("Get the current Awake mode and configuration from the PowerToys settings store.")]
|
||||
public static JsonObject GetAwakeStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
(bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus();
|
||||
|
||||
if (!powerToysRunning || !awakeModuleEnabled)
|
||||
{
|
||||
if (IsAwakeProcessRunning())
|
||||
{
|
||||
// Awake is running via CLI, but we cannot determine its actual configuration
|
||||
Logger.LogInfo("[MCP] Detected Awake CLI process running while PowerToys is not active or Awake module is disabled.");
|
||||
return AwakeStatusPayload.CreateUnknownActive().ToJsonObject();
|
||||
}
|
||||
|
||||
return AwakeStatusPayload.CreateInactive().ToJsonObject();
|
||||
}
|
||||
|
||||
// PowerToys is running and Awake module is enabled
|
||||
bool awakeProcessRunning = IsAwakeProcessRunning();
|
||||
|
||||
AwakeSettings settings = SettingsUtils.GetSettingsOrDefault<AwakeSettings>(AwakeSettings.ModuleName);
|
||||
string summary = FormatAwakeDescription(settings);
|
||||
|
||||
if (awakeProcessRunning)
|
||||
{
|
||||
summary = $"{summary} An Awake process is already running with the current configuration. To override the active session and apply new settings, use force=true.";
|
||||
}
|
||||
|
||||
AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, summary);
|
||||
Logger.LogInfo("[MCP] Retrieved Awake status via SDK tool.");
|
||||
return payload.ToJsonObject();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("[MCP] Failed to read Awake status.", ex);
|
||||
return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Awake mode to passive (allow system sleep).
|
||||
/// </summary>
|
||||
/// <returns>JSON object with updated Awake status.</returns>
|
||||
[McpServerTool]
|
||||
[Description("Set Awake to passive mode (allow system to sleep normally).")]
|
||||
public static JsonObject SetAwakePassive()
|
||||
{
|
||||
try
|
||||
{
|
||||
(bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus();
|
||||
|
||||
if (!powerToysRunning || !awakeModuleEnabled)
|
||||
{
|
||||
StopAwakeProcesses();
|
||||
Logger.LogInfo("[MCP] Stopped all Awake processes because PowerToys is not running.");
|
||||
return AwakeStatusPayload.CreateInactive().ToJsonObject();
|
||||
}
|
||||
|
||||
AwakeSettings settings = SettingsUtils.GetSettingsOrDefault<AwakeSettings>(AwakeSettings.ModuleName);
|
||||
settings.Properties.Mode = AwakeMode.PASSIVE;
|
||||
settings.Properties.KeepDisplayOn = false;
|
||||
settings.Properties.IntervalHours = 0;
|
||||
settings.Properties.IntervalMinutes = 0;
|
||||
SettingsUtils.SaveSettings(settings.ToJsonString(), AwakeSettings.ModuleName);
|
||||
|
||||
string confirmation = FormatAwakeDescription(settings);
|
||||
Logger.LogInfo($"[MCP] {confirmation}");
|
||||
AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation);
|
||||
return payload.ToJsonObject();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("[MCP] Failed to set Awake to passive.", ex);
|
||||
return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Awake mode to indefinite (keep system awake forever).
|
||||
/// </summary>
|
||||
/// <param name="keepDisplayOn">Whether to keep the display on. Default is true.</param>
|
||||
/// <returns>JSON object with updated Awake status.</returns>
|
||||
[McpServerTool]
|
||||
[Description("Set Awake to indefinite mode (keep system awake until manually changed).")]
|
||||
public static JsonObject SetAwakeIndefinite(
|
||||
[Description("Whether to keep the display on")] bool keepDisplayOn = true,
|
||||
[Description("Force the change even if Awake is already running (default: false)")] bool force = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
(bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus();
|
||||
|
||||
if (!powerToysRunning || !awakeModuleEnabled)
|
||||
{
|
||||
return AwakeStatusPayload.CreateError(
|
||||
"Indefinite mode requires PowerToys to be running with Awake module enabled. CLI mode does not support indefinite operation.").ToJsonObject();
|
||||
}
|
||||
|
||||
AwakeSettings settings = SettingsUtils.GetSettingsOrDefault<AwakeSettings>(AwakeSettings.ModuleName);
|
||||
if (!force && IsAwakeActive(settings))
|
||||
{
|
||||
return BuildActiveProcessResponse(settings, true, false);
|
||||
}
|
||||
|
||||
settings.Properties.Mode = AwakeMode.INDEFINITE;
|
||||
settings.Properties.KeepDisplayOn = keepDisplayOn;
|
||||
settings.Properties.IntervalHours = 0;
|
||||
settings.Properties.IntervalMinutes = 0;
|
||||
SettingsUtils.SaveSettings(settings.ToJsonString(), AwakeSettings.ModuleName);
|
||||
|
||||
string confirmation = FormatAwakeDescription(settings);
|
||||
Logger.LogInfo($"[MCP] {confirmation}");
|
||||
AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation);
|
||||
return payload.ToJsonObject();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("[MCP] Failed to set Awake to indefinite.", ex);
|
||||
return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Awake mode to expire at a specific date and time.
|
||||
/// </summary>
|
||||
/// <param name="expireAt">ISO 8601 date/time when Awake should expire (e.g., "2025-10-22T15:30:00").</param>
|
||||
/// <param name="keepDisplayOn">Whether to keep the display on. Default is true.</param>
|
||||
/// <returns>JSON object with updated Awake status.</returns>
|
||||
[McpServerTool]
|
||||
[Description("Set Awake to expire at a specific date and time (ISO 8601 format).")]
|
||||
public static JsonObject SetAwakeExpireAt(
|
||||
[Description("ISO 8601 date/time when Awake should expire (e.g., \"2025-10-22T15:30:00\")")] string expireAt,
|
||||
[Description("Whether to keep the display on")] bool keepDisplayOn = true,
|
||||
[Description("Force the change even if Awake is already running (default: false)")] bool force = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!DateTimeOffset.TryParse(expireAt, out DateTimeOffset expirationDateTime))
|
||||
{
|
||||
return AwakeStatusPayload.CreateError($"Invalid date format: '{expireAt}'. Please use ISO 8601 format (e.g., '2025-10-22T15:30:00').").ToJsonObject();
|
||||
}
|
||||
|
||||
if (expirationDateTime <= DateTimeOffset.Now)
|
||||
{
|
||||
return AwakeStatusPayload.CreateError("Expiration time must be in the future.").ToJsonObject();
|
||||
}
|
||||
|
||||
(bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus();
|
||||
|
||||
if (!powerToysRunning || !awakeModuleEnabled)
|
||||
{
|
||||
TimeSpan duration = expirationDateTime - DateTimeOffset.Now;
|
||||
uint durationSeconds = (uint)Math.Max(60, duration.TotalSeconds);
|
||||
return HandleCliScenario(AwakeMode.EXPIRABLE, keepDisplayOn, durationSeconds, force);
|
||||
}
|
||||
|
||||
AwakeSettings settings = SettingsUtils.GetSettingsOrDefault<AwakeSettings>(AwakeSettings.ModuleName);
|
||||
if (!force && IsAwakeActive(settings))
|
||||
{
|
||||
return BuildActiveProcessResponse(settings, true, false);
|
||||
}
|
||||
|
||||
TimeSpan timeSpan = expirationDateTime - DateTimeOffset.Now;
|
||||
uint hours = (uint)timeSpan.TotalHours;
|
||||
uint minutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60);
|
||||
if (hours == 0 && minutes == 0)
|
||||
{
|
||||
minutes = 1;
|
||||
}
|
||||
|
||||
settings.Properties.Mode = AwakeMode.EXPIRABLE;
|
||||
settings.Properties.KeepDisplayOn = keepDisplayOn;
|
||||
settings.Properties.IntervalHours = hours;
|
||||
settings.Properties.IntervalMinutes = minutes;
|
||||
settings.Properties.ExpirationDateTime = expirationDateTime;
|
||||
SettingsUtils.SaveSettings(settings.ToJsonString(), AwakeSettings.ModuleName);
|
||||
|
||||
string confirmation = FormatAwakeDescription(settings);
|
||||
Logger.LogInfo($"[MCP] {confirmation}");
|
||||
AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation);
|
||||
return payload.ToJsonObject();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("[MCP] Failed to set Awake expire-at mode.", ex);
|
||||
return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Awake mode to timed (keep system awake for a specific duration).
|
||||
/// </summary>
|
||||
/// <param name="durationSeconds">Duration in seconds (minimum 60).</param>
|
||||
/// <param name="keepDisplayOn">Whether to keep the display on. Default is true.</param>
|
||||
/// <returns>JSON object with updated Awake status.</returns>
|
||||
[McpServerTool]
|
||||
[Description("Set Awake to timed mode (keep system awake for a specific duration).")]
|
||||
public static JsonObject SetAwakeTimed(
|
||||
[Description("Duration in seconds (minimum 60)")] int durationSeconds,
|
||||
[Description("Whether to keep the display on")] bool keepDisplayOn = true,
|
||||
[Description("Force the change even if Awake is already running (default: false)")] bool force = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (durationSeconds < 60)
|
||||
{
|
||||
durationSeconds = 60;
|
||||
}
|
||||
|
||||
(bool powerToysRunning, bool awakeModuleEnabled) = CheckPowerToysAndAwakeStatus();
|
||||
|
||||
if (!powerToysRunning || !awakeModuleEnabled)
|
||||
{
|
||||
return HandleCliScenario(AwakeMode.TIMED, keepDisplayOn, (uint)durationSeconds, force);
|
||||
}
|
||||
|
||||
TimeSpan timeSpan = TimeSpan.FromSeconds(durationSeconds);
|
||||
uint hours = (uint)timeSpan.TotalHours;
|
||||
uint minutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60);
|
||||
if (hours == 0 && minutes == 0)
|
||||
{
|
||||
minutes = 1;
|
||||
}
|
||||
|
||||
AwakeSettings settings = SettingsUtils.GetSettingsOrDefault<AwakeSettings>(AwakeSettings.ModuleName);
|
||||
if (!force && IsAwakeActive(settings))
|
||||
{
|
||||
return BuildActiveProcessResponse(settings, true, false);
|
||||
}
|
||||
|
||||
settings.Properties.Mode = AwakeMode.TIMED;
|
||||
settings.Properties.KeepDisplayOn = keepDisplayOn;
|
||||
settings.Properties.IntervalHours = hours;
|
||||
settings.Properties.IntervalMinutes = minutes;
|
||||
settings.Properties.ExpirationDateTime = DateTimeOffset.Now.Add(timeSpan);
|
||||
SettingsUtils.SaveSettings(settings.ToJsonString(), AwakeSettings.ModuleName);
|
||||
|
||||
string confirmation = FormatAwakeDescription(settings);
|
||||
Logger.LogInfo($"[MCP] {confirmation}");
|
||||
AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(settings, confirmation);
|
||||
return payload.ToJsonObject();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("[MCP] Failed to set Awake to timed mode.", ex);
|
||||
return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatAwakeDescription(AwakeSettings settings)
|
||||
{
|
||||
var mode = settings.Properties.Mode.ToString().ToLowerInvariant();
|
||||
var display = settings.Properties.KeepDisplayOn ? "display on" : "display off";
|
||||
|
||||
return settings.Properties.Mode switch
|
||||
{
|
||||
AwakeMode.PASSIVE => "Awake mode: passive (system sleep allowed)",
|
||||
AwakeMode.INDEFINITE => $"Awake mode: indefinite, {display}",
|
||||
AwakeMode.TIMED => $"Awake mode: timed ({settings.Properties.IntervalHours}h {settings.Properties.IntervalMinutes}m), {display}",
|
||||
AwakeMode.EXPIRABLE => $"Awake mode: expirable (until {settings.Properties.ExpirationDateTime:yyyy-MM-dd HH:mm}), {display}",
|
||||
_ => $"Awake mode: {mode}",
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonObject BuildActiveProcessResponse(AwakeSettings settings, bool powerToysRunning, bool launchedViaCli)
|
||||
{
|
||||
return AwakeStatusPayload.CreateError(
|
||||
"Awake is already running. Use force=true to override.",
|
||||
settings).ToJsonObject();
|
||||
}
|
||||
|
||||
private static bool IsPowerToysRunning()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Process.GetProcessesByName(PowerToysProcessName).Length > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"[MCP] Unable to determine PowerToys runner status: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the Awake module is enabled in PowerToys settings.
|
||||
/// </summary>
|
||||
/// <returns>True if Awake module is enabled, false otherwise</returns>
|
||||
private static bool IsAwakeModuleEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
var generalSettings = SettingsUtils.GetSettings<GeneralSettings>();
|
||||
return generalSettings?.Enabled?.Awake == true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't read settings, assume disabled
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks PowerToys and Awake module status.
|
||||
/// </summary>
|
||||
/// <returns>Tuple containing (powerToysRunning, awakeModuleEnabled)</returns>
|
||||
private static (bool PowerToysRunning, bool AwakeModuleEnabled) CheckPowerToysAndAwakeStatus()
|
||||
{
|
||||
bool powerToysRunning = IsPowerToysRunning();
|
||||
bool awakeModuleEnabled = powerToysRunning && IsAwakeModuleEnabled();
|
||||
|
||||
return (powerToysRunning, awakeModuleEnabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles CLI scenario when PowerToys is not running or Awake module is disabled.
|
||||
/// </summary>
|
||||
/// <param name="mode">The Awake mode to set</param>
|
||||
/// <param name="keepDisplayOn">Whether to keep display on</param>
|
||||
/// <param name="durationSeconds">Duration in seconds (0 for indefinite)</param>
|
||||
/// <param name="force">Whether to force override existing process</param>
|
||||
/// <returns>JSON response for CLI scenario</returns>
|
||||
private static JsonObject HandleCliScenario(AwakeMode mode, bool keepDisplayOn, uint durationSeconds, bool force)
|
||||
{
|
||||
if (!force && IsAwakeProcessRunning())
|
||||
{
|
||||
return AwakeStatusPayload.CreateError(
|
||||
"Awake is already running and PowerToys is not active. Use force=true to override.").ToJsonObject();
|
||||
}
|
||||
|
||||
if (IsAwakeProcessRunning())
|
||||
{
|
||||
StopAwakeProcesses();
|
||||
}
|
||||
|
||||
JsonObject cliPayload = StartAwakeCliProcess(mode, keepDisplayOn, durationSeconds);
|
||||
return cliPayload;
|
||||
}
|
||||
|
||||
private static JsonObject StartAwakeCliProcess(AwakeMode mode, bool keepDisplayOn, uint durationSeconds)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!TryResolveAwakeExecutable(out string executablePath))
|
||||
{
|
||||
throw new FileNotFoundException("PowerToys.Awake.exe was not found near the MCP server executable.");
|
||||
}
|
||||
|
||||
ProcessStartInfo startInfo = CreateSimpleStartInfo(executablePath, mode, keepDisplayOn, durationSeconds);
|
||||
Process? launchedProcess = Process.Start(startInfo);
|
||||
if (launchedProcess is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to start PowerToys.Awake.exe.");
|
||||
}
|
||||
|
||||
// No tracking, just launch and forget
|
||||
launchedProcess.Dispose();
|
||||
|
||||
AwakeSettings snapshot = BuildAwakeSnapshot(mode, keepDisplayOn, durationSeconds);
|
||||
string confirmation = FormatAwakeDescription(snapshot);
|
||||
Logger.LogInfo($"[MCP] Launched Awake CLI for mode {mode} (PowerToys not running).");
|
||||
AwakeStatusPayload payload = AwakeStatusPayload.FromSettings(snapshot, confirmation);
|
||||
return payload.ToJsonObject();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("[MCP] Failed to start Awake CLI.", ex);
|
||||
return AwakeStatusPayload.CreateError(ex.Message).ToJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
private static ProcessStartInfo CreateSimpleStartInfo(string executablePath, AwakeMode mode, bool keepDisplayOn, uint durationSeconds)
|
||||
{
|
||||
string workingDirectory = Path.GetDirectoryName(executablePath) ?? AppDomain.CurrentDomain.BaseDirectory;
|
||||
var startInfo = new ProcessStartInfo(executablePath)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = string.IsNullOrEmpty(workingDirectory) ? AppDomain.CurrentDomain.BaseDirectory : workingDirectory,
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add("--display-on");
|
||||
startInfo.ArgumentList.Add(keepDisplayOn ? "true" : "false");
|
||||
|
||||
if (mode == AwakeMode.TIMED && durationSeconds > 0)
|
||||
{
|
||||
startInfo.ArgumentList.Add("--time-limit");
|
||||
startInfo.ArgumentList.Add(durationSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (mode == AwakeMode.EXPIRABLE && durationSeconds > 0)
|
||||
{
|
||||
// For EXPIRABLE mode, convert duration to expiration datetime
|
||||
DateTimeOffset expirationDateTime = DateTimeOffset.Now.AddSeconds(durationSeconds);
|
||||
startInfo.ArgumentList.Add("--expire-at");
|
||||
startInfo.ArgumentList.Add(expirationDateTime.ToString("O")); // ISO 8601 format
|
||||
}
|
||||
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
private static void StopAwakeProcesses()
|
||||
{
|
||||
string processName = Path.GetFileNameWithoutExtension(AwakeExecutableName);
|
||||
try
|
||||
{
|
||||
Process[] awakeProcesses = Process.GetProcessesByName(processName);
|
||||
foreach (Process process in awakeProcesses)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill(true);
|
||||
process.WaitForExit(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"[MCP] Failed to terminate Awake process {process.Id}: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"[MCP] Failed to enumerate Awake processes: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static AwakeSettings BuildAwakeSnapshot(AwakeMode mode, bool keepDisplayOn, uint durationSeconds)
|
||||
{
|
||||
var snapshot = new AwakeSettings();
|
||||
snapshot.Properties.Mode = mode;
|
||||
snapshot.Properties.KeepDisplayOn = keepDisplayOn;
|
||||
|
||||
if (mode == AwakeMode.TIMED && durationSeconds > 0)
|
||||
{
|
||||
TimeSpan timeSpan = TimeSpan.FromSeconds(durationSeconds);
|
||||
snapshot.Properties.IntervalHours = (uint)timeSpan.TotalHours;
|
||||
snapshot.Properties.IntervalMinutes = (uint)Math.Ceiling(timeSpan.TotalMinutes % 60);
|
||||
snapshot.Properties.ExpirationDateTime = DateTimeOffset.Now.Add(timeSpan);
|
||||
}
|
||||
else if (mode == AwakeMode.EXPIRABLE && durationSeconds > 0)
|
||||
{
|
||||
snapshot.Properties.IntervalHours = 0;
|
||||
snapshot.Properties.IntervalMinutes = 0;
|
||||
snapshot.Properties.ExpirationDateTime = DateTimeOffset.Now.AddSeconds(durationSeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.Properties.IntervalHours = 0;
|
||||
snapshot.Properties.IntervalMinutes = 0;
|
||||
snapshot.Properties.ExpirationDateTime = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static bool TryResolveAwakeExecutable(out string executablePath)
|
||||
{
|
||||
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
if (TryResolveAwakeExecutableFrom(baseDirectory, out executablePath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string? parentDirectory = Directory.GetParent(baseDirectory)?.FullName;
|
||||
if (!string.IsNullOrEmpty(parentDirectory) && TryResolveAwakeExecutableFrom(parentDirectory, out executablePath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
executablePath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveAwakeExecutableFrom(string rootDirectory, out string executablePath)
|
||||
{
|
||||
foreach (string relativePath in AwakeRelativeSearchPaths)
|
||||
{
|
||||
string candidate = Path.Combine(rootDirectory, relativePath);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
executablePath = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
executablePath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsAwakeProcessRunning()
|
||||
{
|
||||
try
|
||||
{
|
||||
string processName = Path.GetFileNameWithoutExtension(AwakeExecutableName);
|
||||
return Process.GetProcessesByName(processName).Length > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"[MCP] Unable to determine Awake process status: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAwakeActive(AwakeSettings settings)
|
||||
{
|
||||
// Only check if Awake module is enabled
|
||||
return IsAwakeModuleEnabled();
|
||||
}
|
||||
|
||||
private sealed class AwakeStatusPayload
|
||||
{
|
||||
internal string Mode { get; set; } = "unknown";
|
||||
|
||||
internal bool? KeepDisplayOn { get; set; }
|
||||
|
||||
internal uint? IntervalHours { get; set; }
|
||||
|
||||
internal uint? IntervalMinutes { get; set; }
|
||||
|
||||
internal string? ExpirationDateTime { get; set; }
|
||||
|
||||
internal string Summary { get; set; } = string.Empty;
|
||||
|
||||
internal bool Success { get; set; } = true;
|
||||
|
||||
internal string? ErrorMessage { get; set; }
|
||||
|
||||
internal JsonObject ToJsonObject()
|
||||
{
|
||||
var result = new JsonObject
|
||||
{
|
||||
["mode"] = Mode,
|
||||
["summary"] = Summary,
|
||||
};
|
||||
|
||||
// Add properties only if they have values
|
||||
if (KeepDisplayOn.HasValue)
|
||||
{
|
||||
result["keepDisplayOn"] = KeepDisplayOn.Value;
|
||||
}
|
||||
|
||||
if (IntervalHours.HasValue)
|
||||
{
|
||||
result["intervalHours"] = IntervalHours.Value;
|
||||
}
|
||||
|
||||
if (IntervalMinutes.HasValue)
|
||||
{
|
||||
result["intervalMinutes"] = IntervalMinutes.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ExpirationDateTime))
|
||||
{
|
||||
result["expirationDateTime"] = ExpirationDateTime;
|
||||
}
|
||||
|
||||
// Add error handling properties
|
||||
if (!Success)
|
||||
{
|
||||
result["success"] = false;
|
||||
if (!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
result["error"] = ErrorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static AwakeStatusPayload FromSettings(AwakeSettings settings, string summary)
|
||||
{
|
||||
var payload = new AwakeStatusPayload
|
||||
{
|
||||
Mode = settings.Properties.Mode.ToString().ToLowerInvariant(),
|
||||
Summary = summary,
|
||||
};
|
||||
|
||||
// Only include properties relevant to the current mode
|
||||
if (settings.Properties.Mode != AwakeMode.PASSIVE)
|
||||
{
|
||||
payload.KeepDisplayOn = settings.Properties.KeepDisplayOn;
|
||||
}
|
||||
|
||||
if (settings.Properties.Mode == AwakeMode.TIMED || settings.Properties.Mode == AwakeMode.EXPIRABLE)
|
||||
{
|
||||
payload.IntervalHours = settings.Properties.IntervalHours;
|
||||
payload.IntervalMinutes = settings.Properties.IntervalMinutes;
|
||||
payload.ExpirationDateTime = settings.Properties.ExpirationDateTime.ToString("O");
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
internal static AwakeStatusPayload CreateInactive()
|
||||
{
|
||||
return new AwakeStatusPayload
|
||||
{
|
||||
Mode = "inactive",
|
||||
Summary = "PowerToys Awake is not running because PowerToys is not active.",
|
||||
};
|
||||
}
|
||||
|
||||
internal static AwakeStatusPayload CreateUnknownActive()
|
||||
{
|
||||
return new AwakeStatusPayload
|
||||
{
|
||||
Mode = "unknown",
|
||||
Summary = "An Awake process is currently running, but its configuration cannot be determined. To terminate the existing process and start with new settings, use force=true.",
|
||||
};
|
||||
}
|
||||
|
||||
internal static AwakeStatusPayload CreateError(string errorMessage, AwakeSettings? settings = null)
|
||||
{
|
||||
var payload = new AwakeStatusPayload
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
};
|
||||
|
||||
if (settings != null)
|
||||
{
|
||||
payload.Mode = settings.Properties.Mode.ToString().ToLowerInvariant();
|
||||
|
||||
// Only include properties relevant to the current mode
|
||||
if (settings.Properties.Mode != AwakeMode.PASSIVE)
|
||||
{
|
||||
payload.KeepDisplayOn = settings.Properties.KeepDisplayOn;
|
||||
}
|
||||
|
||||
if (settings.Properties.Mode == AwakeMode.TIMED || settings.Properties.Mode == AwakeMode.EXPIRABLE)
|
||||
{
|
||||
payload.IntervalHours = settings.Properties.IntervalHours;
|
||||
payload.IntervalMinutes = settings.Properties.IntervalMinutes;
|
||||
payload.ExpirationDateTime = settings.Properties.ExpirationDateTime.ToString("O");
|
||||
}
|
||||
|
||||
payload.Summary = "An Awake session is already active with the current settings. To override and change the configuration, use force=true.";
|
||||
}
|
||||
else
|
||||
{
|
||||
payload.Mode = "unknown";
|
||||
payload.Summary = "An Awake process is currently running. To terminate the existing process and start with new settings, use force=true.";
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,5 +63,28 @@
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.McpServer" Executable="PowerToys.McpServer.exe" uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="win32App">
|
||||
<uap:VisualElements
|
||||
AppListEntry="none"
|
||||
DisplayName="PowerToys.McpServer"
|
||||
Description="PowerToys MCP Server"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
</uap:VisualElements>
|
||||
<Extensions>
|
||||
<uap3:Extension Category="windows.appExtension">
|
||||
<uap3:AppExtension
|
||||
Name="com.microsoft.windows.ai.mcpServer"
|
||||
Id="PowerToys.McpServer"
|
||||
DisplayName="PowerToys MCP Server"
|
||||
PublicFolder="Assets">
|
||||
<uap3:Properties>
|
||||
<Registration>mcpServerConfig.json</Registration>
|
||||
</uap3:Properties>
|
||||
</uap3:AppExtension>
|
||||
</uap3:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
</Package>
|
||||
18
src/PackageIdentity/Assets/mcpServerConfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"manifest_version": "0.1",
|
||||
"name": "PowerToys-McpServer",
|
||||
"version": "1.0.0",
|
||||
"description": "PowerToys McpServer",
|
||||
"author": {
|
||||
"name": "Microsoft"
|
||||
},
|
||||
"server": {
|
||||
"type": "binary",
|
||||
"entry_point": "PowerToys.McpServer.exe",
|
||||
"mcp_config": {
|
||||
"command": "PowerToys.McpServer.exe",
|
||||
"args": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,7 @@ Param(
|
||||
|
||||
[switch]$Clean,
|
||||
[switch]$ForceCert,
|
||||
[switch]$NoSign,
|
||||
[switch]$CIBuild
|
||||
[switch]$NoSign
|
||||
)
|
||||
|
||||
# PowerToys sparse packaging helper.
|
||||
@@ -23,7 +22,7 @@ Param(
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$isCIBuild = $false
|
||||
if ($CIBuild.IsPresent) {
|
||||
if ($NoSign.IsPresent) {
|
||||
$isCIBuild = $true
|
||||
} elseif ($env:CIBuild) {
|
||||
$isCIBuild = $env:CIBuild -ieq 'true'
|
||||
@@ -292,6 +291,7 @@ try {
|
||||
$essentialFiles = @(
|
||||
"AppxManifest.xml"
|
||||
"Images\*"
|
||||
"Assets\*"
|
||||
)
|
||||
|
||||
foreach ($filePattern in $essentialFiles) {
|
||||
@@ -310,7 +310,11 @@ try {
|
||||
$sourceDir = $sourcePath.TrimEnd('\*')
|
||||
$targetDir = Join-Path $stagingDir (Split-Path $relativePath.TrimEnd('\*') -Parent)
|
||||
if (Test-Path $sourceDir) {
|
||||
Copy-Item -Path "$sourceDir\*" -Destination $targetDir -Force -ErrorAction SilentlyContinue
|
||||
$targetSubDir = Join-Path $stagingDir ($relativePath.TrimEnd('\*'))
|
||||
if (-not (Test-Path $targetSubDir)) {
|
||||
New-Item -ItemType Directory -Path $targetSubDir -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path "$sourceDir\*" -Destination $targetSubDir -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
} else {
|
||||
# Copy single file
|
||||
|
||||
@@ -2,22 +2,20 @@
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- CI Build Configuration -->
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true' and '$(ForceSign)' != 'true'">
|
||||
<ForceCIPackaging>true</ForceCIPackaging>
|
||||
<NoSignCI>true</NoSignCI>
|
||||
<EffectiveCIBuild>true</EffectiveCIBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Target to generate sparse MSIX package -->
|
||||
<Target Name="GenerateSparsePackage" BeforeTargets="PrepareForBuild">
|
||||
<!-- Use NoSign only for CI builds to avoid certificate issues on hosted agents -->
|
||||
<PropertyGroup>
|
||||
<NoSignParam Condition="'$(NoSignCI)' == 'true'">-NoSign</NoSignParam>
|
||||
<NoSignParam Condition="'$(NoSignCI)' != 'true'"></NoSignParam>
|
||||
<CIBuildParam Condition="'$(CIBuild)' == 'true'">-CIBuild</CIBuildParam>
|
||||
<CIBuildParam Condition="'$(CIBuild)' != 'true'"></CIBuildParam>
|
||||
<NoSignParam Condition="'$(EffectiveCIBuild)' == 'true'">-NoSign</NoSignParam>
|
||||
<NoSignParam Condition="'$(EffectiveCIBuild)' != 'true'"></NoSignParam>
|
||||
</PropertyGroup>
|
||||
|
||||
<Exec Command="powershell -NonInteractive -ExecutionPolicy Bypass -File "$(MSBuildThisFileDirectory)BuildSparsePackage.ps1" -Platform $(Platform) -Configuration $(Configuration) $(NoSignParam) $(CIBuildParam)"
|
||||
<Exec Command="pwsh -NonInteractive -ExecutionPolicy Bypass -File "$(MSBuildThisFileDirectory)BuildSparsePackage.ps1" -Platform $(Platform) -Configuration $(Configuration) $(NoSignParam)"
|
||||
ContinueOnError="false"
|
||||
WorkingDirectory="$(MSBuildThisFileDirectory)" />
|
||||
</Target>
|
||||
@@ -112,6 +110,12 @@
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Assets\mcpServerConfig.json">
|
||||
<Filter>Assets</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
||||
@@ -5,11 +5,18 @@
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>png;jpg;jpeg;gif;bmp;ico</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Assets">
|
||||
<UniqueIdentifier>{B3E94A82-1F2C-4D3E-A5F6-789ABC012DEF}</UniqueIdentifier>
|
||||
<Extensions>json;xml;txt</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="AppxManifest.xml" />
|
||||
<None Include="BuildSparsePackage.ps1" />
|
||||
<None Include="BuildSparsePackage.cmd" />
|
||||
<None Include="Assets\mcpServerConfig.json">
|
||||
<Filter>Assets</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="Images\Square150x150Logo.png">
|
||||
|
||||
@@ -66,5 +66,11 @@ namespace PowerToys.GPOWrapperProjection
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
|
||||
}
|
||||
|
||||
public static GpoRuleConfigured GetConfiguredMcpEnabledValue()
|
||||
{
|
||||
// MCP doesn't have GPO support yet, always return NotConfigured
|
||||
return GpoRuleConfigured.NotConfigured;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
src/common/LanguageModelProvider/AppUtils.cs
Normal file
@@ -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 LanguageModelProvider;
|
||||
|
||||
internal static class AppUtils
|
||||
{
|
||||
public static string GetThemeAssetSuffix()
|
||||
{
|
||||
// Default suffix for assets that are theme-agnostic today.
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -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 LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record FoundryCachedModel(string Name, string? Id);
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record FoundryCatalogModel
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("displayName")]
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("providerType")]
|
||||
public string ProviderType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("uri")]
|
||||
public string Uri { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("modelType")]
|
||||
public string ModelType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("promptTemplate")]
|
||||
public PromptTemplate PromptTemplate { get; init; } = default!;
|
||||
|
||||
[JsonPropertyName("publisher")]
|
||||
public string Publisher { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("task")]
|
||||
public string Task { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("runtime")]
|
||||
public Runtime Runtime { get; init; } = default!;
|
||||
|
||||
[JsonPropertyName("fileSizeMb")]
|
||||
public long FileSizeMb { get; init; }
|
||||
|
||||
[JsonPropertyName("modelSettings")]
|
||||
public ModelSettings ModelSettings { get; init; } = default!;
|
||||
|
||||
[JsonPropertyName("alias")]
|
||||
public string Alias { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("supportsToolCalling")]
|
||||
public bool SupportsToolCalling { get; init; }
|
||||
|
||||
[JsonPropertyName("license")]
|
||||
public string License { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("licenseDescription")]
|
||||
public string LicenseDescription { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("parentModelUri")]
|
||||
public string ParentModelUri { get; init; } = string.Empty;
|
||||
}
|
||||
229
src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed class FoundryClient
|
||||
{
|
||||
public static async Task<FoundryClient?> CreateAsync()
|
||||
{
|
||||
var serviceManager = FoundryServiceManager.TryCreate();
|
||||
if (serviceManager is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!await serviceManager.IsRunning().ConfigureAwait(false))
|
||||
{
|
||||
if (!await serviceManager.StartService().ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var serviceUrl = await serviceManager.GetServiceUrl().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(serviceUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var serviceUri = new Uri(serviceUrl, UriKind.Absolute);
|
||||
var baseAddress = serviceUri.AbsoluteUri.EndsWith('/')
|
||||
? serviceUri
|
||||
: new Uri(serviceUri, "/");
|
||||
|
||||
var httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = baseAddress,
|
||||
Timeout = TimeSpan.FromHours(2),
|
||||
};
|
||||
|
||||
var assemblyVersion = typeof(FoundryClient).Assembly.GetName().Version?.ToString() ?? "unknown";
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"foundry-local-cs-sdk/{assemblyVersion}");
|
||||
|
||||
return new FoundryClient(serviceManager, httpClient);
|
||||
}
|
||||
|
||||
public FoundryServiceManager ServiceManager { get; }
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly List<FoundryCatalogModel> _catalogModels = [];
|
||||
|
||||
private FoundryClient(FoundryServiceManager serviceManager, HttpClient httpClient)
|
||||
{
|
||||
ServiceManager = serviceManager;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<FoundryCatalogModel>> ListCatalogModels()
|
||||
{
|
||||
if (_catalogModels.Count > 0)
|
||||
{
|
||||
return _catalogModels;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/foundry/list").ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var models = await JsonSerializer.DeserializeAsync(
|
||||
response.Content.ReadAsStream(),
|
||||
FoundryJsonContext.Default.ListFoundryCatalogModel).ConfigureAwait(false);
|
||||
|
||||
if (models is { Count: > 0 })
|
||||
{
|
||||
models.ForEach(_catalogModels.Add);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Surfacing errors here prevents listing other providers; swallow and return cached list instead.
|
||||
}
|
||||
|
||||
return _catalogModels;
|
||||
}
|
||||
|
||||
public async Task<List<FoundryCachedModel>> ListCachedModels()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/openai/models").ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var catalogModels = await ListCatalogModels().ConfigureAwait(false);
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var modelIds = content
|
||||
.Trim('[', ']')
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(id => id.Trim('"'));
|
||||
|
||||
List<FoundryCachedModel> models = [];
|
||||
|
||||
foreach (var id in modelIds)
|
||||
{
|
||||
var model = catalogModels.FirstOrDefault(m => m.Name == id);
|
||||
models.Add(model != null ? new FoundryCachedModel(id, model.Alias) : new FoundryCachedModel(id, null));
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
public async Task<FoundryDownloadResult> DownloadModel(FoundryCatalogModel model, IProgress<float>? progress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var models = await ListCachedModels().ConfigureAwait(false);
|
||||
|
||||
if (models.Any(m => m.Name == model.Name))
|
||||
{
|
||||
return new(true, "Model already downloaded");
|
||||
}
|
||||
|
||||
return await Task.Run(
|
||||
async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var providerType = model.ProviderType.EndsWith("Local", StringComparison.OrdinalIgnoreCase)
|
||||
? model.ProviderType
|
||||
: $"{model.ProviderType}Local";
|
||||
|
||||
var downloadRequest = new FoundryDownloadBody
|
||||
{
|
||||
Model = new FoundryModelDownload
|
||||
{
|
||||
Name = model.Name,
|
||||
Uri = model.Uri,
|
||||
Publisher = model.Publisher,
|
||||
ProviderType = providerType,
|
||||
PromptTemplate = model.PromptTemplate,
|
||||
},
|
||||
Token = string.Empty,
|
||||
IgnorePipeReport = true,
|
||||
};
|
||||
|
||||
var downloadBodyContext = FoundryJsonContext.Default.FoundryDownloadBody;
|
||||
string body = JsonSerializer.Serialize(downloadRequest, downloadBodyContext);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/openai/download")
|
||||
{
|
||||
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
||||
};
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
StringBuilder jsonBuilder = new();
|
||||
var collectingJson = false;
|
||||
var completed = false;
|
||||
|
||||
while (!completed && (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("Total", StringComparison.CurrentCultureIgnoreCase) &&
|
||||
line.Contains("Downloading", StringComparison.OrdinalIgnoreCase) &&
|
||||
line.Contains('%'))
|
||||
{
|
||||
var percentStr = line.Split('%')[0].Split(' ').Last();
|
||||
if (double.TryParse(percentStr, NumberStyles.Float, CultureInfo.CurrentCulture, out var percentage))
|
||||
{
|
||||
progress?.Report((float)(percentage / 100));
|
||||
}
|
||||
}
|
||||
else if (line.Contains("[DONE]", StringComparison.OrdinalIgnoreCase) ||
|
||||
line.Contains("All Completed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
collectingJson = true;
|
||||
}
|
||||
else if (collectingJson && line.TrimStart().StartsWith('{'))
|
||||
{
|
||||
jsonBuilder.AppendLine(line);
|
||||
}
|
||||
else if (collectingJson && jsonBuilder.Length > 0)
|
||||
{
|
||||
jsonBuilder.AppendLine(line);
|
||||
if (line.Trim() == "}")
|
||||
{
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var downloadResultContext = FoundryJsonContext.Default.FoundryDownloadResult;
|
||||
var jsonPayload = jsonBuilder.Length > 0 ? jsonBuilder.ToString() : null;
|
||||
|
||||
if (jsonPayload is null)
|
||||
{
|
||||
return new FoundryDownloadResult(false, "No completion response received from server.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize(jsonPayload, downloadResultContext)
|
||||
?? new FoundryDownloadResult(false, "Failed to parse completion response.");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return new FoundryDownloadResult(false, $"Failed to parse completion response: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new FoundryDownloadResult(false, e.Message);
|
||||
}
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed class FoundryDownloadBody
|
||||
{
|
||||
[JsonPropertyName("Model")]
|
||||
public required FoundryModelDownload Model { get; init; }
|
||||
|
||||
[JsonPropertyName("token")]
|
||||
public required string Token { get; init; }
|
||||
|
||||
[JsonPropertyName("IgnorePipeReport")]
|
||||
public required bool IgnorePipeReport { get; init; }
|
||||
}
|
||||
@@ -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 LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record FoundryDownloadResult(bool Success, string? ErrorMessage);
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
WriteIndented = false)]
|
||||
[JsonSerializable(typeof(FoundryCatalogModel))]
|
||||
[JsonSerializable(typeof(List<FoundryCatalogModel>))]
|
||||
[JsonSerializable(typeof(FoundryDownloadResult))]
|
||||
[JsonSerializable(typeof(FoundryModelDownload))]
|
||||
[JsonSerializable(typeof(FoundryDownloadBody))]
|
||||
internal sealed partial class FoundryJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed class FoundryModelDownload
|
||||
{
|
||||
[JsonPropertyName("Name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("Uri")]
|
||||
public required string Uri { get; init; }
|
||||
|
||||
[JsonPropertyName("Publisher")]
|
||||
public required string Publisher { get; init; }
|
||||
|
||||
[JsonPropertyName("ProviderType")]
|
||||
public required string ProviderType { get; init; }
|
||||
|
||||
[JsonPropertyName("PromptTemplate")]
|
||||
public required PromptTemplate? PromptTemplate { get; init; }
|
||||
}
|
||||
@@ -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 System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed class FoundryServiceManager
|
||||
{
|
||||
public static FoundryServiceManager? TryCreate()
|
||||
{
|
||||
return IsAvailable() ? new FoundryServiceManager() : null;
|
||||
}
|
||||
|
||||
private static bool IsAvailable()
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo.FileName = "where";
|
||||
process.StartInfo.Arguments = "foundry";
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
|
||||
private static string? GetUrl(string output)
|
||||
{
|
||||
var match = Regex.Match(output, @"https?:\/\/[^\/]+:\d+");
|
||||
return match.Success ? match.Value : null;
|
||||
}
|
||||
|
||||
public async Task<string?> GetServiceUrl()
|
||||
{
|
||||
var status = await Utils.RunFoundryWithArguments("service status").ConfigureAwait(false);
|
||||
|
||||
if (status.ExitCode != 0 || string.IsNullOrWhiteSpace(status.Output))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetUrl(status.Output);
|
||||
}
|
||||
|
||||
public async Task<bool> IsRunning()
|
||||
{
|
||||
var url = await GetServiceUrl().ConfigureAwait(false);
|
||||
return url is not null;
|
||||
}
|
||||
|
||||
public async Task<bool> StartService(int maxWaitSeconds = 30)
|
||||
{
|
||||
if (await IsRunning().ConfigureAwait(false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start foundry service (fire-and-forget, don't wait for the process to exit)
|
||||
_ = Task.Run(() => Utils.RunFoundryWithArguments("service start"));
|
||||
|
||||
// Poll to check if service is running
|
||||
int elapsedSeconds = 0;
|
||||
while (elapsedSeconds < maxWaitSeconds)
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
elapsedSeconds++;
|
||||
|
||||
if (await IsRunning().ConfigureAwait(false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record ModelSettings
|
||||
{
|
||||
// The sample shows an empty array; keep it open-ended.
|
||||
[JsonPropertyName("parameters")]
|
||||
public List<JsonElement> Parameters { get; init; } = [];
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record PromptTemplate
|
||||
{
|
||||
[JsonPropertyName("assistant")]
|
||||
public string Assistant { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("prompt")]
|
||||
public string Prompt { get; init; } = string.Empty;
|
||||
}
|
||||
16
src/common/LanguageModelProvider/FoundryLocal/Runtime.cs
Normal file
@@ -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.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal sealed record Runtime
|
||||
{
|
||||
[JsonPropertyName("deviceType")]
|
||||
public string DeviceType { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("executionProvider")]
|
||||
public string ExecutionProvider { get; init; } = string.Empty;
|
||||
}
|
||||
37
src/common/LanguageModelProvider/FoundryLocal/Utils.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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;
|
||||
|
||||
namespace LanguageModelProvider.FoundryLocal;
|
||||
|
||||
internal static class Utils
|
||||
{
|
||||
public static async Task<(string? Output, string? Error, int ExitCode)> RunFoundryWithArguments(string arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo.FileName = "foundry";
|
||||
process.StartInfo.Arguments = arguments;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
|
||||
process.Start();
|
||||
|
||||
string? output = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
|
||||
string? error = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
await process.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
return (output, error, process.ExitCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (null, null, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/common/LanguageModelProvider/FoundryLocalModelProvider.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
// 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.ClientModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LanguageModelProvider.FoundryLocal;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
{
|
||||
private IEnumerable<ModelDetails>? _downloadedModels;
|
||||
private IEnumerable<ModelDetails>? _catalogModels;
|
||||
private FoundryClient? _foundryManager;
|
||||
private string? _serviceUrl;
|
||||
|
||||
public static FoundryLocalModelProvider Instance { get; } = new();
|
||||
|
||||
public string Name => "FoundryLocal";
|
||||
|
||||
public HardwareAccelerator ModelHardwareAccelerator => HardwareAccelerator.FOUNDRYLOCAL;
|
||||
|
||||
public string ProviderDescription => "The model will run locally via Foundry Local";
|
||||
|
||||
public string UrlPrefix => "fl://";
|
||||
|
||||
public string Icon => $"fl{AppUtils.GetThemeAssetSuffix()}.svg";
|
||||
|
||||
public string Url => _serviceUrl ?? string.Empty;
|
||||
|
||||
public string GetDetailsUrl(ModelDetails details)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IChatClient? GetIChatClient(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_serviceUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var modelId = url.Split('/').LastOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new OpenAIClient(
|
||||
new ApiKeyCredential("none"),
|
||||
new OpenAIClientOptions { Endpoint = new Uri($"{_serviceUrl}/v1") })
|
||||
.GetChatClient(modelId)
|
||||
.AsIChatClient();
|
||||
}
|
||||
|
||||
public string GetIChatClientString(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var modelId = url.Split('/').LastOrDefault();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_serviceUrl) || string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (ignoreCached)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
await InitializeAsync(cancelationToken);
|
||||
|
||||
return _downloadedModels ?? [];
|
||||
}
|
||||
|
||||
public IEnumerable<ModelDetails> GetAllModelsInCatalog()
|
||||
{
|
||||
return _catalogModels ?? [];
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadModel(ModelDetails modelDetails, IProgress<float>? progress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_foundryManager == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (modelDetails.ProviderModelDetails is not FoundryCatalogModel model)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (await _foundryManager.DownloadModel(model, progress, cancellationToken)).Success;
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_downloadedModels = null;
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (_foundryManager != null && _downloadedModels != null && _downloadedModels.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_foundryManager ??= await FoundryClient.CreateAsync();
|
||||
|
||||
if (_foundryManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_serviceUrl ??= await _foundryManager.ServiceManager.GetServiceUrl();
|
||||
|
||||
if (_catalogModels == null || !_catalogModels.Any())
|
||||
{
|
||||
_catalogModels = (await _foundryManager.ListCatalogModels()).Select(ToModelDetails).ToArray();
|
||||
}
|
||||
|
||||
var cachedModels = await _foundryManager.ListCachedModels();
|
||||
|
||||
List<ModelDetails> downloadedModels = [];
|
||||
|
||||
foreach (var model in _catalogModels)
|
||||
{
|
||||
var cachedModel = cachedModels.FirstOrDefault(m => m.Name == model.Name);
|
||||
|
||||
if (cachedModel != default)
|
||||
{
|
||||
model.Id = $"{UrlPrefix}{cachedModel.Id}";
|
||||
downloadedModels.Add(model);
|
||||
cachedModels.Remove(cachedModel);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var model in cachedModels)
|
||||
{
|
||||
downloadedModels.Add(new ModelDetails
|
||||
{
|
||||
Id = $"fl-{model.Name}",
|
||||
Name = model.Name,
|
||||
Url = $"{UrlPrefix}{model.Name}",
|
||||
Description = $"{model.Name} running locally with Foundry Local",
|
||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||
SupportedOnQualcomm = true,
|
||||
ProviderModelDetails = model,
|
||||
});
|
||||
}
|
||||
|
||||
_downloadedModels = downloadedModels;
|
||||
}
|
||||
|
||||
private ModelDetails ToModelDetails(FoundryCatalogModel model)
|
||||
{
|
||||
return new ModelDetails
|
||||
{
|
||||
Id = $"fl-{model.Name}",
|
||||
Name = model.Name,
|
||||
Url = $"{UrlPrefix}{model.Name}",
|
||||
Description = $"{model.Alias} running locally with Foundry Local",
|
||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||
Size = model.FileSizeMb * 1024 * 1024,
|
||||
SupportedOnQualcomm = true,
|
||||
License = model.License?.ToLowerInvariant() ?? string.Empty,
|
||||
ProviderModelDetails = model,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> IsAvailable()
|
||||
{
|
||||
await InitializeAsync();
|
||||
return _foundryManager != null;
|
||||
}
|
||||
}
|
||||
22
src/common/LanguageModelProvider/HardwareAccelerator.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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 LanguageModelProvider;
|
||||
|
||||
public enum HardwareAccelerator
|
||||
{
|
||||
CPU,
|
||||
DML,
|
||||
QNN,
|
||||
WCRAPI,
|
||||
OLLAMA,
|
||||
OPENAI,
|
||||
FOUNDRYLOCAL,
|
||||
LEMONADE,
|
||||
NPU,
|
||||
GPU,
|
||||
VitisAI,
|
||||
OpenVINO,
|
||||
NvTensorRT,
|
||||
}
|
||||
30
src/common/LanguageModelProvider/ILanguageModelProvider.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public interface ILanguageModelProvider
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
string UrlPrefix { get; }
|
||||
|
||||
string Icon { get; }
|
||||
|
||||
HardwareAccelerator ModelHardwareAccelerator { get; }
|
||||
|
||||
string ProviderDescription { get; }
|
||||
|
||||
Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
|
||||
|
||||
IChatClient? GetIChatClient(string url);
|
||||
|
||||
string GetIChatClientString(string url);
|
||||
|
||||
string GetDetailsUrl(ModelDetails details);
|
||||
|
||||
string Url { get; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="OpenAI" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
106
src/common/LanguageModelProvider/LanguageModelService.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// 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.Concurrent;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public sealed class LanguageModelService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ILanguageModelProvider> _providersByPrefix;
|
||||
|
||||
public LanguageModelService(IEnumerable<ILanguageModelProvider> providers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providers);
|
||||
|
||||
_providersByPrefix = new ConcurrentDictionary<string, ILanguageModelProvider>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(provider.UrlPrefix))
|
||||
{
|
||||
_providersByPrefix[provider.UrlPrefix] = provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static LanguageModelService CreateDefault()
|
||||
{
|
||||
return new LanguageModelService(new[]
|
||||
{
|
||||
FoundryLocalModelProvider.Instance,
|
||||
});
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ILanguageModelProvider> Providers => _providersByPrefix.Values.ToArray();
|
||||
|
||||
public bool RegisterProvider(ILanguageModelProvider provider)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(provider.UrlPrefix))
|
||||
{
|
||||
throw new ArgumentException("Provider must supply a URL prefix.", nameof(provider));
|
||||
}
|
||||
|
||||
_providersByPrefix[provider.UrlPrefix] = provider;
|
||||
return true;
|
||||
}
|
||||
|
||||
public ILanguageModelProvider? GetProviderFor(string? modelReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var provider in _providersByPrefix.Values)
|
||||
{
|
||||
if (modelReference.StartsWith(provider.UrlPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ModelDetails>> GetModelsAsync(bool refresh = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<ModelDetails> models = [];
|
||||
|
||||
foreach (var provider in _providersByPrefix.Values)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var providerModels = await provider.GetModelsAsync(refresh, cancellationToken).ConfigureAwait(false);
|
||||
models.AddRange(providerModels);
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
public IChatClient? GetClient(ModelDetails model)
|
||||
{
|
||||
if (model is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var reference = !string.IsNullOrWhiteSpace(model.Url) ? model.Url : model.Id;
|
||||
return GetClient(reference);
|
||||
}
|
||||
|
||||
public IChatClient? GetClient(string? modelReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = GetProviderFor(modelReference);
|
||||
|
||||
return provider?.GetIChatClient(modelReference);
|
||||
}
|
||||
}
|
||||
32
src/common/LanguageModelProvider/ModelDetails.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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;
|
||||
|
||||
namespace LanguageModelProvider;
|
||||
|
||||
public class ModelDetails
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
public bool IsUserAdded { get; set; }
|
||||
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
|
||||
public List<HardwareAccelerator> HardwareAccelerators { get; set; } = [];
|
||||
|
||||
public bool SupportedOnQualcomm { get; set; }
|
||||
|
||||
public string License { get; set; } = string.Empty;
|
||||
|
||||
public object? ProviderModelDetails { get; set; }
|
||||
}
|
||||
@@ -23,7 +23,8 @@ public sealed class SettingsResourceAdvancedPasteModuleTest : SettingsResourceMo
|
||||
{
|
||||
s.Properties.ShowCustomPreview = !s.Properties.ShowCustomPreview;
|
||||
s.Properties.CloseAfterLosingFocus = !s.Properties.CloseAfterLosingFocus;
|
||||
s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled;
|
||||
|
||||
// s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled;
|
||||
s.Properties.AdvancedPasteUIShortcut = new HotkeySettings
|
||||
{
|
||||
Key = "mock",
|
||||
|
||||
@@ -33,11 +33,26 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="AdvancedPasteXAML\Controls\ClipboardHistoryItemPreviewControl.xaml" />
|
||||
<None Remove="AdvancedPasteXAML\Controls\PromptBox.xaml" />
|
||||
<None Remove="AdvancedPasteXAML\Styles\Button.xaml" />
|
||||
<None Remove="Assets\AdvancedPaste\AIIcon.png" />
|
||||
<None Remove="Assets\AdvancedPaste\Anthropic.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\Azure.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\AzureAI.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\Bedrock.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\FoundryLocal.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\Gemini.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\Gradient.png" />
|
||||
<None Remove="AdvancedPasteXAML\Controls\AnimatedContentControl\AnimatedBorderBrush.xaml" />
|
||||
<None Remove="AdvancedPasteXAML\Views\MainPage.xaml" />
|
||||
<None Remove="Assets\AdvancedPaste\HuggingFace.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\Mistral.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\Ollama.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\Onnx.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\OpenAI.dark.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\OpenAI.light.svg" />
|
||||
<None Remove="Assets\AdvancedPaste\WindowsML.svg" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -49,7 +64,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenAI" />
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
@@ -57,10 +71,17 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageReference Include="MessagePack" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Amazon" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Google" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.HuggingFace" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.MistralAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
<PackageReference Include="System.ClientModel" />
|
||||
<PackageReference Include="Microsoft.Windows.Compatibility" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
@@ -102,6 +123,7 @@
|
||||
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
@@ -114,9 +136,19 @@
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="AdvancedPasteXAML\Controls\ClipboardHistoryItemPreviewControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="AdvancedPasteXAML\Controls\PromptBox.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="AdvancedPasteXAML\Styles\Button.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<ResourceDictionary Source="ms-appx:///AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///AdvancedPasteXAML/Styles/Button.xaml" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
|
||||
@@ -10,10 +10,10 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.ViewModels;
|
||||
using ManagedCommon;
|
||||
@@ -77,11 +77,12 @@ namespace AdvancedPaste
|
||||
{
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
services.AddSingleton<IUserSettings, UserSettings>();
|
||||
services.AddSingleton<IAICredentialsProvider, Services.OpenAI.VaultCredentialsProvider>();
|
||||
services.AddSingleton<IAICredentialsProvider, EnhancedVaultCredentialsProvider>();
|
||||
services.AddSingleton<IPromptModerationService, Services.OpenAI.PromptModerationService>();
|
||||
services.AddSingleton<ICustomTextTransformService, Services.OpenAI.CustomTextTransformService>();
|
||||
services.AddSingleton<IKernelQueryCacheService, CustomActionKernelQueryCacheService>();
|
||||
services.AddSingleton<IKernelService, Services.OpenAI.KernelService>();
|
||||
services.AddSingleton<IPasteAIProviderFactory, PasteAIProviderFactory>();
|
||||
services.AddSingleton<ICustomActionTransformService, CustomActionTransformService>();
|
||||
services.AddSingleton<IKernelService, AdvancedAIKernelService>();
|
||||
services.AddSingleton<IPasteFormatExecutor, PasteFormatExecutor>();
|
||||
services.AddSingleton<OptionsViewModel>();
|
||||
}).Build();
|
||||
@@ -111,7 +112,11 @@ namespace AdvancedPaste
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
#if DEBUG
|
||||
protected async override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
#else
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
#endif
|
||||
{
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
if (cmdArgs?.Length > 1)
|
||||
@@ -133,6 +138,10 @@ namespace AdvancedPaste
|
||||
{
|
||||
ProcessNamedPipe(cmdArgs[2]);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
await ShowWindow(); // This allows for direct access without using PowerToys Runner, not all functionality might work
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ProcessNamedPipe(string pipeName)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:AnimatedContentControl">
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="AdvancedPaste.Controls.ClipboardHistoryItemPreviewControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:AdvancedPaste.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:AdvancedPaste.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<converters:DateTimeToFriendlyStringConverter x:Key="DateTimeToFriendlyStringConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter x:Name="BoolToVisibilityConverter" />
|
||||
</UserControl.Resources>
|
||||
<Grid Height="64" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Background="{ThemeResource SubtleFillColorSecondaryBrush}" CornerRadius="16,0,0,16">
|
||||
<Grid>
|
||||
<!-- Image preview -->
|
||||
<Image
|
||||
Source="{x:Bind ClipboardItem.Image, Mode=OneWay}"
|
||||
Stretch="UniformToFill"
|
||||
Visibility="{x:Bind HasImage, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<!-- Text preview -->
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ClipboardItem.Content, Mode=OneWay}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{x:Bind HasText, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<!-- Icon glyph fallback -->
|
||||
<FontIcon
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="48"
|
||||
Glyph="{x:Bind IconGlyph, Mode=OneWay}"
|
||||
Visibility="{x:Bind HasGlyph, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind Header, Mode=OneWay}"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Timestamp, Converter={StaticResource DateTimeToFriendlyStringConverter}, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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 AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace AdvancedPaste.Controls
|
||||
{
|
||||
public sealed partial class ClipboardHistoryItemPreviewControl : UserControl
|
||||
{
|
||||
public static readonly DependencyProperty ClipboardItemProperty = DependencyProperty.Register(
|
||||
nameof(ClipboardItem),
|
||||
typeof(ClipboardItem),
|
||||
typeof(ClipboardHistoryItemPreviewControl),
|
||||
new PropertyMetadata(defaultValue: null, OnClipboardItemChanged));
|
||||
|
||||
public ClipboardItem ClipboardItem
|
||||
{
|
||||
get => (ClipboardItem)GetValue(ClipboardItemProperty);
|
||||
set => SetValue(ClipboardItemProperty, value);
|
||||
}
|
||||
|
||||
// Computed properties for display
|
||||
public string Header => ClipboardItem != null ? GetHeaderFromFormat(ClipboardItem.Format) : string.Empty;
|
||||
|
||||
public string IconGlyph => ClipboardItem != null ? GetGlyphFromFormat(ClipboardItem.Format) : string.Empty;
|
||||
|
||||
public string ContentText => ClipboardItem?.Content ?? string.Empty;
|
||||
|
||||
public ImageSource ContentImage => ClipboardItem?.Image;
|
||||
|
||||
public DateTimeOffset? Timestamp => ClipboardItem?.Timestamp ?? ClipboardItem?.Item?.Timestamp;
|
||||
|
||||
public bool HasImage => ContentImage is not null;
|
||||
|
||||
public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage;
|
||||
|
||||
public bool HasGlyph => !HasImage && !HasText && !string.IsNullOrEmpty(IconGlyph);
|
||||
|
||||
public ClipboardHistoryItemPreviewControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private static void OnClipboardItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ClipboardHistoryItemPreviewControl control)
|
||||
{
|
||||
// Notify bindings that all computed properties may have changed
|
||||
control.Bindings.Update();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetHeaderFromFormat(ClipboardFormat format)
|
||||
{
|
||||
// Check flags in priority order (most specific first)
|
||||
if (format.HasFlag(ClipboardFormat.Image))
|
||||
{
|
||||
return GetStringOrFallback("ClipboardPreviewCategoryImage", "Image");
|
||||
}
|
||||
|
||||
if (format.HasFlag(ClipboardFormat.Video))
|
||||
{
|
||||
return GetStringOrFallback("ClipboardPreviewCategoryVideo", "Video");
|
||||
}
|
||||
|
||||
if (format.HasFlag(ClipboardFormat.Audio))
|
||||
{
|
||||
return GetStringOrFallback("ClipboardPreviewCategoryAudio", "Audio");
|
||||
}
|
||||
|
||||
if (format.HasFlag(ClipboardFormat.File))
|
||||
{
|
||||
return GetStringOrFallback("ClipboardPreviewCategoryFile", "File");
|
||||
}
|
||||
|
||||
if (format.HasFlag(ClipboardFormat.Text) || format.HasFlag(ClipboardFormat.Html))
|
||||
{
|
||||
return GetStringOrFallback("ClipboardPreviewCategoryText", "Text");
|
||||
}
|
||||
|
||||
return GetStringOrFallback("ClipboardPreviewCategoryUnknown", "Clipboard");
|
||||
}
|
||||
|
||||
private static string GetGlyphFromFormat(ClipboardFormat format)
|
||||
{
|
||||
// Check flags in priority order (most specific first)
|
||||
if (format.HasFlag(ClipboardFormat.Image))
|
||||
{
|
||||
return "\uEB9F"; // Image icon
|
||||
}
|
||||
|
||||
if (format.HasFlag(ClipboardFormat.Video))
|
||||
{
|
||||
return "\uE714"; // Video icon
|
||||
}
|
||||
|
||||
if (format.HasFlag(ClipboardFormat.Audio))
|
||||
{
|
||||
return "\uE189"; // Audio icon
|
||||
}
|
||||
|
||||
if (format.HasFlag(ClipboardFormat.File))
|
||||
{
|
||||
return "\uE8A5"; // File icon
|
||||
}
|
||||
|
||||
if (format.HasFlag(ClipboardFormat.Text) || format.HasFlag(ClipboardFormat.Html))
|
||||
{
|
||||
return "\uE8D2"; // Text icon
|
||||
}
|
||||
|
||||
return "\uE77B"; // Generic clipboard icon
|
||||
}
|
||||
|
||||
private static string GetStringOrFallback(string resourceKey, string fallback)
|
||||
{
|
||||
var value = ResourceLoaderInstance.ResourceLoader.GetString(resourceKey);
|
||||
return string.IsNullOrEmpty(value) ? fallback : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:AdvancedPaste.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:settings="using:Microsoft.PowerToys.Settings.UI.Library"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
x:Name="PromptBoxControl"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
@@ -34,7 +36,7 @@
|
||||
<SolidColorBrush x:Key="AccentGradientBrush" Color="{StaticResource AccentGradientColor}" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<x:Double x:Key="ModelSelectorButtonWidth">44</x:Double>
|
||||
<Style x:Key="CustomTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextControlForeground}" />
|
||||
<Setter Property="Background" Value="{ThemeResource TextControlBackground}" />
|
||||
@@ -155,6 +157,7 @@
|
||||
Foreground="{ThemeResource TextControlHeaderForeground}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<Border
|
||||
x:Name="BorderElement"
|
||||
Grid.Row="1"
|
||||
@@ -168,48 +171,19 @@
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Control.IsTemplateFocusTarget="True"
|
||||
CornerRadius="{TemplateBinding CornerRadius}" />
|
||||
<Viewbox
|
||||
Grid.Row="1"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="8,0,0,0">
|
||||
<StackPanel
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<ProgressRing
|
||||
Width="30"
|
||||
Height="30"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
|
||||
IsIndeterminate="{Binding DataContext.HasIndeterminateTransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
Value="{Binding DataContext.TransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
|
||||
|
||||
<StackPanel
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<Image
|
||||
x:Name="AIGlyphImage"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
|
||||
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<PathIcon
|
||||
x:Name="AIGlyph"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Viewbox>
|
||||
<Grid Grid.Row="1" Width="{StaticResource ModelSelectorButtonWidth}">
|
||||
<ProgressRing
|
||||
Width="20"
|
||||
Height="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
|
||||
IsIndeterminate="{Binding DataContext.HasIndeterminateTransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
Value="{Binding DataContext.TransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
|
||||
</Grid>
|
||||
<ScrollViewer
|
||||
x:Name="ContentElement"
|
||||
Grid.Row="1"
|
||||
@@ -279,12 +253,6 @@
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentElement" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="AIGlyph" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="AIGlyphImage" Storyboard.TargetProperty="Opacity">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="0.4" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PlaceholderTextContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding PlaceholderForeground, RelativeSource={RelativeSource TemplatedParent}, TargetNullValue={ThemeResource TextControlPlaceholderForegroundDisabled}}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
@@ -364,6 +332,8 @@
|
||||
FalseValue="Visible"
|
||||
TrueValue="Collapsed" />
|
||||
<converters:CountToVisibilityConverter x:Key="CountToVisibilityConverter" />
|
||||
<converters:CountToInvertedVisibilityConverter x:Key="CountToInvertedVisibilityConverter" />
|
||||
<converters:ServiceTypeToIconConverter x:Key="ServiceTypeToIconConverter" />
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid x:Name="PromptBoxGrid" Loaded="Grid_Loaded">
|
||||
@@ -374,18 +344,19 @@
|
||||
<local:AnimatedContentControl
|
||||
x:Name="Loader"
|
||||
MinHeight="48"
|
||||
CornerRadius="8">
|
||||
CornerRadius="16">
|
||||
<Grid>
|
||||
<TextBox
|
||||
x:Name="InputTxtBox"
|
||||
HorizontalAlignment="Stretch"
|
||||
x:FieldModifier="public"
|
||||
CornerRadius="16"
|
||||
DataContext="{x:Bind ViewModel}"
|
||||
IsEnabled="{x:Bind ViewModel.ClipboardHasData, Mode=OneWay}"
|
||||
KeyDown="InputTxtBox_KeyDown"
|
||||
PlaceholderText="{x:Bind ViewModel.InputTxtBoxPlaceholderText, Mode=OneWay}"
|
||||
Style="{StaticResource CustomTextBoxStyle}"
|
||||
TabIndex="0"
|
||||
TabIndex="1"
|
||||
Text="{x:Bind ViewModel.Query, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="InputTxtBoxTooltip" />
|
||||
@@ -545,6 +516,136 @@
|
||||
</Flyout>
|
||||
</FlyoutBase.AttachedFlyout>
|
||||
</TextBox>
|
||||
<DropDownButton
|
||||
x:Name="AIProviderButton"
|
||||
x:Uid="AIProviderButton"
|
||||
MinWidth="{StaticResource ModelSelectorButtonWidth}"
|
||||
Margin="1,1,0,2"
|
||||
Padding="0,0,4,0"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,1,0"
|
||||
CornerRadius="16,0,0,16"
|
||||
Style="{StaticResource SubtleDropDownButtonStyle}"
|
||||
TabIndex="0"
|
||||
Visibility="{x:Bind ViewModel.IsBusy, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{x:Bind ViewModel.ActiveAIProviderTooltip, Mode=OneWay}" TextWrapping="WrapWholeWords" />
|
||||
</ToolTipService.ToolTip>
|
||||
<DropDownButton.Content>
|
||||
<Image
|
||||
x:Name="AIProviderIcon"
|
||||
Width="16"
|
||||
Height="16"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Source="{x:Bind ViewModel.ActiveAIProvider?.ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}" />
|
||||
</DropDownButton.Content>
|
||||
<DropDownButton.Flyout>
|
||||
<Flyout Placement="Bottom" ShouldConstrainToRootBounds="False">
|
||||
<Grid
|
||||
Width="386"
|
||||
Margin="-4"
|
||||
RowSpacing="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
x:Uid="AIProvidersFlyoutHeader"
|
||||
Grid.Row="0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
<ListView
|
||||
x:Name="AIProviderListView"
|
||||
Grid.Row="1"
|
||||
MaxHeight="320"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
ItemsSource="{x:Bind ViewModel.AIProviders, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectedItem="{x:Bind ViewModel.ActiveAIProvider, Mode=OneWay}"
|
||||
SelectionChanged="AIProviderListView_SelectionChanged"
|
||||
SelectionMode="Single"
|
||||
Visibility="{x:Bind ViewModel.AIProviders.Count, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
|
||||
<ListView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical" Spacing="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListView.ItemsPanel>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="settings:PasteAIProviderDefinition">
|
||||
<Grid Padding="0,8,0,8" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Width="20"
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Source="{x:Bind ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}" />
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="2">
|
||||
<TextBlock Text="{x:Bind DisplayName, Mode=OneWay}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ServiceType, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
Padding="2,0,2,0"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="{ThemeResource ControlStrokeColorSecondary}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind IsLocalModel, Mode=OneWay}">
|
||||
<TextBlock
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Local" />
|
||||
</Border>
|
||||
<!--<Border
|
||||
Grid.Column="2"
|
||||
Padding="2,0,2,0"
|
||||
VerticalAlignment="Center"
|
||||
BorderBrush="{ThemeResource TertiaryButtonBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind EnableAdvanceAI}">
|
||||
<TextBlock
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="ADVANCED" />
|
||||
</Border>-->
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<TextBlock
|
||||
x:Uid="AIProvidersEmptyText"
|
||||
Grid.Row="1"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
TextWrapping="WrapWholeWords"
|
||||
Visibility="{x:Bind ViewModel.AIProviders.Count, Mode=OneWay, Converter={StaticResource CountToInvertedVisibilityConverter}}" />
|
||||
<HyperlinkButton
|
||||
x:Uid="AIProvidersManageButtonContent"
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Bind ViewModel.OpenSettingsCommand, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</DropDownButton.Flyout>
|
||||
</DropDownButton>
|
||||
<Grid
|
||||
Width="32"
|
||||
Height="32"
|
||||
@@ -562,10 +663,9 @@
|
||||
Command="{x:Bind GenerateCustomAICommand}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
IsEnabled="{x:Bind ViewModel.IsCustomAIAvailable, Mode=OneWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
TabIndex="1"
|
||||
TabIndex="2"
|
||||
Visibility="{x:Bind ViewModel.Query.Length, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="SendBtnToolTip" TextWrapping="WrapWholeWords" />
|
||||
|
||||
@@ -10,9 +10,11 @@ using AdvancedPaste.Models;
|
||||
using AdvancedPaste.ViewModels;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
|
||||
namespace AdvancedPaste.Controls
|
||||
{
|
||||
@@ -44,6 +46,18 @@ namespace AdvancedPaste.Controls
|
||||
set => SetValue(FooterProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ModelSelectorProperty = DependencyProperty.Register(
|
||||
nameof(ModelSelector),
|
||||
typeof(object),
|
||||
typeof(PromptBox),
|
||||
new PropertyMetadata(defaultValue: null));
|
||||
|
||||
public object ModelSelector
|
||||
{
|
||||
get => GetValue(ModelSelectorProperty);
|
||||
set => SetValue(ModelSelectorProperty, value);
|
||||
}
|
||||
|
||||
public PromptBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -111,5 +125,19 @@ namespace AdvancedPaste.Controls
|
||||
{
|
||||
Loader.IsLoading = loading;
|
||||
}
|
||||
|
||||
private async void AIProviderListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (AIProviderListView.SelectedItem is PasteAIProviderDefinition provider)
|
||||
{
|
||||
if (ViewModel.SetActiveProviderCommand.CanExecute(provider))
|
||||
{
|
||||
await ViewModel.SetActiveProviderCommand.ExecuteAsync(provider);
|
||||
}
|
||||
|
||||
var flyout = FlyoutBase.GetAttachedFlyout(AIProviderButton);
|
||||
flyout?.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace AdvancedPaste.Converters;
|
||||
|
||||
public sealed partial class CountToInvertedVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
bool hasItems = ((value is int intValue) && intValue > 0) || (value is IEnumerable enumerable && enumerable.GetEnumerator().MoveNext());
|
||||
return targetType == typeof(Visibility)
|
||||
? (hasItems ? Visibility.Collapsed : Visibility.Visible)
|
||||
: !hasItems;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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 System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace AdvancedPaste.Converters
|
||||
{
|
||||
public sealed partial class DateTimeToFriendlyStringConverter : IValueConverter
|
||||
{
|
||||
private static readonly ResourceLoader _resources = new ResourceLoader();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is not DateTimeOffset dto)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Use local times to calculate relative values and formatting
|
||||
var now = DateTimeOffset.Now;
|
||||
var localValue = dto.ToLocalTime();
|
||||
var culture = !string.IsNullOrEmpty(language)
|
||||
? new CultureInfo(language)
|
||||
: CultureInfo.CurrentCulture;
|
||||
|
||||
var delta = now - localValue;
|
||||
|
||||
// Future dates: fall back to date/time formatting
|
||||
if (delta < TimeSpan.Zero)
|
||||
{
|
||||
return FormatDateAndTime(localValue, culture);
|
||||
}
|
||||
|
||||
// < 1 minute
|
||||
if (delta.TotalSeconds < 60)
|
||||
{
|
||||
return _resources.GetString("Relative_JustNow"); // "Just now"
|
||||
}
|
||||
|
||||
// < 60 minutes
|
||||
if (delta.TotalMinutes < 60)
|
||||
{
|
||||
var mins = (int)Math.Round(delta.TotalMinutes);
|
||||
if (mins <= 1)
|
||||
{
|
||||
return _resources.GetString("Relative_MinuteAgo"); // "1 minute ago"
|
||||
}
|
||||
|
||||
var fmt = _resources.GetString("Relative_MinutesAgo_Format"); // "{0} minutes ago"
|
||||
return string.Format(culture, fmt, mins);
|
||||
}
|
||||
|
||||
// Same calendar day → "Today, {time}"
|
||||
var today = now.Date;
|
||||
if (localValue.Date == today)
|
||||
{
|
||||
var time = localValue.ToString("t", culture); // localized short time
|
||||
var fmt = _resources.GetString("Relative_Today_TimeFormat"); // "Today, {0}"
|
||||
return string.Format(culture, fmt, time);
|
||||
}
|
||||
|
||||
// Yesterday → "Yesterday, {time}"
|
||||
if (localValue.Date == today.AddDays(-1))
|
||||
{
|
||||
var time = localValue.ToString("t", culture);
|
||||
var fmt = _resources.GetString("Relative_Yesterday_TimeFormat"); // "Yesterday, {0}"
|
||||
return string.Format(culture, fmt, time);
|
||||
}
|
||||
|
||||
// Within last 7 days → "{Weekday}, {time}"
|
||||
if (delta.TotalDays < 7)
|
||||
{
|
||||
var weekday = localValue.ToString("dddd", culture); // localized weekday
|
||||
var time = localValue.ToString("t", culture);
|
||||
var fmt = _resources.GetString("Relative_Weekday_TimeFormat"); // "{0}, {1}"
|
||||
return string.Format(culture, fmt, weekday, time);
|
||||
}
|
||||
|
||||
// Older → localized date + time
|
||||
return FormatDateAndTime(localValue, culture);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
private static string FormatDateAndTime(DateTimeOffset localValue, CultureInfo culture)
|
||||
{
|
||||
// Use localized short date + short time
|
||||
var date = localValue.ToString("d", culture);
|
||||
var time = localValue.ToString("t", culture);
|
||||
var fmt = _resources.GetString("Relative_Date_TimeFormat"); // "{0}, {1}"
|
||||
return string.Format(culture, fmt, date, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
namespace AdvancedPaste.Converters;
|
||||
|
||||
public sealed partial class ServiceTypeToIconConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
string iconPath = value switch
|
||||
{
|
||||
string service when !string.IsNullOrWhiteSpace(service) => AIServiceTypeRegistry.GetIconPath(service),
|
||||
AIServiceType serviceType => AIServiceTypeRegistry.GetIconPath(serviceType),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(iconPath))
|
||||
{
|
||||
iconPath = AIServiceTypeRegistry.GetIconPath(AIServiceType.Unknown);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new SvgImageSource(new Uri(iconPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug("Failed to create SvgImageSource for AI service icon", ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:pages="using:AdvancedPaste.Pages"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Width="420"
|
||||
Width="486"
|
||||
Height="188"
|
||||
MinWidth="420"
|
||||
MinWidth="486"
|
||||
MinHeight="188"
|
||||
Closed="WindowEx_Closed"
|
||||
IsAlwaysOnTop="True"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
AutomationProperties.HelpText="{x:Bind Name, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind AccessibleName, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="26" />
|
||||
<ColumnDefinition Width="48" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
@@ -52,6 +52,7 @@
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
x:Phase="1"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
@@ -74,7 +75,7 @@
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Opacity="0.5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="26" />
|
||||
<ColumnDefinition Width="48" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
@@ -87,6 +88,7 @@
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
x:Phase="1"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
@@ -142,14 +144,107 @@
|
||||
</Page.KeyboardAccelerators>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid
|
||||
Margin="8,8,8,4"
|
||||
Padding="4"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Visibility="{x:Bind ViewModel.ClipboardHasData, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:ClipboardHistoryItemPreviewControl Height="64" ClipboardItem="{x:Bind ViewModel.CurrentClipboardItem, Mode=OneWay}" />
|
||||
<Button
|
||||
x:Uid="ClipboardHistoryButton"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="ClipboardHistoryButtonToolTip" />
|
||||
</ToolTipService.ToolTip>
|
||||
<FontIcon
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="16"
|
||||
Glyph="" />
|
||||
<Button.Flyout>
|
||||
<Flyout
|
||||
FlyoutPresenterStyle="{StaticResource PaddingLessFlyoutPresenterStyle}"
|
||||
Placement="Right"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
<ItemsView
|
||||
Width="320"
|
||||
Margin="8,8,8,0"
|
||||
IsItemInvokedEnabled="True"
|
||||
ItemInvoked="ClipboardHistory_ItemInvoked"
|
||||
ItemsSource="{x:Bind clipboardHistory, Mode=OneWay}"
|
||||
SelectionMode="None">
|
||||
<ItemsView.Layout>
|
||||
<StackLayout Orientation="Vertical" Spacing="8" />
|
||||
</ItemsView.Layout>
|
||||
<ItemsView.Transitions />
|
||||
<ItemsView.ItemTemplate>
|
||||
<DataTemplate x:DataType="local:ClipboardItem">
|
||||
<ItemContainer
|
||||
AutomationProperties.Name="{x:Bind Description, Mode=OneWay}"
|
||||
CornerRadius="16"
|
||||
ToolTipService.ToolTip="{x:Bind Content}">
|
||||
<Grid
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
ColumnSpacing="8"
|
||||
CornerRadius="16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MaxWidth="240" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:ClipboardHistoryItemPreviewControl x:Phase="0" ClipboardItem="{x:Bind}" />
|
||||
<Button
|
||||
x:Name="ClipboardHistoryItemMoreOptionsButton"
|
||||
x:Uid="ClipboardHistoryItemMoreOptionsButton"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Button.Content>
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="ClipboardHistoryItemDeleteButton"
|
||||
Click="ClipboardHistoryItemDeleteButton_Click"
|
||||
CommandParameter="{x:Bind (local:ClipboardItem)}"
|
||||
Icon="Delete" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Grid>
|
||||
</ItemContainer>
|
||||
</DataTemplate>
|
||||
</ItemsView.ItemTemplate>
|
||||
</ItemsView>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Grid>
|
||||
<controls:PromptBox
|
||||
x:Name="CustomFormatTextBox"
|
||||
x:Uid="CustomFormatTextBox"
|
||||
Margin="8,4,8,0"
|
||||
Grid.Row="1"
|
||||
Margin="20,4,20,0"
|
||||
x:FieldModifier="public"
|
||||
TabIndex="0">
|
||||
<controls:PromptBox.Footer>
|
||||
@@ -246,117 +341,6 @@
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
|
||||
<Rectangle
|
||||
Grid.Row="3"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
<Button
|
||||
Grid.Row="4"
|
||||
Height="32"
|
||||
Margin="4,0,4,4"
|
||||
Padding="{StaticResource ButtonPadding}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AutomationProperties.LabeledBy="{x:Bind ClipboardHistoryButton}"
|
||||
IsEnabled="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="16"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
x:Name="ClipboardHistoryButton"
|
||||
x:Uid="ClipboardHistoryButton"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center" />
|
||||
<FontIcon
|
||||
Grid.Column="2"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
</Grid>
|
||||
<Button.Flyout>
|
||||
<Flyout
|
||||
FlyoutPresenterStyle="{StaticResource PaddingLessFlyoutPresenterStyle}"
|
||||
Placement="Right"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
|
||||
<ListView
|
||||
Width="320"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ClipboardHistory_ItemClick"
|
||||
ItemsSource="{x:Bind clipboardHistory, Mode=OneWay}"
|
||||
SelectionMode="None">
|
||||
<ListView.Transitions />
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="local:ClipboardItem">
|
||||
<Grid
|
||||
Height="40"
|
||||
HorizontalAlignment="Stretch"
|
||||
AutomationProperties.Name="{x:Bind Description, Mode=OneWay}"
|
||||
ColumnSpacing="8"
|
||||
ToolTipService.ToolTip="{x:Bind Content}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MaxWidth="240" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
x:Phase="2"
|
||||
Source="{x:Bind Image}"
|
||||
Visibility="Visible" />
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
x:Phase="1"
|
||||
Text="{x:Bind Content}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Visibility="Visible" />
|
||||
<Button
|
||||
x:Name="ClipboardHistoryItemMoreOptionsButton"
|
||||
x:Uid="ClipboardHistoryItemMoreOptionsButton"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<Button.Content>
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem
|
||||
x:Uid="ClipboardHistoryItemDeleteButton"
|
||||
Click="ClipboardHistoryItemDeleteButton_Click"
|
||||
CommandParameter="{x:Bind (local:ClipboardItem)}"
|
||||
Icon="Delete" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -68,11 +68,22 @@ namespace AdvancedPaste.Pages
|
||||
if (item.Content.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
string text = await item.Content.GetTextAsync();
|
||||
items.Add(new ClipboardItem { Content = text, Item = item });
|
||||
items.Add(new ClipboardItem
|
||||
{
|
||||
Content = text,
|
||||
Format = ClipboardFormat.Text,
|
||||
Timestamp = item.Timestamp,
|
||||
Item = item,
|
||||
});
|
||||
}
|
||||
else if (item.Content.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
items.Add(new ClipboardItem { Item = item });
|
||||
items.Add(new ClipboardItem
|
||||
{
|
||||
Format = ClipboardFormat.Image,
|
||||
Timestamp = item.Timestamp,
|
||||
Item = item,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,10 +198,9 @@ namespace AdvancedPaste.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void ClipboardHistory_ItemClick(object sender, ItemClickEventArgs e)
|
||||
private async void ClipboardHistory_ItemInvoked(ItemsView sender, ItemsViewItemInvokedEventArgs args)
|
||||
{
|
||||
var item = e.ClickedItem as ClipboardItem;
|
||||
if (item is not null)
|
||||
if (args.InvokedItem is ClipboardItem item)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
|
||||
if (!string.IsNullOrEmpty(item.Content))
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals">
|
||||
|
||||
<Style x:Key="SubtleDropDownButtonStyle" TargetType="DropDownButton">
|
||||
<Setter Property="Background" Value="{ThemeResource SubtleButtonBackground}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource SubtleButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource SubtleButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
|
||||
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Grid.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</Grid.BackgroundTransition>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}" />
|
||||
<AnimatedIcon
|
||||
xmlns:local="using:Microsoft.UI.Xaml.Controls"
|
||||
x:Name="ChevronIcon"
|
||||
Grid.Column="1"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Margin="-4,0,0,0"
|
||||
local:AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource DropDownButtonForegroundSecondary}">
|
||||
<animatedvisuals:AnimatedChevronDownSmallVisualSource />
|
||||
<AnimatedIcon.FallbackIconSource>
|
||||
<FontIconSource
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
FontSize="8"
|
||||
Glyph=""
|
||||
IsTextScaleFactorEnabled="False" />
|
||||
</AnimatedIcon.FallbackIconSource>
|
||||
</AnimatedIcon>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ChevronIcon" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource DropDownButtonForegroundSecondaryPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ChevronIcon.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ChevronIcon" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource DropDownButtonForegroundSecondaryPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ChevronIcon.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBackgroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonBorderBrushDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ChevronIcon" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ChevronIcon.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1822)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.718 2.34668H12.12L16.5 13.3333H14.098L9.718 2.34668ZM4.87933 2.34668H7.39067L11.7707 13.3333H9.32133L8.426 11.026H3.84467L2.94867 13.3327H0.5L4.88 2.34801L4.87933 2.34668ZM7.634 8.98601L6.13533 5.12468L4.63667 8.98668H7.63333L7.634 8.98601Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1822">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
@@ -0,0 +1,23 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.05607 1.09062H10.3957L5.89074 14.4385C5.84444 14.5756 5.75629 14.6948 5.6387 14.7792C5.52111 14.8637 5.38 14.9091 5.23524 14.9091H1.85791C1.74822 14.9091 1.64011 14.883 1.54252 14.833C1.44493 14.7829 1.36066 14.7103 1.29669 14.6212C1.23271 14.5322 1.19087 14.4291 1.17462 14.3206C1.15837 14.2122 1.16818 14.1014 1.20324 13.9975L5.40041 1.56129C5.44669 1.42407 5.53485 1.30483 5.65248 1.22037C5.7701 1.1359 5.91126 1.09063 6.05607 1.09062Z" fill="url(#paint0_linear_2092_1811)"/>
|
||||
<path d="M12.3626 10.0435H5.48096C5.41698 10.0434 5.35447 10.0626 5.30156 10.0986C5.24864 10.1345 5.20779 10.1856 5.18432 10.2451C5.16085 10.3046 5.15584 10.3698 5.16996 10.4322C5.18408 10.4946 5.21666 10.5513 5.26346 10.595L9.68546 14.7223C9.81421 14.8424 9.98373 14.9092 10.1598 14.9091H14.0565L12.3626 10.0435Z" fill="#0078D4"/>
|
||||
<path d="M6.05617 1.0907C5.90978 1.09014 5.76704 1.1364 5.64881 1.22273C5.53058 1.30906 5.44305 1.43093 5.399 1.57054L1.2085 13.9862C1.17108 14.0905 1.15933 14.2023 1.17425 14.3121C1.18917 14.4219 1.23031 14.5265 1.2942 14.617C1.3581 14.7076 1.44285 14.7814 1.54131 14.8323C1.63976 14.8831 1.74902 14.9095 1.85983 14.9092H5.32433C5.45337 14.8861 5.57397 14.8293 5.67382 14.7443C5.77367 14.6594 5.84919 14.5495 5.89267 14.4259L6.72833 11.963L9.71333 14.7472C9.83842 14.8507 9.99534 14.9079 10.1577 14.9092H14.0398L12.3372 10.0435L7.37367 10.0447L10.4115 1.0907H6.05617Z" fill="url(#paint1_linear_2092_1811)"/>
|
||||
<path d="M11.5996 1.5607C11.5533 1.4237 11.4653 1.30466 11.3479 1.22034C11.2304 1.13603 11.0895 1.09068 10.9449 1.0907H6.1084C6.25297 1.09071 6.3939 1.13606 6.51135 1.22038C6.62879 1.30469 6.71683 1.42372 6.76307 1.5607L10.9604 13.9974C10.9955 14.1013 11.0053 14.2121 10.9891 14.3206C10.9729 14.4291 10.931 14.5322 10.867 14.6213C10.8031 14.7104 10.7188 14.7831 10.6212 14.8331C10.5236 14.8832 10.4154 14.9094 10.3057 14.9094H15.1424C15.2521 14.9093 15.3602 14.8832 15.4578 14.8331C15.5554 14.783 15.6396 14.7104 15.7036 14.6213C15.7675 14.5321 15.8094 14.4291 15.8256 14.3206C15.8418 14.2121 15.832 14.1013 15.7969 13.9974L11.5996 1.5607Z" fill="url(#paint2_linear_2092_1811)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1811" x1="7.63774" y1="2.11462" x2="3.1309" y2="15.429" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#114A8B"/>
|
||||
<stop offset="1" stop-color="#0669BC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1811" x1="9.04567" y1="8.31954" x2="8.00317" y2="8.67204" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-opacity="0.3"/>
|
||||
<stop offset="0.071" stop-opacity="0.2"/>
|
||||
<stop offset="0.321" stop-opacity="0.1"/>
|
||||
<stop offset="0.623" stop-opacity="0.05"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1811" x1="8.4729" y1="1.72636" x2="13.4201" y2="14.9065" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3CCBF4"/>
|
||||
<stop offset="1" stop-color="#2892DF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,49 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1818)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3222 0C11.7976 0 12.2189 0.367333 12.3702 0.886C12.5216 1.40467 13.4069 4.61267 13.4069 4.61267V10.9873H10.1982L10.2636 0H11.3222Z" fill="url(#paint0_linear_2092_1818)"/>
|
||||
<path d="M16.0323 4.97996C16.0323 4.75329 15.849 4.57996 15.6323 4.57996H13.7423C13.1034 4.58049 12.4908 4.83459 12.039 5.28645C11.5873 5.73832 11.3334 6.35101 11.333 6.98996V10.9873H13.6237C14.2624 10.9866 14.8747 10.7325 15.3263 10.2808C15.7779 9.82909 16.0318 9.21667 16.0323 8.57796V4.97996Z" fill="url(#paint1_linear_2092_1818)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3216 1.6754e-05C11.2349 -0.00060392 11.1489 0.0160265 11.0686 0.0489413C10.9883 0.0818561 10.9154 0.130399 10.854 0.191747C10.7927 0.253096 10.7442 0.326027 10.7112 0.406302C10.6783 0.486576 10.6617 0.572592 10.6623 0.65935L10.5976 12.7914C10.5975 13.6423 10.2594 14.4583 9.65765 15.06C9.05595 15.6617 8.23992 15.9998 7.38898 16H1.56631C1.50256 16.0004 1.43966 15.9854 1.3829 15.9564C1.32613 15.9274 1.27717 15.8852 1.24012 15.8333C1.20308 15.7814 1.17903 15.7214 1.17002 15.6583C1.161 15.5952 1.16727 15.5309 1.18831 15.4707L5.85498 2.15002C6.07482 1.5228 6.48378 0.979202 7.02549 0.594138C7.56721 0.209074 8.21502 0.00149836 8.87964 1.6754e-05H11.3323H11.3216Z" fill="url(#paint2_linear_2092_1818)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1818" x1="12.6616" y1="11.2247" x2="9.96091" y2="0.410667" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#712575"/>
|
||||
<stop offset="0.09" stop-color="#9A2884"/>
|
||||
<stop offset="0.18" stop-color="#BF2C92"/>
|
||||
<stop offset="0.27" stop-color="#DA2E9C"/>
|
||||
<stop offset="0.34" stop-color="#EB30A2"/>
|
||||
<stop offset="0.4" stop-color="#F131A5"/>
|
||||
<stop offset="0.5" stop-color="#EC30A3"/>
|
||||
<stop offset="0.61" stop-color="#DF2F9E"/>
|
||||
<stop offset="0.72" stop-color="#C92D96"/>
|
||||
<stop offset="0.83" stop-color="#AA2A8A"/>
|
||||
<stop offset="0.95" stop-color="#83267C"/>
|
||||
<stop offset="1" stop-color="#712575"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1818" x1="13.6883" y1="0.226623" x2="13.6883" y2="15.4813" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.08" stop-color="#B17BD5"/>
|
||||
<stop offset="0.19" stop-color="#8778DB"/>
|
||||
<stop offset="0.3" stop-color="#6276E1"/>
|
||||
<stop offset="0.41" stop-color="#4574E5"/>
|
||||
<stop offset="0.54" stop-color="#2E72E8"/>
|
||||
<stop offset="0.67" stop-color="#1D71EB"/>
|
||||
<stop offset="0.81" stop-color="#1471EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1818" x1="12.769" y1="0.572683" x2="2.65698" y2="16.7887" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.05" stop-color="#B77BD4"/>
|
||||
<stop offset="0.11" stop-color="#9079DA"/>
|
||||
<stop offset="0.18" stop-color="#6E77DF"/>
|
||||
<stop offset="0.25" stop-color="#5175E3"/>
|
||||
<stop offset="0.33" stop-color="#3973E7"/>
|
||||
<stop offset="0.42" stop-color="#2772E9"/>
|
||||
<stop offset="0.54" stop-color="#1A71EB"/>
|
||||
<stop offset="0.68" stop-color="#1371EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2092_1818">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,59 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1741)">
|
||||
<mask id="mask0_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1741)">
|
||||
<mask id="mask1_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-1" y="-2" width="19" height="20">
|
||||
<path d="M17.8337 -1.33337H-0.833008V17.3333H17.8337V-1.33337Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2092_1741)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1137 0.315668C11.57 0.315668 11.9744 0.657891 12.1196 1.15567C12.2648 1.65345 13.1152 4.73345 13.1152 4.73345V10.852H10.0352L10.0974 0.305298H11.1137V0.315668Z" fill="url(#paint0_linear_2092_1741)"/>
|
||||
<path d="M15.6352 5.09586C15.6352 4.87808 15.4589 4.71216 15.2515 4.71216H13.4366C12.1611 4.71216 11.124 5.7492 11.124 7.02472V10.8618H13.3226C14.5982 10.8618 15.6352 9.82472 15.6352 8.54919V5.09586Z" fill="url(#paint1_linear_2092_1741)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1133 0.315674C10.7607 0.315674 10.4807 0.595674 10.4807 0.948265L10.4185 12.5942C10.4185 14.2949 9.0392 15.6742 7.33847 15.6742H1.74885C1.47921 15.6742 1.30292 15.4149 1.38589 15.1661L5.86589 2.37938C6.30144 1.14531 7.46293 0.315674 8.7696 0.315674H11.1237H11.1133Z" fill="url(#paint2_linear_2092_1741)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1741" x1="12.3996" y1="11.0801" x2="9.80702" y2="0.699373" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#712575"/>
|
||||
<stop offset="0.09" stop-color="#9A2884"/>
|
||||
<stop offset="0.18" stop-color="#BF2C92"/>
|
||||
<stop offset="0.27" stop-color="#DA2E9C"/>
|
||||
<stop offset="0.34" stop-color="#EB30A2"/>
|
||||
<stop offset="0.4" stop-color="#F131A5"/>
|
||||
<stop offset="0.5" stop-color="#EC30A3"/>
|
||||
<stop offset="0.61" stop-color="#DF2F9E"/>
|
||||
<stop offset="0.72" stop-color="#C92D96"/>
|
||||
<stop offset="0.83" stop-color="#AA2A8A"/>
|
||||
<stop offset="0.95" stop-color="#83267C"/>
|
||||
<stop offset="1" stop-color="#712575"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1741" x1="13.3848" y1="0.532897" x2="13.3848" y2="15.1759" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.08" stop-color="#B17BD5"/>
|
||||
<stop offset="0.19" stop-color="#8778DB"/>
|
||||
<stop offset="0.3" stop-color="#6276E1"/>
|
||||
<stop offset="0.41" stop-color="#4574E5"/>
|
||||
<stop offset="0.54" stop-color="#2E72E8"/>
|
||||
<stop offset="0.67" stop-color="#1D71EB"/>
|
||||
<stop offset="0.81" stop-color="#1471EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1741" x1="12.5029" y1="0.865306" x2="2.79625" y2="16.4313" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.05" stop-color="#B77BD4"/>
|
||||
<stop offset="0.11" stop-color="#9079DA"/>
|
||||
<stop offset="0.18" stop-color="#6E77DF"/>
|
||||
<stop offset="0.25" stop-color="#5175E3"/>
|
||||
<stop offset="0.33" stop-color="#3973E7"/>
|
||||
<stop offset="0.42" stop-color="#2772E9"/>
|
||||
<stop offset="0.54" stop-color="#1A71EB"/>
|
||||
<stop offset="0.68" stop-color="#1371EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2092_1741">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.2445 7.22331C13.137 6.75184 12.13 6.07273 11.2778 5.22264C10.0911 4.03353 9.2444 2.54831 8.8258 0.921308C8.80744 0.849046 8.76551 0.784967 8.70665 0.739197C8.6478 0.693428 8.57536 0.668579 8.5008 0.668579C8.42624 0.668579 8.35381 0.693428 8.29495 0.739197C8.2361 0.784967 8.19417 0.849046 8.1758 0.921308C7.75632 2.5481 6.90952 4.03315 5.72314 5.22264C4.87089 6.07263 3.8639 6.75172 2.75647 7.22331C2.32314 7.40998 1.8778 7.55998 1.4218 7.67531C1.3491 7.69317 1.28448 7.7349 1.23829 7.79382C1.1921 7.85274 1.16699 7.92544 1.16699 8.00031C1.16699 8.07518 1.1921 8.14788 1.23829 8.2068C1.28448 8.26572 1.3491 8.30744 1.4218 8.32531C1.8778 8.43998 2.3218 8.58998 2.75647 8.77664C3.86397 9.24811 4.87098 9.92722 5.72314 10.7773C6.9102 11.9666 7.75709 13.452 8.1758 15.0793C8.19367 15.152 8.2354 15.2166 8.29431 15.2628C8.35323 15.309 8.42594 15.3341 8.5008 15.3341C8.57567 15.3341 8.64838 15.309 8.7073 15.2628C8.76621 15.2166 8.80794 15.152 8.8258 15.0793C8.94047 14.6226 9.09047 14.1786 9.27714 13.744C9.74858 12.6365 10.4277 11.6294 11.2778 10.7773C12.4671 9.59052 13.9526 8.74386 15.5798 8.32531C15.6521 8.30694 15.7161 8.26502 15.7619 8.20616C15.8077 8.1473 15.8325 8.07487 15.8325 8.00031C15.8325 7.92575 15.8077 7.85332 15.7619 7.79446C15.7161 7.7356 15.6521 7.69367 15.5798 7.67531C15.1234 7.56047 14.6768 7.40932 14.2445 7.22331Z" fill="#3186FF"/>
|
||||
<path d="M14.2445 7.22331C13.137 6.75184 12.13 6.07273 11.2778 5.22264C10.0911 4.03353 9.2444 2.54831 8.8258 0.921308C8.80744 0.849046 8.76551 0.784967 8.70665 0.739197C8.6478 0.693428 8.57536 0.668579 8.5008 0.668579C8.42624 0.668579 8.35381 0.693428 8.29495 0.739197C8.2361 0.784967 8.19417 0.849046 8.1758 0.921308C7.75632 2.5481 6.90952 4.03315 5.72314 5.22264C4.87089 6.07263 3.8639 6.75172 2.75647 7.22331C2.32314 7.40998 1.8778 7.55998 1.4218 7.67531C1.3491 7.69317 1.28448 7.7349 1.23829 7.79382C1.1921 7.85274 1.16699 7.92544 1.16699 8.00031C1.16699 8.07518 1.1921 8.14788 1.23829 8.2068C1.28448 8.26572 1.3491 8.30744 1.4218 8.32531C1.8778 8.43998 2.3218 8.58998 2.75647 8.77664C3.86397 9.24811 4.87098 9.92722 5.72314 10.7773C6.9102 11.9666 7.75709 13.452 8.1758 15.0793C8.19367 15.152 8.2354 15.2166 8.29431 15.2628C8.35323 15.309 8.42594 15.3341 8.5008 15.3341C8.57567 15.3341 8.64838 15.309 8.7073 15.2628C8.76621 15.2166 8.80794 15.152 8.8258 15.0793C8.94047 14.6226 9.09047 14.1786 9.27714 13.744C9.74858 12.6365 10.4277 11.6294 11.2778 10.7773C12.4671 9.59052 13.9526 8.74386 15.5798 8.32531C15.6521 8.30694 15.7161 8.26502 15.7619 8.20616C15.8077 8.1473 15.8325 8.07487 15.8325 8.00031C15.8325 7.92575 15.8077 7.85332 15.7619 7.79446C15.7161 7.7356 15.6521 7.69367 15.5798 7.67531C15.1234 7.56047 14.6768 7.40932 14.2445 7.22331Z" fill="url(#paint0_linear_2092_1806)"/>
|
||||
<path d="M14.2445 7.22331C13.137 6.75184 12.13 6.07273 11.2778 5.22264C10.0911 4.03353 9.2444 2.54831 8.8258 0.921308C8.80744 0.849046 8.76551 0.784967 8.70665 0.739197C8.6478 0.693428 8.57536 0.668579 8.5008 0.668579C8.42624 0.668579 8.35381 0.693428 8.29495 0.739197C8.2361 0.784967 8.19417 0.849046 8.1758 0.921308C7.75632 2.5481 6.90952 4.03315 5.72314 5.22264C4.87089 6.07263 3.8639 6.75172 2.75647 7.22331C2.32314 7.40998 1.8778 7.55998 1.4218 7.67531C1.3491 7.69317 1.28448 7.7349 1.23829 7.79382C1.1921 7.85274 1.16699 7.92544 1.16699 8.00031C1.16699 8.07518 1.1921 8.14788 1.23829 8.2068C1.28448 8.26572 1.3491 8.30744 1.4218 8.32531C1.8778 8.43998 2.3218 8.58998 2.75647 8.77664C3.86397 9.24811 4.87098 9.92722 5.72314 10.7773C6.9102 11.9666 7.75709 13.452 8.1758 15.0793C8.19367 15.152 8.2354 15.2166 8.29431 15.2628C8.35323 15.309 8.42594 15.3341 8.5008 15.3341C8.57567 15.3341 8.64838 15.309 8.7073 15.2628C8.76621 15.2166 8.80794 15.152 8.8258 15.0793C8.94047 14.6226 9.09047 14.1786 9.27714 13.744C9.74858 12.6365 10.4277 11.6294 11.2778 10.7773C12.4671 9.59052 13.9526 8.74386 15.5798 8.32531C15.6521 8.30694 15.7161 8.26502 15.7619 8.20616C15.8077 8.1473 15.8325 8.07487 15.8325 8.00031C15.8325 7.92575 15.8077 7.85332 15.7619 7.79446C15.7161 7.7356 15.6521 7.69367 15.5798 7.67531C15.1234 7.56047 14.6768 7.40932 14.2445 7.22331Z" fill="url(#paint1_linear_2092_1806)"/>
|
||||
<path d="M14.2445 7.22331C13.137 6.75184 12.13 6.07273 11.2778 5.22264C10.0911 4.03353 9.2444 2.54831 8.8258 0.921308C8.80744 0.849046 8.76551 0.784967 8.70665 0.739197C8.6478 0.693428 8.57536 0.668579 8.5008 0.668579C8.42624 0.668579 8.35381 0.693428 8.29495 0.739197C8.2361 0.784967 8.19417 0.849046 8.1758 0.921308C7.75632 2.5481 6.90952 4.03315 5.72314 5.22264C4.87089 6.07263 3.8639 6.75172 2.75647 7.22331C2.32314 7.40998 1.8778 7.55998 1.4218 7.67531C1.3491 7.69317 1.28448 7.7349 1.23829 7.79382C1.1921 7.85274 1.16699 7.92544 1.16699 8.00031C1.16699 8.07518 1.1921 8.14788 1.23829 8.2068C1.28448 8.26572 1.3491 8.30744 1.4218 8.32531C1.8778 8.43998 2.3218 8.58998 2.75647 8.77664C3.86397 9.24811 4.87098 9.92722 5.72314 10.7773C6.9102 11.9666 7.75709 13.452 8.1758 15.0793C8.19367 15.152 8.2354 15.2166 8.29431 15.2628C8.35323 15.309 8.42594 15.3341 8.5008 15.3341C8.57567 15.3341 8.64838 15.309 8.7073 15.2628C8.76621 15.2166 8.80794 15.152 8.8258 15.0793C8.94047 14.6226 9.09047 14.1786 9.27714 13.744C9.74858 12.6365 10.4277 11.6294 11.2778 10.7773C12.4671 9.59052 13.9526 8.74386 15.5798 8.32531C15.6521 8.30694 15.7161 8.26502 15.7619 8.20616C15.8077 8.1473 15.8325 8.07487 15.8325 8.00031C15.8325 7.92575 15.8077 7.85332 15.7619 7.79446C15.7161 7.7356 15.6521 7.69367 15.5798 7.67531C15.1234 7.56047 14.6768 7.40932 14.2445 7.22331Z" fill="url(#paint2_linear_2092_1806)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1806" x1="5.16714" y1="10.3333" x2="7.8338" y2="7.99997" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#08B962"/>
|
||||
<stop offset="1" stop-color="#08B962" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1806" x1="5.8338" y1="3.66664" x2="8.16714" y2="7.33331" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F94543"/>
|
||||
<stop offset="1" stop-color="#F94543" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1806" x1="2.8338" y1="8.99998" x2="12.1671" y2="7.99998" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FABC12"/>
|
||||
<stop offset="0.46" stop-color="#FABC12" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1755)">
|
||||
<mask id="mask0_2092_1755" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.428 0H0.5V16H16.428V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1755)">
|
||||
<path d="M5.05035 0H2.77441V3.21057H5.05035V0Z" fill="#FFD800"/>
|
||||
<path d="M14.1529 0H11.877V3.21057H14.1529V0Z" fill="#FFD800"/>
|
||||
<path d="M7.32555 3.21082H2.77441V6.42139H7.32555V3.21082Z" fill="#FFAF00"/>
|
||||
<path d="M14.1537 3.21082H9.60254V6.42139H14.1537V3.21082Z" fill="#FFAF00"/>
|
||||
<path d="M14.1519 6.41992H2.77441V9.63049H14.1519V6.41992Z" fill="#FF8205"/>
|
||||
<path d="M5.05035 9.63074H2.77441V12.8414H5.05035V9.63074Z" fill="#FA500F"/>
|
||||
<path d="M9.60213 9.63074H7.32617V12.8414H9.60213V9.63074Z" fill="#FA500F"/>
|
||||
<path d="M14.1529 9.63074H11.877V12.8414H14.1529V9.63074Z" fill="#FA500F"/>
|
||||
<path d="M7.32633 12.8402H0.5V16.0509H7.32633V12.8402Z" fill="#E10500"/>
|
||||
<path d="M16.4296 12.8402H9.60254V16.0509H16.4296V12.8402Z" fill="#E10500"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1755">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1790)">
|
||||
<path d="M15.85 7.50005H15.75L13.08 2.54505C13.1287 2.45432 13.1544 2.35303 13.155 2.25005C13.155 2.16427 13.138 2.07933 13.105 2.00014C13.072 1.92095 13.0237 1.84907 12.9628 1.78865C12.9019 1.72823 12.8297 1.68045 12.7502 1.64808C12.6708 1.61571 12.5857 1.59939 12.5 1.60005C12.4129 1.59972 12.3267 1.6173 12.2467 1.65171C12.1667 1.68612 12.0946 1.73661 12.035 1.80005L6.71496 0.740047C6.70184 0.64745 6.66874 0.558814 6.61795 0.480283C6.56716 0.401752 6.4999 0.335204 6.42084 0.285253C6.34177 0.235302 6.25279 0.203143 6.16006 0.191003C6.06733 0.178864 5.97307 0.187036 5.88381 0.214952C5.79455 0.242869 5.71243 0.289862 5.64314 0.352674C5.57385 0.415486 5.51905 0.492615 5.48253 0.578715C5.44602 0.664814 5.42867 0.757825 5.43167 0.851299C5.43468 0.944773 5.45798 1.03647 5.49996 1.12005L1.34996 7.06505C1.29477 7.04867 1.23753 7.04025 1.17996 7.04005C1.01805 7.05429 0.867358 7.12867 0.757581 7.24853C0.647805 7.36838 0.586914 7.52502 0.586914 7.68755C0.586914 7.85008 0.647805 8.00671 0.757581 8.12657C0.867358 8.24643 1.01805 8.32081 1.17996 8.33505L3.42996 13.87C3.39194 13.9551 3.37153 14.0469 3.36996 14.14C3.37128 14.3116 3.44034 14.4756 3.5621 14.5964C3.68386 14.7173 3.84843 14.7851 4.01996 14.785C4.10788 14.7861 4.19505 14.7688 4.27596 14.7344C4.35686 14.7 4.42973 14.6491 4.48996 14.585L11.21 15.24C11.2312 15.4111 11.3195 15.5667 11.4554 15.6727C11.5914 15.7787 11.7639 15.8263 11.935 15.805C12.106 15.7838 12.2617 15.6955 12.3676 15.5596C12.4736 15.4236 12.5212 15.2511 12.5 15.08C12.498 14.9264 12.4433 14.7781 12.345 14.66L15.73 8.76005H15.84C15.9253 8.76137 16.0101 8.74587 16.0895 8.71442C16.1688 8.68297 16.2412 8.63619 16.3025 8.57676C16.3638 8.51733 16.4128 8.44641 16.4467 8.36804C16.4805 8.28968 16.4987 8.20541 16.5 8.12005C16.4947 7.95205 16.4236 7.79286 16.302 7.67686C16.1804 7.56085 16.018 7.49734 15.85 7.50005ZM12 2.64505C12.0769 2.74772 12.1833 2.82448 12.305 2.86505L11.305 10.55C11.2418 10.5642 11.1811 10.5878 11.125 10.62L5.49996 5.76005C5.51878 5.70543 5.52726 5.64778 5.52496 5.59005C5.52496 5.55005 5.52496 5.50505 5.52496 5.46505L12 2.64505ZM15.235 8.30505L11.79 10.66C11.763 10.6412 11.7345 10.6245 11.705 10.61L12.725 2.84505L15.355 7.69505C15.2499 7.81343 15.1928 7.96678 15.195 8.12505L15.235 8.30505ZM4.78496 4.95005C4.63217 4.97297 4.49283 5.0504 4.39266 5.16803C4.29249 5.28566 4.23825 5.43555 4.23996 5.59005V5.63505L1.92996 7.00005L5.49996 1.87005L4.78496 4.95005ZM4.99996 6.22505C5.08363 6.20348 5.16307 6.16798 5.23496 6.12005L10.79 10.955C10.7638 11.0306 10.7502 11.11 10.75 11.19V11.225L4.54996 13.775C4.46194 13.6393 4.32644 13.5412 4.16996 13.5L4.99996 6.22505ZM10.935 11.62C11.0198 11.7179 11.1337 11.7862 11.26 11.815L11.565 14.5C11.4362 14.5671 11.3327 14.6741 11.27 14.805L4.76996 14.165L10.935 11.62ZM11.7 11.765C11.8068 11.7099 11.8967 11.6269 11.9601 11.5248C12.0235 11.4226 12.058 11.3052 12.06 11.185C12.0622 11.1306 12.0537 11.0762 12.035 11.025L15.185 8.86005L12 14.4L11.7 11.765ZM11.86 2.22505L5.28996 5.08505L5.21496 5.03505L6.04996 1.45505H6.07496C6.18268 1.45615 6.28889 1.42961 6.38342 1.37796C6.47796 1.32631 6.55768 1.25129 6.61496 1.16005L11.86 2.20505V2.22505ZM1.82996 7.69005C1.82996 7.64505 1.82996 7.60505 1.82996 7.57005L4.42496 6.04005C4.47735 6.09439 4.53812 6.13997 4.60496 6.17505L3.74996 13.43L1.60996 8.17005C1.67867 8.11029 1.73383 8.03657 1.77177 7.95379C1.80971 7.87102 1.82955 7.7811 1.82996 7.69005Z" fill="#333333"/>
|
||||
<path d="M12.7446 2.84497L15.3696 7.69497C15.2665 7.81456 15.2098 7.96712 15.2096 8.12497C15.2023 8.18475 15.2023 8.24519 15.2096 8.30497L11.7696 10.66L11.6846 10.61L12.6846 2.84497H12.7446Z" fill="#DEDEDD"/>
|
||||
<path d="M11.7002 11.765C11.807 11.7099 11.8969 11.6268 11.9603 11.5247C12.0237 11.4226 12.0582 11.3052 12.0602 11.185C12.0625 11.1305 12.054 11.0762 12.0352 11.025L15.1852 8.85999L12.0002 14.4L11.7002 11.765Z" fill="#B2B2B2"/>
|
||||
<path d="M10.9345 11.62C11.0194 11.7179 11.1332 11.7862 11.2595 11.815L11.5645 14.5C11.4358 14.567 11.3322 14.6741 11.2695 14.805L4.76953 14.165L10.9345 11.62Z" fill="#D1D1D1"/>
|
||||
<path d="M4.99992 6.225C5.08359 6.20343 5.16303 6.16793 5.23492 6.12L10.7899 10.955C10.7637 11.0306 10.7502 11.11 10.7499 11.19V11.225L4.54992 13.775C4.4619 13.6392 4.3264 13.5412 4.16992 13.5L4.99992 6.225Z" fill="#F2F2F2"/>
|
||||
<path d="M1.83035 7.69004C1.83035 7.64504 1.83035 7.60504 1.83035 7.57004L4.42535 6.04004C4.47774 6.09439 4.53851 6.13997 4.60535 6.17504L3.75035 13.43L1.61035 8.17004C1.67906 8.11029 1.73422 8.03656 1.77216 7.95378C1.8101 7.87101 1.82994 7.78109 1.83035 7.69004Z" fill="#D8D8D7"/>
|
||||
<path d="M4.78469 4.95C4.6319 4.97292 4.49255 5.05034 4.39238 5.16797C4.29221 5.28561 4.23798 5.4355 4.23969 5.59V5.635L1.92969 7L5.49969 1.87L4.78469 4.95Z" fill="#B2B2B2"/>
|
||||
<path d="M11.8598 2.22503L5.28984 5.08503L5.21484 5.03503L6.04984 1.45503H6.07484C6.18256 1.45613 6.28877 1.42959 6.38331 1.37795C6.47785 1.3263 6.55757 1.25127 6.61484 1.16003L11.8598 2.20503V2.22503Z" fill="#D1D1D1"/>
|
||||
<path d="M12 2.64502C12.0769 2.74769 12.1833 2.82445 12.305 2.86502L11.305 10.55C11.2418 10.5642 11.1811 10.5878 11.125 10.62L5.5 5.76002C5.51882 5.7054 5.5273 5.64775 5.525 5.59002C5.525 5.55002 5.525 5.50502 5.525 5.46502L12 2.64502Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1790">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1800)">
|
||||
<mask id="mask0_2092_1800" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1800)">
|
||||
<path d="M6.63673 5.772V4.26557C6.63673 4.13867 6.68433 4.0435 6.79527 3.98013L9.8241 2.23585C10.2364 1.998 10.728 1.88706 11.2353 1.88706C13.1382 1.88706 14.3434 3.3618 14.3434 4.9316C14.3434 5.0426 14.3434 5.16947 14.3275 5.29633L11.1877 3.45687C10.9975 3.3459 10.8071 3.3459 10.6169 3.45687L6.63673 5.772ZM13.709 11.6392V8.03953C13.709 7.8175 13.6138 7.65893 13.4236 7.54793L9.44343 5.2328L10.7437 4.48747C10.8547 4.42413 10.9499 4.42413 11.0609 4.48747L14.0897 6.23177C14.9619 6.73927 15.5485 7.8175 15.5485 8.864C15.5485 10.0691 14.835 11.1793 13.709 11.6392ZM5.70117 8.46777L4.40087 7.70667C4.28993 7.64333 4.2423 7.5481 4.2423 7.42123V3.9327C4.2423 2.23602 5.5426 0.951497 7.30277 0.951497C7.96887 0.951497 8.58717 1.17355 9.1106 1.56996L5.98673 3.37773C5.7965 3.4887 5.7013 3.64723 5.7013 3.86933L5.70117 8.46777ZM8.5 10.0852L6.63673 9.03863V6.8187L8.5 5.77217L10.3631 6.8187V9.03863L8.5 10.0852ZM9.6972 14.9058C9.03113 14.9058 8.41283 14.6838 7.8894 14.2874L11.0132 12.4796C11.2035 12.3686 11.2987 12.2101 11.2987 11.988V7.3894L12.6149 8.15053C12.7258 8.21387 12.7735 8.30907 12.7735 8.43597V11.9245C12.7735 13.6212 11.4572 14.9058 9.6972 14.9058ZM5.939 11.3697L2.91018 9.62543C2.03797 9.1179 1.45133 8.0397 1.45133 6.99317C1.45133 5.77217 2.18077 4.67803 3.30657 4.21813V7.83357C3.30657 8.0556 3.40178 8.21417 3.592 8.32513L7.5564 10.6244L6.2561 11.3697C6.14517 11.433 6.04993 11.433 5.939 11.3697ZM5.76467 13.9703C3.9728 13.9703 2.6566 12.6224 2.6566 10.9574C2.6566 10.8305 2.6725 10.7036 2.68826 10.5768L5.81213 12.3845C6.00237 12.4955 6.19273 12.4955 6.38297 12.3845L10.3631 10.0853V11.5918C10.3631 11.7186 10.3155 11.8138 10.2046 11.8772L7.17577 13.6215C6.76347 13.8593 6.27203 13.9703 5.76467 13.9703ZM9.6972 15.8572C11.6159 15.8572 13.2174 14.4935 13.5823 12.6857C15.3583 12.2258 16.5 10.5608 16.5 8.86417C16.5 7.7541 16.0243 6.6759 15.168 5.89887C15.2473 5.56583 15.2949 5.2328 15.2949 4.89993C15.2949 2.6324 13.4554 0.935567 11.3305 0.935567C10.9025 0.935567 10.4902 0.99892 10.0778 1.14172C9.36417 0.443973 8.381 0 7.30277 0C5.38407 0 3.78256 1.36364 3.41771 3.17142C1.64172 3.63133 0.5 5.29633 0.5 6.993C0.5 8.10307 0.975663 9.18127 1.83198 9.9583C1.7527 10.2913 1.70511 10.6244 1.70511 10.9572C1.70511 13.2248 3.54458 14.9216 5.66947 14.9216C6.09753 14.9216 6.50983 14.8582 6.92217 14.7154C7.63567 15.4132 8.61883 15.8572 9.6972 15.8572Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1800">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1732)">
|
||||
<path d="M6.63673 5.772V4.26557C6.63673 4.13867 6.68433 4.0435 6.79527 3.98013L9.8241 2.23585C10.2364 1.998 10.728 1.88706 11.2353 1.88706C13.1382 1.88706 14.3434 3.3618 14.3434 4.9316C14.3434 5.0426 14.3434 5.16947 14.3275 5.29633L11.1877 3.45687C10.9975 3.3459 10.8071 3.3459 10.6169 3.45687L6.63673 5.772ZM13.709 11.6392V8.03953C13.709 7.8175 13.6138 7.65893 13.4236 7.54793L9.44343 5.2328L10.7437 4.48747C10.8547 4.42413 10.9499 4.42413 11.0609 4.48747L14.0897 6.23177C14.9619 6.73927 15.5485 7.8175 15.5485 8.864C15.5485 10.0691 14.835 11.1793 13.709 11.6392ZM5.70117 8.46777L4.40087 7.70667C4.28993 7.64333 4.2423 7.5481 4.2423 7.42123V3.9327C4.2423 2.23602 5.5426 0.951497 7.30277 0.951497C7.96887 0.951497 8.58717 1.17355 9.1106 1.56996L5.98673 3.37773C5.7965 3.4887 5.7013 3.64723 5.7013 3.86933L5.70117 8.46777ZM8.5 10.0852L6.63673 9.03863V6.8187L8.5 5.77217L10.3631 6.8187V9.03863L8.5 10.0852ZM9.6972 14.9058C9.03113 14.9058 8.41283 14.6838 7.8894 14.2874L11.0132 12.4796C11.2035 12.3686 11.2987 12.2101 11.2987 11.988V7.3894L12.6149 8.15053C12.7258 8.21387 12.7735 8.30907 12.7735 8.43597V11.9245C12.7735 13.6212 11.4572 14.9058 9.6972 14.9058ZM5.939 11.3697L2.91018 9.62543C2.03797 9.1179 1.45133 8.0397 1.45133 6.99317C1.45133 5.77217 2.18077 4.67803 3.30657 4.21813V7.83357C3.30657 8.0556 3.40178 8.21417 3.592 8.32513L7.5564 10.6244L6.2561 11.3697C6.14517 11.433 6.04993 11.433 5.939 11.3697ZM5.76467 13.9703C3.9728 13.9703 2.6566 12.6224 2.6566 10.9574C2.6566 10.8305 2.6725 10.7036 2.68826 10.5768L5.81213 12.3845C6.00237 12.4955 6.19273 12.4955 6.38297 12.3845L10.3631 10.0853V11.5918C10.3631 11.7186 10.3155 11.8138 10.2046 11.8772L7.17577 13.6215C6.76347 13.8593 6.27203 13.9703 5.76467 13.9703ZM9.6972 15.8572C11.6159 15.8572 13.2174 14.4935 13.5823 12.6857C15.3583 12.2258 16.5 10.5608 16.5 8.86417C16.5 7.7541 16.0243 6.6759 15.168 5.89887C15.2473 5.56583 15.2949 5.2328 15.2949 4.89993C15.2949 2.6324 13.4554 0.935567 11.3305 0.935567C10.9025 0.935567 10.4902 0.99892 10.0778 1.14172C9.36417 0.443973 8.381 0 7.30277 0C5.38407 0 3.78256 1.36364 3.41771 3.17142C1.64172 3.63133 0.5 5.29633 0.5 6.993C0.5 8.10307 0.975663 9.18127 1.83198 9.9583C1.7527 10.2913 1.70511 10.6244 1.70511 10.9572C1.70511 13.2248 3.54458 14.9216 5.66947 14.9216C6.09753 14.9216 6.50983 14.8582 6.92217 14.7154C7.63567 15.4132 8.61883 15.8572 9.6972 15.8572Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2092_1732">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,74 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1770)">
|
||||
<mask id="mask0_2092_1770" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1770)">
|
||||
<mask id="mask1_2092_1770" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2092_1770)">
|
||||
<path d="M16.131 10.9838L9.24712 15.124C9.02152 15.2597 8.76328 15.3313 8.5 15.3313C8.23675 15.3313 7.97846 15.2597 7.75286 15.124L0.869003 10.9838C0.640038 10.8461 0.5 10.5985 0.5 10.3313C0.5 10.0641 0.640038 9.81645 0.869003 9.67877L7.75286 5.53867C7.97846 5.40299 8.23675 5.3313 8.5 5.3313C8.76328 5.3313 9.02152 5.40299 9.24712 5.53867L16.131 9.67877C16.36 9.81645 16.5 10.0641 16.5 10.3313C16.5 10.5985 16.36 10.8461 16.131 10.9838Z" fill="url(#paint0_linear_2092_1770)"/>
|
||||
<path d="M16.131 8.65256L9.24712 12.7926C9.02152 12.9283 8.76328 13 8.5 13C8.23675 13 7.97846 12.9283 7.75286 12.7926L0.869003 8.65256C0.640038 8.5148 0.5 8.2672 0.5 8C0.5 7.73282 0.640038 7.48518 0.869003 7.34746L7.75286 3.20737C7.97846 3.07168 8.23675 3 8.5 3C8.76328 3 9.02152 3.07168 9.24712 3.20737L16.131 7.34746C16.36 7.48518 16.5 7.73282 16.5 8C16.5 8.2672 16.36 8.5148 16.131 8.65256Z" fill="url(#paint1_linear_2092_1770)"/>
|
||||
<path d="M16.131 6.31818L9.24712 10.4583C9.02152 10.5939 8.76328 10.6656 8.5 10.6656C8.23675 10.6656 7.97846 10.5939 7.75286 10.4583L0.869003 6.31818C0.640038 6.18046 0.5 5.93283 0.5 5.66565C0.5 5.39846 0.640038 5.15083 0.869003 5.01311L7.75286 0.873017C7.97846 0.737337 8.23675 0.665649 8.5 0.665649C8.76328 0.665649 9.02152 0.737337 9.24712 0.873017L16.131 5.01311C16.36 5.15083 16.5 5.39846 16.5 5.66565C16.5 5.93283 16.36 6.18046 16.131 6.31818Z" fill="url(#paint2_linear_2092_1770)"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint3_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint4_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint5_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint6_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint7_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M11.8334 7.55765C11.8334 8.29403 11.2364 8.89099 10.5 8.89099H5.83334C5.09695 8.89099 4.5 8.29403 4.5 7.55765V2.66565H11.8334V7.55765Z" fill="url(#paint8_radial_2092_1770)" fill-opacity="0.4"/>
|
||||
<path d="M8.01858 3.31028L7.76982 2.80011C7.749 2.7608 7.7104 2.72675 7.65934 2.70267C7.60826 2.67859 7.54725 2.66565 7.48468 2.66565C7.42214 2.66565 7.36112 2.67859 7.31005 2.70267C7.25898 2.72675 7.22038 2.7608 7.19957 2.80011L6.9508 3.31028C6.87498 3.46428 6.74675 3.60454 6.57612 3.72002C6.40548 3.83551 6.19708 3.92314 5.9672 3.97603L5.20177 4.14185C5.14277 4.15571 5.09168 4.18143 5.05555 4.21548C5.0194 4.24951 5 4.29019 5 4.33188C5 4.37357 5.0194 4.41423 5.05555 4.44828C5.09168 4.48231 5.14277 4.50803 5.20177 4.52191L5.9672 4.68771C6.16372 4.73139 6.34502 4.80035 6.501 4.89045C6.53005 4.90723 6.55823 4.92475 6.58548 4.94297C6.72874 5.03882 6.84258 5.152 6.92118 5.27615C6.93774 5.30231 6.95274 5.32895 6.9661 5.35602L7.21486 5.86619C7.23363 5.90162 7.26685 5.93279 7.31062 5.95622C7.3154 5.95879 7.32032 5.96127 7.32535 5.96363C7.37642 5.98771 7.43743 6.00065 7.5 6.00065C7.56257 6.00065 7.62358 5.98771 7.67465 5.96363C7.72572 5.93955 7.76432 5.9055 7.78514 5.86619L8.0339 5.35602C8.11125 5.20091 8.24182 5.05999 8.41522 4.94442C8.58864 4.82885 8.80008 4.74182 9.0328 4.69027L9.79824 4.52447C9.8572 4.51059 9.90832 4.48487 9.94448 4.45083C9.98056 4.41679 10 4.37611 10 4.33443C10 4.29274 9.98056 4.25207 9.94448 4.21803C9.90832 4.18399 9.8572 4.15827 9.79824 4.1444L9.78296 4.14185L9.01752 3.97603C8.7848 3.92448 8.57328 3.83747 8.39992 3.72188C8.22652 3.60631 8.09595 3.46539 8.01858 3.31028Z" fill="url(#paint9_linear_2092_1770)"/>
|
||||
<path d="M11.0775 6.78623L11.5368 6.88572L11.546 6.88725C11.5813 6.89557 11.612 6.911 11.6336 6.93143C11.6553 6.95185 11.667 6.97625 11.667 7.00126C11.667 7.02628 11.6553 7.05068 11.6336 7.0711C11.612 7.09154 11.5813 7.10697 11.546 7.11528L11.0867 7.21477C10.947 7.2457 10.8202 7.29792 10.7161 7.36726C10.6121 7.4366 10.5337 7.52117 10.4873 7.61422L10.338 7.92032C10.3256 7.94392 10.3024 7.96434 10.2718 7.97878C10.2412 7.99323 10.2045 8.00104 10.167 8.00104C10.1295 8.00104 10.0928 7.99323 10.0622 7.97878C10.0316 7.96434 10.0084 7.94392 9.99587 7.92032L9.99539 7.91937L9.84667 7.61422C9.80051 7.52088 9.72235 7.43602 9.61827 7.3664C9.51419 7.29677 9.38715 7.24434 9.24731 7.21323L8.78803 7.11375C8.75267 7.10543 8.72195 7.09 8.70035 7.06958C8.67859 7.04915 8.66699 7.02475 8.66699 6.99974C8.66699 6.97472 8.67859 6.95032 8.70035 6.9299C8.72195 6.90946 8.75267 6.89403 8.78803 6.88572L9.24731 6.78623C9.38523 6.7545 9.51027 6.70192 9.61267 6.63262C9.71499 6.56334 9.79195 6.47918 9.83747 6.38678L9.98675 6.08068C9.99923 6.05708 10.0224 6.03666 10.053 6.02222C10.0836 6.00777 10.1203 6 10.1578 6C10.1953 6 10.232 6.00777 10.2626 6.02222C10.2932 6.03666 10.3164 6.05708 10.3288 6.08068L10.4781 6.38678C10.5245 6.47983 10.6029 6.5644 10.7069 6.63375C10.811 6.70308 10.9379 6.7553 11.0775 6.78623Z" fill="url(#paint10_linear_2092_1770)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1770" x1="0.5" y1="5.3313" x2="9.4888" y2="19.7133" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#004695"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1770" x1="0.5" y1="3" x2="9.4888" y2="17.382" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0078D4"/>
|
||||
<stop offset="1" stop-color="#0FAFFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1770" x1="0.9" y1="0.66565" x2="9.8888" y2="15.0477" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3BD5FF"/>
|
||||
<stop offset="1" stop-color="#0FAFFF"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint3_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7.5 5.33231) rotate(90) scale(1 0.97124)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint4_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.5 4.99899) rotate(-14.0362) scale(1.37437 0.380635)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.5 4.99899) rotate(-165.964) scale(1.37437 0.384775)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint6_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.5 7.33231) rotate(-153.435) scale(0.745357 0.359002)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint7_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.8334 7.33231) rotate(-26.565) scale(0.745357 0.290223)">
|
||||
<stop stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint8_radial_2092_1770" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.1666 7.66565) rotate(90) scale(0.666666 0.627526)">
|
||||
<stop offset="0.0638343" stop-color="#00204D"/>
|
||||
<stop offset="1" stop-color="#00204D" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint9_linear_2092_1770" x1="6.43217" y1="3.09882" x2="8.32475" y2="8.55931" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#DFFAFF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_2092_1770" x1="6.43248" y1="3.09983" x2="8.32506" y2="8.56032" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="#DFFAFF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2092_1770">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
@@ -0,0 +1,54 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
using Microsoft.SemanticKernel;
|
||||
|
||||
namespace AdvancedPaste.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for extracting AI service usage information from chat messages.
|
||||
/// </summary>
|
||||
public static class AIServiceUsageHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts AI service usage information from OpenAI chat message metadata.
|
||||
/// </summary>
|
||||
/// <param name="chatMessage">The chat message containing usage metadata.</param>
|
||||
/// <returns>AI service usage information or AIServiceUsage.None if extraction fails.</returns>
|
||||
public static AIServiceUsage GetOpenAIServiceUsage(ChatMessageContent chatMessage)
|
||||
{
|
||||
// Try to get usage information from metadata
|
||||
if (chatMessage.Metadata?.TryGetValue("Usage", out var usageObj) == true)
|
||||
{
|
||||
// Handle different possible usage types through reflection to be version-agnostic
|
||||
var usageType = usageObj.GetType();
|
||||
|
||||
try
|
||||
{
|
||||
// Try common property names for prompt tokens
|
||||
var promptTokensProp = usageType.GetProperty("PromptTokens") ??
|
||||
usageType.GetProperty("InputTokens") ??
|
||||
usageType.GetProperty("InputTokenCount");
|
||||
|
||||
var completionTokensProp = usageType.GetProperty("CompletionTokens") ??
|
||||
usageType.GetProperty("OutputTokens") ??
|
||||
usageType.GetProperty("OutputTokenCount");
|
||||
|
||||
if (promptTokensProp != null && completionTokensProp != null)
|
||||
{
|
||||
var promptTokens = (int)(promptTokensProp.GetValue(usageObj) ?? 0);
|
||||
var completionTokens = (int)(completionTokensProp.GetValue(usageObj) ?? 0);
|
||||
return new AIServiceUsage(promptTokens, completionTokens);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If reflection fails, fall back to no usage
|
||||
}
|
||||
}
|
||||
|
||||
return AIServiceUsage.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Helpers
|
||||
{
|
||||
internal static class ClipboardItemHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a ClipboardItem from current clipboard data.
|
||||
/// </summary>
|
||||
public static async Task<ClipboardItem> CreateFromCurrentClipboardAsync(
|
||||
DataPackageView clipboardData,
|
||||
ClipboardFormat availableFormats,
|
||||
DateTimeOffset? timestamp = null,
|
||||
BitmapImage existingImage = null)
|
||||
{
|
||||
if (clipboardData == null || availableFormats == ClipboardFormat.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var clipboardItem = new ClipboardItem
|
||||
{
|
||||
Format = availableFormats,
|
||||
Timestamp = timestamp,
|
||||
};
|
||||
|
||||
// Text or HTML content
|
||||
if (availableFormats.HasFlag(ClipboardFormat.Text) || availableFormats.HasFlag(ClipboardFormat.Html))
|
||||
{
|
||||
clipboardItem.Content = await clipboardData.GetTextOrEmptyAsync();
|
||||
}
|
||||
|
||||
// Image content
|
||||
else if (availableFormats.HasFlag(ClipboardFormat.Image))
|
||||
{
|
||||
// Reuse existing image if provided
|
||||
if (existingImage != null)
|
||||
{
|
||||
clipboardItem.Image = existingImage;
|
||||
}
|
||||
else
|
||||
{
|
||||
clipboardItem.Image = await TryCreateBitmapImageAsync(clipboardData);
|
||||
}
|
||||
}
|
||||
|
||||
return clipboardItem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BitmapImage from clipboard data.
|
||||
/// </summary>
|
||||
private static async Task<BitmapImage> TryCreateBitmapImageAsync(DataPackageView clipboardData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageReference = await clipboardData.GetBitmapAsync();
|
||||
if (imageReference != null)
|
||||
{
|
||||
using (var imageStream = await imageReference.OpenReadAsync())
|
||||
{
|
||||
var bitmapImage = new BitmapImage();
|
||||
await bitmapImage.SetSourceAsync(imageStream);
|
||||
return bitmapImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail - caller can check for null
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Microsoft.Win32;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Data.Html;
|
||||
@@ -180,6 +182,46 @@ internal static class DataPackageHelpers
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<string> GetClipboardTextOrThrowAsync(this DataPackageView dataPackageView, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dataPackageView);
|
||||
|
||||
try
|
||||
{
|
||||
if (dataPackageView.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
return await dataPackageView.GetTextAsync();
|
||||
}
|
||||
|
||||
if (dataPackageView.Contains(StandardDataFormats.Html))
|
||||
{
|
||||
var html = await dataPackageView.GetHtmlFormatAsync();
|
||||
return HtmlUtilities.ConvertToText(html);
|
||||
}
|
||||
|
||||
if (dataPackageView.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
var bitmap = await dataPackageView.GetImageContentAsync();
|
||||
if (bitmap != null)
|
||||
{
|
||||
return await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is COMException or InvalidOperationException)
|
||||
{
|
||||
throw CreateClipboardTextMissingException(ex);
|
||||
}
|
||||
|
||||
throw CreateClipboardTextMissingException();
|
||||
}
|
||||
|
||||
private static PasteActionException CreateClipboardTextMissingException(Exception innerException = null)
|
||||
{
|
||||
var message = ResourceLoaderInstance.ResourceLoader.GetString("ClipboardEmptyWarning");
|
||||
return new PasteActionException(message, innerException ?? new InvalidOperationException("Clipboard does not contain text content."));
|
||||
}
|
||||
|
||||
internal static async Task<string> GetHtmlContentAsync(this DataPackageView dataPackageView) =>
|
||||
dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty;
|
||||
|
||||
@@ -195,6 +237,22 @@ internal static class DataPackageHelpers
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static async Task<BitmapImage> GetPreviewBitmapAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
var stream = await dataPackageView.GetImageStreamAsync();
|
||||
if (stream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (stream)
|
||||
{
|
||||
var bitmapImage = new BitmapImage();
|
||||
bitmapImage.SetSource(stream);
|
||||
return bitmapImage;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IRandomAccessStream> GetImageStreamAsync(this DataPackageView dataPackageView)
|
||||
{
|
||||
if (dataPackageView.Contains(StandardDataFormats.StorageItems))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
@@ -12,7 +13,7 @@ namespace AdvancedPaste.Settings
|
||||
{
|
||||
public interface IUserSettings
|
||||
{
|
||||
public bool IsAdvancedAIEnabled { get; }
|
||||
public bool IsAIEnabled { get; }
|
||||
|
||||
public bool ShowCustomPreview { get; }
|
||||
|
||||
@@ -22,6 +23,10 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions { get; }
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; }
|
||||
|
||||
public event EventHandler Changed;
|
||||
|
||||
Task SetActiveAIProviderAsync(string providerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using AdvancedPaste.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
||||
using Windows.Security.Credentials;
|
||||
|
||||
namespace AdvancedPaste.Settings
|
||||
{
|
||||
@@ -33,7 +34,7 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public event EventHandler Changed;
|
||||
|
||||
public bool IsAdvancedAIEnabled { get; private set; }
|
||||
public bool IsAIEnabled { get; private set; }
|
||||
|
||||
public bool ShowCustomPreview { get; private set; }
|
||||
|
||||
@@ -43,13 +44,16 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; private set; }
|
||||
|
||||
public UserSettings(IFileSystem fileSystem)
|
||||
{
|
||||
_settingsUtils = new SettingsUtils(fileSystem);
|
||||
|
||||
IsAdvancedAIEnabled = false;
|
||||
IsAIEnabled = false;
|
||||
ShowCustomPreview = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
PasteAIConfiguration = new PasteAIConfiguration();
|
||||
_additionalActions = [];
|
||||
_customActions = [];
|
||||
_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
@@ -94,13 +98,16 @@ namespace AdvancedPaste.Settings
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteModuleName);
|
||||
if (settings != null)
|
||||
{
|
||||
bool migratedLegacyEnablement = TryMigrateLegacyAIEnablement(settings);
|
||||
|
||||
void UpdateSettings()
|
||||
{
|
||||
var properties = settings.Properties;
|
||||
|
||||
IsAdvancedAIEnabled = properties.IsAdvancedAIEnabled;
|
||||
IsAIEnabled = properties.IsAIEnabled;
|
||||
ShowCustomPreview = properties.ShowCustomPreview;
|
||||
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
|
||||
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
|
||||
|
||||
var sourceAdditionalActions = properties.AdditionalActions;
|
||||
(PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats =
|
||||
@@ -126,6 +133,11 @@ namespace AdvancedPaste.Settings
|
||||
Task.Factory
|
||||
.StartNew(UpdateSettings, CancellationToken.None, TaskCreationOptions.None, _taskScheduler)
|
||||
.Wait();
|
||||
|
||||
if (migratedLegacyEnablement)
|
||||
{
|
||||
settings.Save(_settingsUtils);
|
||||
}
|
||||
}
|
||||
|
||||
retry = false;
|
||||
@@ -144,6 +156,114 @@ namespace AdvancedPaste.Settings
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryMigrateLegacyAIEnablement(AdvancedPasteSettings settings)
|
||||
{
|
||||
if (settings?.Properties is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (settings.Properties.IsAIEnabled || !LegacyOpenAIKeyExists())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
settings.Properties.IsAIEnabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool LegacyOpenAIKeyExists()
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordVault vault = new();
|
||||
return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetActiveAIProviderAsync(string providerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteModuleName);
|
||||
var configuration = settings?.Properties?.PasteAIConfiguration;
|
||||
var providers = configuration?.Providers;
|
||||
|
||||
if (configuration == null || providers == null || providers.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = providers.FirstOrDefault(provider => string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase));
|
||||
if (target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(configuration.ActiveProvider?.Id, providerId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
configuration.ActiveProviderId = providerId;
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
provider.IsActive = string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
settings.Save(_settingsUtils);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to set active AI provider", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Task.Factory
|
||||
.StartNew(
|
||||
() =>
|
||||
{
|
||||
PasteAIConfiguration.ActiveProviderId = providerId;
|
||||
|
||||
if (PasteAIConfiguration.Providers is not null)
|
||||
{
|
||||
foreach (var provider in PasteAIConfiguration.Providers)
|
||||
{
|
||||
provider.IsActive = string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_taskScheduler)
|
||||
.Wait();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to dispatch active AI provider change", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
@@ -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 System;
|
||||
using AdvancedPaste.Helpers;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
@@ -12,10 +13,15 @@ public class ClipboardItem
|
||||
{
|
||||
public string Content { get; set; }
|
||||
|
||||
public ClipboardHistoryItem Item { get; set; }
|
||||
|
||||
public BitmapImage Image { get; set; }
|
||||
|
||||
public ClipboardFormat Format { get; set; }
|
||||
|
||||
public DateTimeOffset? Timestamp { get; set; }
|
||||
|
||||
// Only used for clipboard history items that have a ClipboardHistoryItem
|
||||
public ClipboardHistoryItem Item { get; set; }
|
||||
|
||||
public string Description => !string.IsNullOrEmpty(Content) ? Content :
|
||||
Image is not null ? ResourceLoaderInstance.ResourceLoader.GetString("ClipboardHistoryImage") :
|
||||
string.Empty;
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
// 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 AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Settings;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.Amazon;
|
||||
using Microsoft.SemanticKernel.Connectors.AzureAIInference;
|
||||
using Microsoft.SemanticKernel.Connectors.Google;
|
||||
using Microsoft.SemanticKernel.Connectors.HuggingFace;
|
||||
using Microsoft.SemanticKernel.Connectors.MistralAI;
|
||||
using Microsoft.SemanticKernel.Connectors.Ollama;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public sealed class AdvancedAIKernelService : KernelServiceBase
|
||||
{
|
||||
private readonly IAICredentialsProvider credentialsProvider;
|
||||
|
||||
private readonly record struct RuntimeConfig(
|
||||
AIServiceType ServiceType,
|
||||
string ModelName,
|
||||
string Endpoint,
|
||||
string DeploymentName,
|
||||
string ModelPath,
|
||||
bool UsePasteScope,
|
||||
bool ModerationEnabled);
|
||||
|
||||
public AdvancedAIKernelService(
|
||||
IAICredentialsProvider credentialsProvider,
|
||||
IKernelQueryCacheService queryCacheService,
|
||||
IPromptModerationService promptModerationService,
|
||||
IUserSettings userSettings,
|
||||
ICustomActionTransformService customActionTransformService)
|
||||
: base(queryCacheService, promptModerationService, userSettings, customActionTransformService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(credentialsProvider);
|
||||
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
}
|
||||
|
||||
protected override string AdvancedAIModelName => GetRuntimeConfig().ModelName;
|
||||
|
||||
protected override PromptExecutionSettings PromptExecutionSettings => CreatePromptExecutionSettings();
|
||||
|
||||
protected override void AddChatCompletionService(IKernelBuilder kernelBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(kernelBuilder);
|
||||
|
||||
var runtimeConfig = GetRuntimeConfig();
|
||||
var serviceType = runtimeConfig.ServiceType;
|
||||
var modelName = runtimeConfig.ModelName;
|
||||
var requiresApiKey = RequiresApiKey(serviceType);
|
||||
var apiKey = string.Empty;
|
||||
if (requiresApiKey)
|
||||
{
|
||||
var scope = runtimeConfig.UsePasteScope ? AICredentialScope.PasteAI : AICredentialScope.AdvancedAI;
|
||||
this.credentialsProvider.Refresh(scope);
|
||||
apiKey = (this.credentialsProvider.GetKey(scope) ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
throw new InvalidOperationException($"An API key is required for {serviceType} but none was found in the credential vault.");
|
||||
}
|
||||
}
|
||||
|
||||
var endpoint = string.IsNullOrWhiteSpace(runtimeConfig.Endpoint) ? null : runtimeConfig.Endpoint.Trim();
|
||||
var deployment = string.IsNullOrWhiteSpace(runtimeConfig.DeploymentName) ? modelName : runtimeConfig.DeploymentName;
|
||||
|
||||
switch (serviceType)
|
||||
{
|
||||
case AIServiceType.OpenAI:
|
||||
kernelBuilder.AddOpenAIChatCompletion(modelName, apiKey, serviceId: modelName);
|
||||
break;
|
||||
case AIServiceType.AzureOpenAI:
|
||||
kernelBuilder.AddAzureOpenAIChatCompletion(deployment, RequireEndpoint(endpoint, serviceType), apiKey, serviceId: modelName);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Service type '{runtimeConfig.ServiceType}' is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage)
|
||||
{
|
||||
return AIServiceUsageHelper.GetOpenAIServiceUsage(chatMessage);
|
||||
}
|
||||
|
||||
protected override bool ShouldModerateAdvancedAI()
|
||||
{
|
||||
if (!TryGetRuntimeConfig(out var runtimeConfig))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return runtimeConfig.ModerationEnabled && (runtimeConfig.ServiceType == AIServiceType.OpenAI || runtimeConfig.ServiceType == AIServiceType.AzureOpenAI);
|
||||
}
|
||||
|
||||
private static string GetModelName(PasteAIProviderDefinition config)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(config?.ModelName))
|
||||
{
|
||||
return config.ModelName;
|
||||
}
|
||||
|
||||
return "gpt-4o";
|
||||
}
|
||||
|
||||
private RuntimeConfig GetRuntimeConfig()
|
||||
{
|
||||
if (TryGetRuntimeConfig(out var runtimeConfig))
|
||||
{
|
||||
return runtimeConfig;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No Advanced AI provider is configured.");
|
||||
}
|
||||
|
||||
private bool TryGetRuntimeConfig(out RuntimeConfig runtimeConfig)
|
||||
{
|
||||
runtimeConfig = default;
|
||||
|
||||
if (!TryResolveAdvancedProvider(out var provider, out var usePasteScope))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
|
||||
if (!IsServiceTypeSupported(serviceType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
runtimeConfig = new RuntimeConfig(
|
||||
serviceType,
|
||||
GetModelName(provider),
|
||||
provider.EndpointUrl,
|
||||
provider.DeploymentName,
|
||||
provider.ModelPath,
|
||||
usePasteScope,
|
||||
provider.ModerationEnabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryResolveAdvancedProvider(out PasteAIProviderDefinition provider, out bool usePasteScope)
|
||||
{
|
||||
provider = null;
|
||||
usePasteScope = false;
|
||||
|
||||
var configuration = this.UserSettings?.PasteAIConfiguration;
|
||||
if (configuration is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var activeProvider = configuration.ActiveProvider;
|
||||
if (IsAdvancedProvider(activeProvider))
|
||||
{
|
||||
provider = activeProvider;
|
||||
usePasteScope = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedProvider);
|
||||
if (fallback is not null)
|
||||
{
|
||||
provider = fallback;
|
||||
usePasteScope = configuration.UseSharedCredentials;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsAdvancedProvider(PasteAIProviderDefinition provider)
|
||||
{
|
||||
if (provider is null || !provider.EnableAdvancedAI)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
|
||||
return IsServiceTypeSupported(serviceType);
|
||||
}
|
||||
|
||||
private static bool IsServiceTypeSupported(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType is AIServiceType.OpenAI or AIServiceType.AzureOpenAI;
|
||||
}
|
||||
|
||||
private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
|
||||
}
|
||||
|
||||
private static bool RequiresApiKey(AIServiceType serviceType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string RequireEndpoint(string endpoint, AIServiceType serviceType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Endpoint is required for {serviceType} configuration but was not provided.");
|
||||
}
|
||||
|
||||
private PromptExecutionSettings CreatePromptExecutionSettings()
|
||||
{
|
||||
var serviceType = GetRuntimeConfig().ServiceType;
|
||||
return new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
||||
Temperature = 0.01,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class CustomActionTransformResult
|
||||
{
|
||||
public CustomActionTransformResult(string content, AIServiceUsage usage)
|
||||
{
|
||||
Content = content;
|
||||
Usage = usage;
|
||||
}
|
||||
|
||||
public string Content { get; }
|
||||
|
||||
public AIServiceUsage Usage { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Settings;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class CustomActionTransformService : ICustomActionTransformService
|
||||
{
|
||||
private const string DefaultSystemPrompt = """
|
||||
You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it.
|
||||
Do not output anything else besides the reformatted clipboard content.
|
||||
""";
|
||||
|
||||
private readonly IPromptModerationService promptModerationService;
|
||||
private readonly IPasteAIProviderFactory providerFactory;
|
||||
private readonly IAICredentialsProvider credentialsProvider;
|
||||
private readonly IUserSettings userSettings;
|
||||
|
||||
public CustomActionTransformService(IPromptModerationService promptModerationService, IPasteAIProviderFactory providerFactory, IAICredentialsProvider credentialsProvider, IUserSettings userSettings)
|
||||
{
|
||||
this.promptModerationService = promptModerationService;
|
||||
this.providerFactory = providerFactory;
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
this.userSettings = userSettings;
|
||||
}
|
||||
|
||||
public async Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var pasteConfig = userSettings?.PasteAIConfiguration;
|
||||
var providerConfig = BuildProviderConfig(pasteConfig);
|
||||
|
||||
return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress);
|
||||
}
|
||||
|
||||
private async Task<CustomActionTransformResult> TransformAsync(string prompt, string inputText, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providerConfig);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
Logger.LogWarning("Clipboard has no usable text data");
|
||||
return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
|
||||
}
|
||||
|
||||
var systemPrompt = providerConfig.SystemPrompt ?? DefaultSystemPrompt;
|
||||
|
||||
var fullPrompt = (systemPrompt ?? string.Empty) + "\n\n" + (inputText ?? string.Empty);
|
||||
|
||||
if (ShouldModerate(providerConfig))
|
||||
{
|
||||
await promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var provider = providerFactory.CreateProvider(providerConfig);
|
||||
|
||||
var request = new PasteAIRequest
|
||||
{
|
||||
Prompt = prompt,
|
||||
InputText = inputText,
|
||||
SystemPrompt = systemPrompt,
|
||||
};
|
||||
|
||||
var providerContent = await provider.ProcessPasteAsync(
|
||||
request,
|
||||
cancellationToken,
|
||||
progress);
|
||||
|
||||
var usage = request.Usage;
|
||||
var content = providerContent ?? string.Empty;
|
||||
|
||||
Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}");
|
||||
|
||||
return new CustomActionTransformResult(content, usage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} failed", ex);
|
||||
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
throw new PasteActionException(ErrorHelpers.TranslateErrorText(-1), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
|
||||
}
|
||||
|
||||
private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config)
|
||||
{
|
||||
config ??= new PasteAIConfiguration();
|
||||
var provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition();
|
||||
var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
|
||||
var systemPrompt = string.IsNullOrWhiteSpace(provider.SystemPrompt) ? DefaultSystemPrompt : provider.SystemPrompt;
|
||||
var apiKey = AcquireApiKey(serviceType);
|
||||
var modelName = provider.ModelName;
|
||||
|
||||
var providerConfig = new PasteAIConfig
|
||||
{
|
||||
ProviderType = serviceType,
|
||||
ApiKey = apiKey,
|
||||
Model = modelName,
|
||||
Endpoint = provider.EndpointUrl,
|
||||
DeploymentName = provider.DeploymentName,
|
||||
LocalModelPath = provider.ModelPath,
|
||||
ModelPath = provider.ModelPath,
|
||||
SystemPrompt = systemPrompt,
|
||||
ModerationEnabled = provider.ModerationEnabled,
|
||||
};
|
||||
|
||||
return providerConfig;
|
||||
}
|
||||
|
||||
private string AcquireApiKey(AIServiceType serviceType)
|
||||
{
|
||||
if (!RequiresApiKey(serviceType))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
credentialsProvider.Refresh(AICredentialScope.PasteAI);
|
||||
return credentialsProvider.GetKey(AICredentialScope.PasteAI) ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool RequiresApiKey(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.Onnx => false,
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ShouldModerate(PasteAIConfig providerConfig)
|
||||
{
|
||||
if (providerConfig is null || !providerConfig.ModerationEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return providerConfig.ProviderType == AIServiceType.OpenAI || providerConfig.ProviderType == AIServiceType.AzureOpenAI;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// 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.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Models;
|
||||
using LanguageModelProvider;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions;
|
||||
|
||||
public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
{
|
||||
private static readonly IReadOnlyCollection<AIServiceType> SupportedTypes = new[]
|
||||
{
|
||||
AIServiceType.FoundryLocal,
|
||||
};
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new FoundryLocalPasteProvider(config));
|
||||
|
||||
private static readonly LanguageModelService LanguageModels = LanguageModelService.CreateDefault();
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
|
||||
public FoundryLocalPasteProvider(PasteAIConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey();
|
||||
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model;
|
||||
|
||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await FoundryLocalModelProvider.Instance.IsAvailable().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var systemPrompt = request.SystemPrompt;
|
||||
if (string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
throw new ArgumentException("System prompt must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var prompt = request.Prompt;
|
||||
var inputText = request.InputText;
|
||||
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
throw new ArgumentException("Prompt and input text must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var modelReference = _config?.Model;
|
||||
if (string.IsNullOrWhiteSpace(modelReference))
|
||||
{
|
||||
throw new InvalidOperationException("Foundry Local requires a model identifier (for example, 'fl://model-name').");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var chatClient = LanguageModels.GetClient(modelReference);
|
||||
if (chatClient is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to resolve Foundry Local client for '{modelReference}'. Ensure the model is downloaded.");
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Text:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
|
||||
var chatMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.System, systemPrompt),
|
||||
new(ChatRole.User, userMessageContent),
|
||||
};
|
||||
|
||||
var chatOptions = CreateChatOptions(_config?.SystemPrompt, modelReference);
|
||||
|
||||
progress?.Report(0.1);
|
||||
|
||||
var response = await chatClient.GetResponseAsync(chatMessages, chatOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress?.Report(0.8);
|
||||
|
||||
var responseText = GetResponseText(response);
|
||||
request.Usage = ToUsage(response.Usage);
|
||||
|
||||
progress?.Report(1.0);
|
||||
|
||||
return responseText ?? string.Empty;
|
||||
}
|
||||
|
||||
private static ChatOptions CreateChatOptions(string systemPrompt, string modelReference)
|
||||
{
|
||||
var options = new ChatOptions
|
||||
{
|
||||
ModelId = modelReference,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
options.Instructions = systemPrompt;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static string GetResponseText(ChatResponse response)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(response.Text))
|
||||
{
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
if (response.Messages is { Count: > 0 })
|
||||
{
|
||||
var lastMessage = response.Messages.LastOrDefault(m => !string.IsNullOrWhiteSpace(m.Text));
|
||||
if (!string.IsNullOrWhiteSpace(lastMessage?.Text))
|
||||
{
|
||||
return lastMessage.Text;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static AIServiceUsage ToUsage(UsageDetails usageDetails)
|
||||
{
|
||||
if (usageDetails is null)
|
||||
{
|
||||
return AIServiceUsage.None;
|
||||
}
|
||||
|
||||
int promptTokens = (int)(usageDetails.InputTokenCount ?? 0);
|
||||
int completionTokens = (int)(usageDetails.OutputTokenCount ?? 0);
|
||||
|
||||
if (promptTokens == 0 && completionTokens == 0)
|
||||
{
|
||||
return AIServiceUsage.None;
|
||||
}
|
||||
|
||||
return new AIServiceUsage(promptTokens, completionTokens);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Settings;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface ICustomActionTransformService
|
||||
{
|
||||
Task<CustomActionTransformResult> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface IPasteAIProvider
|
||||
{
|
||||
Task<bool> IsAvailableAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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 AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public interface IPasteAIProviderFactory
|
||||
{
|
||||
IPasteAIProvider CreateProvider(PasteAIConfig config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class LocalModelPasteProvider : IPasteAIProvider
|
||||
{
|
||||
private static readonly IReadOnlyCollection<AIServiceType> SupportedTypes = new[]
|
||||
{
|
||||
AIServiceType.Onnx,
|
||||
AIServiceType.ML,
|
||||
};
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new LocalModelPasteProvider(config));
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
|
||||
public LocalModelPasteProvider(PasteAIConfig config)
|
||||
{
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
public Task<bool> IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true);
|
||||
|
||||
public Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
// TODO: Implement local model inference logic using _config.LocalModelPath/_config.ModelPath
|
||||
var content = request.InputText ?? string.Empty;
|
||||
request.Usage = AIServiceUsage.None;
|
||||
return Task.FromResult(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public class PasteAIConfig
|
||||
{
|
||||
public AIServiceType ProviderType { get; set; }
|
||||
|
||||
public string Model { get; set; }
|
||||
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
public string DeploymentName { get; set; }
|
||||
|
||||
public string LocalModelPath { get; set; }
|
||||
|
||||
public string ModelPath { get; set; }
|
||||
|
||||
public string SystemPrompt { get; set; }
|
||||
|
||||
public bool ModerationEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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 Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class PasteAIProviderFactory : IPasteAIProviderFactory
|
||||
{
|
||||
private static readonly IReadOnlyList<PasteAIProviderRegistration> ProviderRegistrations = new[]
|
||||
{
|
||||
SemanticKernelPasteProvider.Registration,
|
||||
LocalModelPasteProvider.Registration,
|
||||
FoundryLocalPasteProvider.Registration,
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>> ProviderFactories = CreateProviderFactories();
|
||||
|
||||
public IPasteAIProvider CreateProvider(PasteAIConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
var serviceType = config.ProviderType;
|
||||
if (serviceType == AIServiceType.Unknown)
|
||||
{
|
||||
serviceType = AIServiceType.OpenAI;
|
||||
config.ProviderType = serviceType;
|
||||
}
|
||||
|
||||
if (!ProviderFactories.TryGetValue(serviceType, out var factory))
|
||||
{
|
||||
throw new NotSupportedException($"Provider {config.ProviderType} not supported");
|
||||
}
|
||||
|
||||
return factory(config);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>> CreateProviderFactories()
|
||||
{
|
||||
var map = new Dictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>>();
|
||||
|
||||
foreach (var registration in ProviderRegistrations)
|
||||
{
|
||||
Register(map, registration.SupportedTypes, registration.Factory);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void Register(Dictionary<AIServiceType, Func<PasteAIConfig, IPasteAIProvider>> map, IReadOnlyCollection<AIServiceType> types, Func<PasteAIConfig, IPasteAIProvider> factory)
|
||||
{
|
||||
foreach (var type in types)
|
||||
{
|
||||
map[type] = factory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class PasteAIProviderRegistration
|
||||
{
|
||||
public PasteAIProviderRegistration(IReadOnlyCollection<Microsoft.PowerToys.Settings.UI.Library.AIServiceType> supportedTypes, Func<PasteAIConfig, IPasteAIProvider> factory)
|
||||
{
|
||||
SupportedTypes = supportedTypes ?? throw new ArgumentNullException(nameof(supportedTypes));
|
||||
Factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<Microsoft.PowerToys.Settings.UI.Library.AIServiceType> SupportedTypes { get; }
|
||||
|
||||
public Func<PasteAIConfig, IPasteAIProvider> Factory { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 AdvancedPaste.Models;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class PasteAIRequest
|
||||
{
|
||||
public string Prompt { get; init; }
|
||||
|
||||
public string InputText { get; init; }
|
||||
|
||||
public string SystemPrompt { get; init; }
|
||||
|
||||
public AIServiceUsage Usage { get; set; } = AIServiceUsage.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.Amazon;
|
||||
using Microsoft.SemanticKernel.Connectors.AzureAIInference;
|
||||
using Microsoft.SemanticKernel.Connectors.Google;
|
||||
using Microsoft.SemanticKernel.Connectors.HuggingFace;
|
||||
using Microsoft.SemanticKernel.Connectors.MistralAI;
|
||||
using Microsoft.SemanticKernel.Connectors.Ollama;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
public sealed class SemanticKernelPasteProvider : IPasteAIProvider
|
||||
{
|
||||
private static readonly IReadOnlyCollection<AIServiceType> SupportedTypes = new[]
|
||||
{
|
||||
AIServiceType.OpenAI,
|
||||
AIServiceType.AzureOpenAI,
|
||||
AIServiceType.Mistral,
|
||||
AIServiceType.Google,
|
||||
AIServiceType.HuggingFace,
|
||||
AIServiceType.AzureAIInference,
|
||||
AIServiceType.Ollama,
|
||||
AIServiceType.Anthropic,
|
||||
AIServiceType.AmazonBedrock,
|
||||
};
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new SemanticKernelPasteProvider(config));
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
private readonly AIServiceType _serviceType;
|
||||
|
||||
public SemanticKernelPasteProvider(PasteAIConfig config)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
_config = config;
|
||||
_serviceType = config.ProviderType;
|
||||
if (_serviceType == AIServiceType.Unknown)
|
||||
{
|
||||
_serviceType = AIServiceType.OpenAI;
|
||||
_config.ProviderType = _serviceType;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<AIServiceType> SupportedServiceTypes => SupportedTypes;
|
||||
|
||||
public Task<bool> IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true);
|
||||
|
||||
public async Task<string> ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var systemPrompt = request.SystemPrompt;
|
||||
if (string.IsNullOrWhiteSpace(systemPrompt))
|
||||
{
|
||||
throw new ArgumentException("System prompt must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var prompt = request.Prompt;
|
||||
var inputText = request.InputText;
|
||||
if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
throw new ArgumentException("Prompt and input text must be provided", nameof(request));
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
""";
|
||||
|
||||
var executionSettings = CreateExecutionSettings();
|
||||
var kernel = CreateKernel();
|
||||
var modelId = _config.Model;
|
||||
|
||||
IChatCompletionService chatService;
|
||||
if (!string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
try
|
||||
{
|
||||
chatService = kernel.GetRequiredService<IChatCompletionService>(modelId);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
chatService = kernel.GetRequiredService<IChatCompletionService>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
chatService = kernel.GetRequiredService<IChatCompletionService>();
|
||||
}
|
||||
|
||||
var chatHistory = new ChatHistory();
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddUserMessage(userMessageContent);
|
||||
|
||||
var response = await chatService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel, cancellationToken);
|
||||
chatHistory.Add(response);
|
||||
|
||||
request.Usage = AIServiceUsageHelper.GetOpenAIServiceUsage(response);
|
||||
return response.Content;
|
||||
}
|
||||
|
||||
private Kernel CreateKernel()
|
||||
{
|
||||
var kernelBuilder = Kernel.CreateBuilder();
|
||||
var endpoint = string.IsNullOrWhiteSpace(_config.Endpoint) ? null : _config.Endpoint.Trim();
|
||||
var apiKey = _config.ApiKey?.Trim() ?? string.Empty;
|
||||
|
||||
if (RequiresApiKey(_serviceType) && string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
throw new InvalidOperationException($"API key is required for {_serviceType} but was not provided.");
|
||||
}
|
||||
|
||||
switch (_serviceType)
|
||||
{
|
||||
case AIServiceType.OpenAI:
|
||||
kernelBuilder.AddOpenAIChatCompletion(_config.Model, apiKey, serviceId: _config.Model);
|
||||
break;
|
||||
case AIServiceType.AzureOpenAI:
|
||||
var deploymentName = string.IsNullOrWhiteSpace(_config.DeploymentName) ? _config.Model : _config.DeploymentName;
|
||||
kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey, serviceId: _config.Model);
|
||||
break;
|
||||
case AIServiceType.Mistral:
|
||||
kernelBuilder.AddMistralChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.Google:
|
||||
kernelBuilder.AddGoogleAIGeminiChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.HuggingFace:
|
||||
kernelBuilder.AddHuggingFaceChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.AzureAIInference:
|
||||
kernelBuilder.AddAzureAIInferenceChatCompletion(_config.Model, apiKey: apiKey);
|
||||
break;
|
||||
case AIServiceType.Ollama:
|
||||
kernelBuilder.AddOllamaChatCompletion(_config.Model, endpoint: new Uri(endpoint));
|
||||
break;
|
||||
case AIServiceType.Anthropic:
|
||||
kernelBuilder.AddBedrockChatCompletionService(_config.Model);
|
||||
break;
|
||||
case AIServiceType.AmazonBedrock:
|
||||
kernelBuilder.AddBedrockChatCompletionService(_config.Model);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Provider '{_config.ProviderType}' is not supported by {nameof(SemanticKernelPasteProvider)}");
|
||||
}
|
||||
|
||||
return kernelBuilder.Build();
|
||||
}
|
||||
|
||||
private PromptExecutionSettings CreateExecutionSettings()
|
||||
{
|
||||
return _serviceType switch
|
||||
{
|
||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
|
||||
{
|
||||
Temperature = 0.01,
|
||||
MaxTokens = 2000,
|
||||
FunctionChoiceBehavior = null,
|
||||
},
|
||||
_ => new PromptExecutionSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
private static bool RequiresApiKey(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
private static string RequireEndpoint(string endpoint, AIServiceType serviceType)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Endpoint is required for {serviceType} but was not provided.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// 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.Linq;
|
||||
using AdvancedPaste.Settings;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Windows.Security.Credentials;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Enhanced credentials provider that supports different AI service types
|
||||
/// Keys are stored in Windows Credential Vault with service-specific identifiers
|
||||
/// </summary>
|
||||
public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
|
||||
{
|
||||
private sealed class CredentialSlot
|
||||
{
|
||||
public AIServiceType ServiceType { get; set; } = AIServiceType.Unknown;
|
||||
|
||||
public string ProviderId { get; set; } = string.Empty;
|
||||
|
||||
public (string Resource, string Username)? Entry { get; set; }
|
||||
|
||||
public string Key { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly Dictionary<AICredentialScope, CredentialSlot> _slots;
|
||||
private readonly object _syncRoot = new();
|
||||
|
||||
public EnhancedVaultCredentialsProvider(IUserSettings userSettings)
|
||||
{
|
||||
_userSettings = userSettings ?? throw new ArgumentNullException(nameof(userSettings));
|
||||
|
||||
_slots = new Dictionary<AICredentialScope, CredentialSlot>
|
||||
{
|
||||
[AICredentialScope.PasteAI] = new CredentialSlot(),
|
||||
[AICredentialScope.AdvancedAI] = new CredentialSlot(),
|
||||
};
|
||||
|
||||
Refresh(AICredentialScope.PasteAI);
|
||||
Refresh(AICredentialScope.AdvancedAI);
|
||||
}
|
||||
|
||||
public string GetKey(AICredentialScope scope)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
UpdateSlot(scope, forceRefresh: false);
|
||||
return _slots[scope].Key;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsConfigured(AICredentialScope scope)
|
||||
{
|
||||
return !string.IsNullOrEmpty(GetKey(scope));
|
||||
}
|
||||
|
||||
public bool Refresh(AICredentialScope scope)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return UpdateSlot(scope, forceRefresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UpdateSlot(AICredentialScope scope, bool forceRefresh)
|
||||
{
|
||||
var slot = _slots[scope];
|
||||
var (serviceType, providerId) = ResolveCredentialTarget(scope);
|
||||
var desiredServiceType = NormalizeServiceType(serviceType);
|
||||
providerId ??= string.Empty;
|
||||
|
||||
var hasChanged = false;
|
||||
|
||||
if (slot.ServiceType != desiredServiceType || !string.Equals(slot.ProviderId, providerId, StringComparison.Ordinal))
|
||||
{
|
||||
slot.ServiceType = desiredServiceType;
|
||||
slot.ProviderId = providerId;
|
||||
slot.Entry = BuildCredentialEntry(desiredServiceType, providerId, scope);
|
||||
forceRefresh = true;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
if (!forceRefresh)
|
||||
{
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
var newKey = LoadKey(slot.Entry);
|
||||
if (!string.Equals(slot.Key, newKey, StringComparison.Ordinal))
|
||||
{
|
||||
slot.Key = newKey;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
private (AIServiceType ServiceType, string ProviderId) ResolveCredentialTarget(AICredentialScope scope)
|
||||
{
|
||||
return scope switch
|
||||
{
|
||||
AICredentialScope.AdvancedAI => (ResolveAdvancedAiServiceType(), string.Empty),
|
||||
AICredentialScope.PasteAI => ResolvePasteAiServiceTarget(),
|
||||
_ => (AIServiceType.OpenAI, string.Empty),
|
||||
};
|
||||
}
|
||||
|
||||
private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
|
||||
}
|
||||
|
||||
private AIServiceType ResolveAdvancedAiServiceType()
|
||||
{
|
||||
var configuration = _userSettings.PasteAIConfiguration;
|
||||
if (configuration is null)
|
||||
{
|
||||
return AIServiceType.OpenAI;
|
||||
}
|
||||
|
||||
var activeProvider = configuration.ActiveProvider;
|
||||
if (IsAdvancedProvider(activeProvider))
|
||||
{
|
||||
return NormalizeServiceType(activeProvider.ServiceTypeKind);
|
||||
}
|
||||
|
||||
var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedProvider);
|
||||
if (fallback is not null)
|
||||
{
|
||||
return NormalizeServiceType(fallback.ServiceTypeKind);
|
||||
}
|
||||
|
||||
return AIServiceType.OpenAI;
|
||||
}
|
||||
|
||||
private (AIServiceType ServiceType, string ProviderId) ResolvePasteAiServiceTarget()
|
||||
{
|
||||
var provider = _userSettings.PasteAIConfiguration?.ActiveProvider;
|
||||
if (provider is null)
|
||||
{
|
||||
return (AIServiceType.OpenAI, string.Empty);
|
||||
}
|
||||
|
||||
return (provider.ServiceTypeKind, provider.Id ?? string.Empty);
|
||||
}
|
||||
|
||||
private static bool IsAdvancedProvider(PasteAIProviderDefinition provider)
|
||||
{
|
||||
if (provider is null || !provider.EnableAdvancedAI)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SupportsAdvancedAI(provider.ServiceTypeKind);
|
||||
}
|
||||
|
||||
private static bool SupportsAdvancedAI(AIServiceType serviceType)
|
||||
{
|
||||
return NormalizeServiceType(serviceType) is AIServiceType.OpenAI or AIServiceType.AzureOpenAI;
|
||||
}
|
||||
|
||||
private static string LoadKey((string Resource, string Username)? entry)
|
||||
{
|
||||
if (entry is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var credential = new PasswordVault().Retrieve(entry.Value.Resource, entry.Value.Username);
|
||||
return credential?.Password ?? string.Empty;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static (string Resource, string Username)? BuildCredentialEntry(AIServiceType serviceType, string providerId, AICredentialScope scope)
|
||||
{
|
||||
return scope switch
|
||||
{
|
||||
AICredentialScope.AdvancedAI => GetAdvancedAiEntry(serviceType),
|
||||
AICredentialScope.PasteAI => GetPasteAiEntry(serviceType, providerId),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static (string Resource, string Username)? GetAdvancedAiEntry(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.OpenAI => ("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_AdvancedAI_OpenAI"),
|
||||
AIServiceType.AzureOpenAI => ("https://azure.microsoft.com/products/ai-services/openai-service", "PowerToys_AdvancedPaste_AdvancedAI_AzureOpenAI"),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static (string Resource, string Username)? GetPasteAiEntry(AIServiceType serviceType, string providerId)
|
||||
{
|
||||
string resource;
|
||||
string serviceKey;
|
||||
|
||||
switch (serviceType)
|
||||
{
|
||||
case AIServiceType.OpenAI:
|
||||
resource = "https://platform.openai.com/api-keys";
|
||||
serviceKey = "openai";
|
||||
break;
|
||||
case AIServiceType.AzureOpenAI:
|
||||
resource = "https://azure.microsoft.com/products/ai-services/openai-service";
|
||||
serviceKey = "azureopenai";
|
||||
break;
|
||||
case AIServiceType.AzureAIInference:
|
||||
resource = "https://azure.microsoft.com/products/ai-services/ai-inference";
|
||||
serviceKey = "azureaiinference";
|
||||
break;
|
||||
case AIServiceType.Mistral:
|
||||
resource = "https://console.mistral.ai/account/api-keys";
|
||||
serviceKey = "mistral";
|
||||
break;
|
||||
case AIServiceType.Google:
|
||||
resource = "https://ai.google.dev/";
|
||||
serviceKey = "google";
|
||||
break;
|
||||
case AIServiceType.HuggingFace:
|
||||
resource = "https://huggingface.co/settings/tokens";
|
||||
serviceKey = "huggingface";
|
||||
break;
|
||||
case AIServiceType.FoundryLocal:
|
||||
case AIServiceType.ML:
|
||||
case AIServiceType.Onnx:
|
||||
case AIServiceType.Ollama:
|
||||
case AIServiceType.Anthropic:
|
||||
case AIServiceType.AmazonBedrock:
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
string username = $"PowerToys_AdvancedPaste_PasteAI_{serviceKey}_{NormalizeProviderIdentifier(providerId)}";
|
||||
return (resource, username);
|
||||
}
|
||||
|
||||
private static string NormalizeProviderIdentifier(string providerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
{
|
||||
return "default";
|
||||
}
|
||||
|
||||
var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray());
|
||||
return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,38 @@
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the scope a credential lookup is targeting.
|
||||
/// </summary>
|
||||
public enum AICredentialScope
|
||||
{
|
||||
PasteAI,
|
||||
AdvancedAI,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to AI credentials stored for Advanced Paste scenarios.
|
||||
/// </summary>
|
||||
public interface IAICredentialsProvider
|
||||
{
|
||||
bool IsConfigured { get; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the specified scope has a configured credential.
|
||||
/// </summary>
|
||||
/// <param name="scope">Scope to evaluate.</param>
|
||||
/// <returns><see langword="true"/> when a non-empty credential exists for the scope.</returns>
|
||||
bool IsConfigured(AICredentialScope scope);
|
||||
|
||||
string Key { get; }
|
||||
/// <summary>
|
||||
/// Retrieves the credential for the requested scope.
|
||||
/// </summary>
|
||||
/// <param name="scope">Scope to evaluate.</param>
|
||||
/// <returns>Credential string or <see cref="string.Empty"/> when missing.</returns>
|
||||
string GetKey(AICredentialScope scope);
|
||||
|
||||
bool Refresh();
|
||||
/// <summary>
|
||||
/// Refreshes the cached credential for the provided scope.
|
||||
/// </summary>
|
||||
/// <param name="scope">Scope to refresh.</param>
|
||||
/// <returns><see langword="true"/> when the credential changed.</returns>
|
||||
bool Refresh(AICredentialScope scope);
|
||||
}
|
||||
|
||||
@@ -1,14 +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 System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public interface ICustomTextTransformService
|
||||
{
|
||||
Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
}
|
||||
@@ -5,15 +5,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Models.KernelQueryCache;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.Telemetry;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
@@ -21,15 +22,20 @@ using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheService, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) : IKernelService
|
||||
public abstract class KernelServiceBase(
|
||||
IKernelQueryCacheService queryCacheService,
|
||||
IPromptModerationService promptModerationService,
|
||||
IUserSettings userSettings,
|
||||
ICustomActionTransformService customActionTransformService) : IKernelService
|
||||
{
|
||||
private const string PromptParameterName = "prompt";
|
||||
|
||||
private readonly IKernelQueryCacheService _queryCacheService = queryCacheService;
|
||||
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
||||
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
|
||||
private readonly IUserSettings _userSettings = userSettings;
|
||||
private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
|
||||
|
||||
protected abstract string ModelName { get; }
|
||||
protected abstract string AdvancedAIModelName { get; }
|
||||
|
||||
protected abstract PromptExecutionSettings PromptExecutionSettings { get; }
|
||||
|
||||
@@ -144,9 +150,12 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
|
||||
if (ShouldModerateAdvancedAI())
|
||||
{
|
||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
|
||||
}
|
||||
|
||||
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
|
||||
var chatResult = await kernel.GetRequiredService<IChatCompletionService>(AdvancedAIModelName)
|
||||
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken);
|
||||
chatHistory.Add(chatResult);
|
||||
|
||||
@@ -175,9 +184,11 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
return ([], AIServiceUsage.None);
|
||||
}
|
||||
|
||||
protected IUserSettings UserSettings => _userSettings;
|
||||
|
||||
private void LogResult(bool cacheUsed, bool isSavedQuery, IEnumerable<ActionChainItem> actionChain, AIServiceUsage usage)
|
||||
{
|
||||
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, ModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
|
||||
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, AdvancedAIModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
var logEvent = new AIServiceFormatEvent(telemetryEvent);
|
||||
Logger.LogDebug($"{nameof(TransformClipboardAsync)} complete; {logEvent.ToJsonString()}");
|
||||
@@ -191,20 +202,96 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
return kernelBuilder.Build();
|
||||
}
|
||||
|
||||
private IEnumerable<KernelFunction> GetKernelFunctions() =>
|
||||
from format in Enum.GetValues<PasteFormats>()
|
||||
let metadata = PasteFormat.MetadataDict[format]
|
||||
let coreDescription = metadata.KernelFunctionDescription
|
||||
where !string.IsNullOrEmpty(coreDescription)
|
||||
let requiresPrompt = metadata.RequiresPrompt
|
||||
orderby requiresPrompt descending
|
||||
select KernelFunctionFactory.CreateFromMethod(
|
||||
method: requiresPrompt ? async (Kernel kernel, string prompt) => await ExecutePromptTransformAsync(kernel, format, prompt)
|
||||
: async (Kernel kernel) => await ExecuteStandardTransformAsync(kernel, format),
|
||||
functionName: format.ToString(),
|
||||
description: requiresPrompt ? coreDescription : $"{coreDescription} Puts the result back on the clipboard.",
|
||||
parameters: requiresPrompt ? [new(PromptParameterName) { Description = "Input instructions to AI", ParameterType = typeof(string) }] : null,
|
||||
returnParameter: new() { Description = "Array of available clipboard formats after operation" });
|
||||
private IEnumerable<KernelFunction> GetKernelFunctions()
|
||||
{
|
||||
// Get standard format functions
|
||||
var standardFunctions =
|
||||
from format in Enum.GetValues<PasteFormats>()
|
||||
let metadata = PasteFormat.MetadataDict[format]
|
||||
let coreDescription = metadata.KernelFunctionDescription
|
||||
where !string.IsNullOrEmpty(coreDescription)
|
||||
let requiresPrompt = metadata.RequiresPrompt
|
||||
orderby requiresPrompt descending
|
||||
select KernelFunctionFactory.CreateFromMethod(
|
||||
method: requiresPrompt ? async (Kernel kernel, string prompt) => await ExecutePromptTransformAsync(kernel, format, prompt)
|
||||
: async (Kernel kernel) => await ExecuteStandardTransformAsync(kernel, format),
|
||||
functionName: format.ToString(),
|
||||
description: requiresPrompt ? coreDescription : $"{coreDescription} Puts the result back on the clipboard.",
|
||||
parameters: requiresPrompt ? [new(PromptParameterName) { Description = "Input instructions to AI", ParameterType = typeof(string) }] : null,
|
||||
returnParameter: new() { Description = "Array of available clipboard formats after operation" });
|
||||
|
||||
HashSet<string> usedFunctionNames = new(Enum.GetNames<PasteFormats>(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Get custom action functions
|
||||
var customActionFunctions = _userSettings.CustomActions
|
||||
.Where(customAction => !string.IsNullOrWhiteSpace(customAction.Name) && !string.IsNullOrWhiteSpace(customAction.Prompt))
|
||||
.Select(customAction =>
|
||||
{
|
||||
var sanitizedBaseName = SanitizeFunctionName(customAction.Name);
|
||||
var functionName = GetUniqueFunctionName(sanitizedBaseName, usedFunctionNames, customAction.Id);
|
||||
var description = string.IsNullOrWhiteSpace(customAction.Description)
|
||||
? $"Runs the \"{customAction.Name}\" custom action."
|
||||
: customAction.Description;
|
||||
return KernelFunctionFactory.CreateFromMethod(
|
||||
method: async (Kernel kernel) => await ExecuteCustomActionAsync(kernel, customAction.Prompt),
|
||||
functionName: functionName,
|
||||
description: description,
|
||||
parameters: null,
|
||||
returnParameter: new() { Description = "Array of available clipboard formats after operation" });
|
||||
});
|
||||
|
||||
return standardFunctions.Concat(customActionFunctions);
|
||||
}
|
||||
|
||||
private static string GetUniqueFunctionName(string baseName, HashSet<string> usedFunctionNames, int customActionId)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(usedFunctionNames);
|
||||
|
||||
var candidate = string.IsNullOrEmpty(baseName) ? "_CustomAction" : baseName;
|
||||
|
||||
if (usedFunctionNames.Add(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
int suffix = 1;
|
||||
while (true)
|
||||
{
|
||||
var nextCandidate = $"{candidate}_{customActionId}_{suffix}";
|
||||
if (usedFunctionNames.Add(nextCandidate))
|
||||
{
|
||||
return nextCandidate;
|
||||
}
|
||||
|
||||
suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeFunctionName(string name)
|
||||
{
|
||||
// Remove invalid characters and ensure the function name is valid for kernel
|
||||
var sanitized = new string(name.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray());
|
||||
|
||||
// Ensure it starts with a letter or underscore
|
||||
if (sanitized.Length > 0 && !char.IsLetter(sanitized[0]) && sanitized[0] != '_')
|
||||
{
|
||||
sanitized = "_" + sanitized;
|
||||
}
|
||||
|
||||
// Ensure it's not empty
|
||||
return string.IsNullOrEmpty(sanitized) ? "_CustomAction" : sanitized;
|
||||
}
|
||||
|
||||
private Task<string> ExecuteCustomActionAsync(Kernel kernel, string fixedPrompt) =>
|
||||
ExecuteTransformAsync(
|
||||
kernel,
|
||||
new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty);
|
||||
});
|
||||
|
||||
private Task<string> ExecutePromptTransformAsync(Kernel kernel, PasteFormats format, string prompt) =>
|
||||
ExecuteTransformAsync(
|
||||
@@ -212,7 +299,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }),
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetTextAsync();
|
||||
var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
return DataPackageHelpers.CreateFromText(output);
|
||||
});
|
||||
@@ -220,7 +307,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
format switch
|
||||
{
|
||||
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty,
|
||||
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
||||
};
|
||||
|
||||
@@ -281,4 +368,9 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
var usageString = usage.HasUsage ? $" [{usage}]" : string.Empty;
|
||||
return $"-> {role}: {redactedContent}{usageString}";
|
||||
}
|
||||
|
||||
protected virtual bool ShouldModerateAdvancedAI()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +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 System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Telemetry;
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
|
||||
namespace AdvancedPaste.Services.OpenAI;
|
||||
|
||||
public sealed class CustomTextTransformService(IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService) : ICustomTextTransformService
|
||||
{
|
||||
private const string ModelName = "gpt-3.5-turbo-instruct";
|
||||
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
||||
|
||||
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
var fullPrompt = systemInstructions + "\n\n" + userMessage;
|
||||
|
||||
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
|
||||
|
||||
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
|
||||
|
||||
var response = await azureAIClient.GetCompletionsAsync(
|
||||
new()
|
||||
{
|
||||
DeploymentName = ModelName,
|
||||
Prompts =
|
||||
{
|
||||
fullPrompt,
|
||||
},
|
||||
Temperature = 0.01F,
|
||||
MaxTokens = 2000,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
if (response.Value.Choices[0].FinishReason == "length")
|
||||
{
|
||||
Logger.LogDebug("Cut off due to length constraints");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputText))
|
||||
{
|
||||
Logger.LogWarning("Clipboard has no usable text data");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string systemInstructions =
|
||||
$@"You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it.
|
||||
Do not output anything else besides the reformatted clipboard content.";
|
||||
|
||||
string userMessage =
|
||||
$@"User instructions:
|
||||
{prompt}
|
||||
|
||||
Clipboard Content:
|
||||
{inputText}
|
||||
|
||||
Output:
|
||||
";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
|
||||
|
||||
var usage = response.Usage;
|
||||
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
var logEvent = new AIServiceFormatEvent(telemetryEvent);
|
||||
|
||||
Logger.LogDebug($"{nameof(TransformTextAsync)} complete; {logEvent.ToJsonString()}");
|
||||
|
||||
return response.Choices[0].Text;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"{nameof(TransformTextAsync)} failed", ex);
|
||||
|
||||
AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PasteActionException(ErrorHelpers.TranslateErrorText((ex as RequestFailedException)?.Status ?? -1), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +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 System.Collections.Generic;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using Azure.AI.OpenAI;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace AdvancedPaste.Services.OpenAI;
|
||||
|
||||
public sealed class KernelService(IKernelQueryCacheService queryCacheService, IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) :
|
||||
KernelServiceBase(queryCacheService, promptModerationService, customTextTransformService)
|
||||
{
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||
|
||||
protected override string ModelName => "gpt-4o";
|
||||
|
||||
protected override PromptExecutionSettings PromptExecutionSettings =>
|
||||
new OpenAIPromptExecutionSettings()
|
||||
{
|
||||
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
|
||||
Temperature = 0.01,
|
||||
};
|
||||
|
||||
protected override void AddChatCompletionService(IKernelBuilder kernelBuilder) => kernelBuilder.AddOpenAIChatCompletion(ModelName, _aiCredentialsProvider.Key);
|
||||
|
||||
protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage) =>
|
||||
chatMessage.Metadata?.GetValueOrDefault("Usage") is CompletionsUsage completionsUsage
|
||||
? new(PromptTokens: completionsUsage.PromptTokens, CompletionTokens: completionsUsage.CompletionTokens)
|
||||
: AIServiceUsage.None;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using ManagedCommon;
|
||||
using OpenAI.Moderations;
|
||||
|
||||
@@ -23,7 +24,24 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials
|
||||
{
|
||||
try
|
||||
{
|
||||
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
|
||||
_aiCredentialsProvider.Refresh(AICredentialScope.AdvancedAI);
|
||||
var apiKey = _aiCredentialsProvider.GetKey(AICredentialScope.AdvancedAI);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
_aiCredentialsProvider.Refresh(AICredentialScope.PasteAI);
|
||||
apiKey = _aiCredentialsProvider.GetKey(AICredentialScope.PasteAI);
|
||||
}
|
||||
|
||||
apiKey = apiKey?.Trim() ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
Logger.LogWarning("Skipping OpenAI moderation because no credential is configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
ModerationClient moderationClient = new(ModelName, apiKey);
|
||||
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
|
||||
var moderationResult = moderationClientResult.Value;
|
||||
|
||||
|
||||
@@ -1,37 +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 System;
|
||||
|
||||
using Windows.Security.Credentials;
|
||||
|
||||
namespace AdvancedPaste.Services.OpenAI;
|
||||
|
||||
public sealed class VaultCredentialsProvider : IAICredentialsProvider
|
||||
{
|
||||
public VaultCredentialsProvider() => Refresh();
|
||||
|
||||
public string Key { get; private set; }
|
||||
|
||||
public bool IsConfigured => !string.IsNullOrEmpty(Key);
|
||||
|
||||
public bool Refresh()
|
||||
{
|
||||
var oldKey = Key;
|
||||
Key = LoadKey();
|
||||
return oldKey != Key;
|
||||
}
|
||||
|
||||
private static string LoadKey()
|
||||
{
|
||||
try
|
||||
{
|
||||
return new PasswordVault().Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey")?.Password ?? string.Empty;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,16 @@ using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTextTransformService customTextTransformService) : IPasteFormatExecutor
|
||||
public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService) : IPasteFormatExecutor
|
||||
{
|
||||
private readonly IKernelService _kernelService = kernelService;
|
||||
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
|
||||
private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
|
||||
|
||||
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
@@ -36,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
|
||||
pasteFormat.Format switch
|
||||
{
|
||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,16 +144,67 @@
|
||||
<data name="PasteActionModerated" xml:space="preserve">
|
||||
<value>The paste operation was moderated due to sensitive content. Please try another query.</value>
|
||||
</data>
|
||||
<data name="ClipboardHistoryButton.Text" xml:space="preserve">
|
||||
<data name="ClipboardHistoryButtonToolTip.Text" xml:space="preserve">
|
||||
<value>Clipboard history</value>
|
||||
</data>
|
||||
<data name="ClipboardHistoryButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Clipboard history</value>
|
||||
</data>
|
||||
<data name="AIProviderButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>AI provider selector</value>
|
||||
</data>
|
||||
<data name="AIProviderButtonTooltipEmpty" xml:space="preserve">
|
||||
<value>Select an AI provider</value>
|
||||
</data>
|
||||
<data name="AIProviderButtonTooltipFormat" xml:space="preserve">
|
||||
<value>Active provider: {0}</value>
|
||||
</data>
|
||||
<data name="AIProvidersFlyoutHeader.Text" xml:space="preserve">
|
||||
<value>AI providers</value>
|
||||
</data>
|
||||
<data name="AIProvidersEmptyText.Text" xml:space="preserve">
|
||||
<value>No AI providers configured</value>
|
||||
</data>
|
||||
<data name="AIProvidersManageButtonContent.Content" xml:space="preserve">
|
||||
<value>Configure models in Settings</value>
|
||||
</data>
|
||||
<data name="ClipboardHistoryImage" xml:space="preserve">
|
||||
<value>Image data</value>
|
||||
<comment>Label used to represent an image in the clipboard history</comment>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCategoryText" xml:space="preserve">
|
||||
<value>Text</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCategoryImage" xml:space="preserve">
|
||||
<value>Image</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCategoryAudio" xml:space="preserve">
|
||||
<value>Audio</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCategoryVideo" xml:space="preserve">
|
||||
<value>Video</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCategoryFile" xml:space="preserve">
|
||||
<value>File</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCategoryUnknown" xml:space="preserve">
|
||||
<value>Clipboard</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCopiedJustNow" xml:space="preserve">
|
||||
<value>Copied just now</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCopiedSeconds" xml:space="preserve">
|
||||
<value>Copied {0} sec ago</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCopiedMinutes" xml:space="preserve">
|
||||
<value>Copied {0} min ago</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCopiedHours" xml:space="preserve">
|
||||
<value>Copied {0} hr ago</value>
|
||||
</data>
|
||||
<data name="ClipboardPreviewCopiedDays" xml:space="preserve">
|
||||
<value>Copied {0} day ago</value>
|
||||
</data>
|
||||
<data name="ClipboardHistoryItemMoreOptionsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>More options</value>
|
||||
</data>
|
||||
@@ -196,7 +247,7 @@
|
||||
<data name="TranscodeToMp3" xml:space="preserve">
|
||||
<value>Transcode to .mp3</value>
|
||||
<comment>Option to transcode audio files to MP3 format</comment>
|
||||
</data>
|
||||
</data>
|
||||
<data name="TranscodeToMp4" xml:space="preserve">
|
||||
<value>Transcode to .mp4 (H.264/AAC)</value>
|
||||
<comment>Option to transcode video files to MP4 format with H.264 video codec and AAC audio codec</comment>
|
||||
@@ -287,4 +338,27 @@
|
||||
<data name="PasteAsFile_FilePrefix" xml:space="preserve">
|
||||
<value>PowerToys_Paste_</value>
|
||||
</data>
|
||||
<data name="Relative_JustNow" xml:space="preserve">
|
||||
<value>Just now</value>
|
||||
</data>
|
||||
<data name="Relative_MinuteAgo" xml:space="preserve">
|
||||
<value>1 minute ago</value>
|
||||
</data>
|
||||
<data name="Relative_MinutesAgo_Format" xml:space="preserve">
|
||||
<value>{0} minutes ago</value>
|
||||
</data>
|
||||
<data name="Relative_Today_TimeFormat" xml:space="preserve">
|
||||
<value>Today, {0}</value>
|
||||
</data>
|
||||
<data name="Relative_Yesterday_TimeFormat" xml:space="preserve">
|
||||
<value>Yesterday, {0}</value>
|
||||
</data>
|
||||
<data name="Relative_Weekday_TimeFormat" xml:space="preserve">
|
||||
<value>{0}, {1}</value>
|
||||
<comment>(e.g., “Wednesday, 17:05”)</comment>
|
||||
</data>
|
||||
<data name="Relative_Date_TimeFormat" xml:space="preserve">
|
||||
<value>{0}, {1}</value>
|
||||
<comment>(e.g., “10/20/2025, 17:05” in the user’s locale)</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -22,6 +23,8 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Microsoft.Win32;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.System;
|
||||
@@ -37,12 +40,20 @@ namespace AdvancedPaste.ViewModels
|
||||
private readonly DispatcherTimer _clipboardTimer;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider;
|
||||
private readonly IAICredentialsProvider _credentialsProvider;
|
||||
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
private string _currentClipboardHistoryId;
|
||||
private DateTimeOffset? _currentClipboardTimestamp;
|
||||
private ClipboardFormat _lastClipboardFormats = ClipboardFormat.None;
|
||||
private bool _clipboardHistoryUnavailableLogged;
|
||||
|
||||
public DataPackageView ClipboardData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
private ClipboardItem _currentClipboardItem;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))]
|
||||
[NotifyPropertyChangedFor(nameof(ClipboardHasData))]
|
||||
@@ -79,11 +90,69 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public ObservableCollection<PasteFormat> CustomActionPasteFormats { get; } = [];
|
||||
|
||||
public bool IsCustomAIServiceEnabled => IsAllowedByGPO && _aiCredentialsProvider.IsConfigured;
|
||||
public bool IsCustomAIServiceEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsAllowedByGPO || !_userSettings.IsAIEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We should handle the IsAIEnabled logic in settings, don't check again here.
|
||||
// If setting says yes, and here should pass check, and if error happens, it happens.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCustomAIAvailable => IsCustomAIServiceEnabled && ClipboardHasDataForCustomAI;
|
||||
|
||||
public bool IsAdvancedAIEnabled => IsCustomAIServiceEnabled && _userSettings.IsAdvancedAIEnabled;
|
||||
public bool IsAdvancedAIEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsAllowedByGPO || !_userSettings.IsAIEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveAdvancedAIProvider(out var provider, out var usesPasteScope))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var scope = usesPasteScope ? AICredentialScope.PasteAI : AICredentialScope.AdvancedAI;
|
||||
return _credentialsProvider.IsConfigured(scope);
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<PasteAIProviderDefinition> AIProviders => _userSettings?.PasteAIConfiguration?.Providers ?? new ObservableCollection<PasteAIProviderDefinition>();
|
||||
|
||||
public PasteAIProviderDefinition ActiveAIProvider => _userSettings?.PasteAIConfiguration?.ActiveProvider;
|
||||
|
||||
public string ActiveAIProviderTooltip
|
||||
{
|
||||
get
|
||||
{
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
var provider = ActiveAIProvider;
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
return resourceLoader.GetString("AIProviderButtonTooltipEmpty");
|
||||
}
|
||||
|
||||
var format = resourceLoader.GetString("AIProviderButtonTooltipFormat");
|
||||
var displayName = provider.DisplayName;
|
||||
|
||||
if (!string.IsNullOrEmpty(format))
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, format, displayName);
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
|
||||
|
||||
@@ -91,7 +160,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
|
||||
|
||||
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
|
||||
private PasteFormats CustomAIFormat => _userSettings.IsAIEnabled && TryResolveAdvancedAIProvider(out _, out _) ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
|
||||
|
||||
private bool Visible
|
||||
{
|
||||
@@ -110,9 +179,9 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public event EventHandler PreviewRequested;
|
||||
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider aiCredentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
{
|
||||
_aiCredentialsProvider = aiCredentialsProvider;
|
||||
_credentialsProvider = credentialsProvider;
|
||||
_userSettings = userSettings;
|
||||
_pasteFormatExecutor = pasteFormatExecutor;
|
||||
|
||||
@@ -130,6 +199,7 @@ namespace AdvancedPaste.ViewModels
|
||||
_clipboardTimer.Start();
|
||||
|
||||
RefreshPasteFormats();
|
||||
UpdateAIProviderActiveFlags();
|
||||
_userSettings.Changed += UserSettings_Changed;
|
||||
PropertyChanged += (_, e) =>
|
||||
{
|
||||
@@ -164,9 +234,14 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
private void UserSettings_Changed(object sender, EventArgs e)
|
||||
{
|
||||
UpdateAIProviderActiveFlags();
|
||||
OnPropertyChanged(nameof(IsCustomAIServiceEnabled));
|
||||
OnPropertyChanged(nameof(ClipboardHasDataForCustomAI));
|
||||
OnPropertyChanged(nameof(IsCustomAIAvailable));
|
||||
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
|
||||
OnPropertyChanged(nameof(AIProviders));
|
||||
OnPropertyChanged(nameof(ActiveAIProvider));
|
||||
OnPropertyChanged(nameof(ActiveAIProviderTooltip));
|
||||
|
||||
EnqueueRefreshPasteFormats();
|
||||
}
|
||||
@@ -192,6 +267,23 @@ namespace AdvancedPaste.ViewModels
|
||||
private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery) =>
|
||||
PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled);
|
||||
|
||||
private void UpdateAIProviderActiveFlags()
|
||||
{
|
||||
var providers = _userSettings?.PasteAIConfiguration?.Providers;
|
||||
if (providers is not null)
|
||||
{
|
||||
var activeId = ActiveAIProvider?.Id;
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
provider.IsActive = !string.IsNullOrEmpty(activeId) && string.Equals(provider.Id, activeId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ActiveAIProvider));
|
||||
OnPropertyChanged(nameof(ActiveAIProviderTooltip));
|
||||
}
|
||||
|
||||
private void RefreshPasteFormats()
|
||||
{
|
||||
var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey");
|
||||
@@ -253,8 +345,96 @@ namespace AdvancedPaste.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
ClipboardData = Clipboard.GetContent();
|
||||
AvailableClipboardFormats = await ClipboardData.GetAvailableFormatsAsync();
|
||||
try
|
||||
{
|
||||
ClipboardData = Clipboard.GetContent();
|
||||
AvailableClipboardFormats = ClipboardData != null ? await ClipboardData.GetAvailableFormatsAsync() : ClipboardFormat.None;
|
||||
}
|
||||
catch (Exception ex) when (ex is COMException or InvalidOperationException)
|
||||
{
|
||||
// Logger.LogDebug("Failed to read clipboard content", ex);
|
||||
ClipboardData = null;
|
||||
AvailableClipboardFormats = ClipboardFormat.None;
|
||||
}
|
||||
|
||||
await UpdateClipboardPreviewAsync();
|
||||
}
|
||||
|
||||
private async Task UpdateClipboardPreviewAsync()
|
||||
{
|
||||
if (ClipboardData is null || !ClipboardHasData)
|
||||
{
|
||||
ResetClipboardPreview();
|
||||
_currentClipboardHistoryId = null;
|
||||
_currentClipboardTimestamp = null;
|
||||
_lastClipboardFormats = ClipboardFormat.None;
|
||||
return;
|
||||
}
|
||||
|
||||
var formatsChanged = AvailableClipboardFormats != _lastClipboardFormats;
|
||||
_lastClipboardFormats = AvailableClipboardFormats;
|
||||
|
||||
var clipboardChanged = await UpdateClipboardTimestampAsync(formatsChanged);
|
||||
|
||||
// Create ClipboardItem directly from current clipboard data using helper
|
||||
CurrentClipboardItem = await ClipboardItemHelper.CreateFromCurrentClipboardAsync(
|
||||
ClipboardData,
|
||||
AvailableClipboardFormats,
|
||||
_currentClipboardTimestamp,
|
||||
clipboardChanged ? null : CurrentClipboardItem?.Image);
|
||||
}
|
||||
|
||||
private async Task<bool> UpdateClipboardTimestampAsync(bool formatsChanged)
|
||||
{
|
||||
bool clipboardChanged = formatsChanged;
|
||||
|
||||
if (Clipboard.IsHistoryEnabled())
|
||||
{
|
||||
try
|
||||
{
|
||||
var historyItems = await Clipboard.GetHistoryItemsAsync();
|
||||
if (historyItems.Status == ClipboardHistoryItemsResultStatus.Success && historyItems.Items.Count > 0)
|
||||
{
|
||||
var latest = historyItems.Items[0];
|
||||
if (_currentClipboardHistoryId != latest.Id)
|
||||
{
|
||||
clipboardChanged = true;
|
||||
_currentClipboardHistoryId = latest.Id;
|
||||
}
|
||||
|
||||
_currentClipboardTimestamp = latest.Timestamp;
|
||||
_clipboardHistoryUnavailableLogged = false;
|
||||
return clipboardChanged;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_clipboardHistoryUnavailableLogged)
|
||||
{
|
||||
Logger.LogDebug("Failed to access clipboard history timestamp", ex.Message);
|
||||
_clipboardHistoryUnavailableLogged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_currentClipboardTimestamp.HasValue || clipboardChanged)
|
||||
{
|
||||
_currentClipboardTimestamp = DateTimeOffset.Now;
|
||||
clipboardChanged = true;
|
||||
}
|
||||
|
||||
return clipboardChanged;
|
||||
}
|
||||
|
||||
private void ResetClipboardPreview()
|
||||
{
|
||||
// Clear to avoid leaks due to Garbage Collection not clearing the bitmap from memory
|
||||
if (CurrentClipboardItem?.Image is not null)
|
||||
{
|
||||
CurrentClipboardItem.Image.ClearValue(BitmapImage.UriSourceProperty);
|
||||
}
|
||||
|
||||
CurrentClipboardItem = null;
|
||||
}
|
||||
|
||||
public async Task OnShowAsync()
|
||||
@@ -270,7 +450,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
GetMainWindow()?.FinishLoading(_aiCredentialsProvider.IsConfigured);
|
||||
GetMainWindow()?.FinishLoading(IsCustomAIServiceEnabled);
|
||||
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
|
||||
OnPropertyChanged(nameof(CustomAIUnavailableErrorText));
|
||||
OnPropertyChanged(nameof(IsCustomAIServiceEnabled));
|
||||
@@ -319,7 +499,7 @@ namespace AdvancedPaste.ViewModels
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
|
||||
}
|
||||
|
||||
if (!_aiCredentialsProvider.IsConfigured)
|
||||
if (!IsCustomAIServiceEnabled)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
|
||||
}
|
||||
@@ -515,11 +695,83 @@ namespace AdvancedPaste.ViewModels
|
||||
IsAllowedByGPO = PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled;
|
||||
}
|
||||
|
||||
private bool TryResolveAdvancedAIProvider(out PasteAIProviderDefinition provider, out bool usesPasteScope)
|
||||
{
|
||||
provider = null;
|
||||
usesPasteScope = false;
|
||||
|
||||
var configuration = _userSettings?.PasteAIConfiguration;
|
||||
if (configuration is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var activeProvider = configuration.ActiveProvider;
|
||||
if (IsAdvancedAIProvider(activeProvider))
|
||||
{
|
||||
provider = activeProvider;
|
||||
usesPasteScope = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedAIProvider);
|
||||
if (fallback is not null)
|
||||
{
|
||||
provider = fallback;
|
||||
usesPasteScope = configuration.UseSharedCredentials;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsAdvancedAIProvider(PasteAIProviderDefinition provider)
|
||||
{
|
||||
return provider is not null && provider.EnableAdvancedAI && SupportsAdvancedAI(provider.ServiceTypeKind);
|
||||
}
|
||||
|
||||
private static bool SupportsAdvancedAI(AIServiceType serviceType)
|
||||
{
|
||||
return serviceType is AIServiceType.OpenAI
|
||||
or AIServiceType.AzureOpenAI;
|
||||
}
|
||||
|
||||
private bool UpdateOpenAIKey()
|
||||
{
|
||||
UpdateAllowedByGPO();
|
||||
|
||||
return IsAllowedByGPO && _aiCredentialsProvider.Refresh();
|
||||
var pasteKeyChanged = _credentialsProvider.Refresh(AICredentialScope.PasteAI);
|
||||
var advancedKeyChanged = _credentialsProvider.Refresh(AICredentialScope.AdvancedAI);
|
||||
|
||||
return pasteKeyChanged || advancedKeyChanged;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SetActiveProviderAsync(PasteAIProviderDefinition provider)
|
||||
{
|
||||
if (provider is null || string.IsNullOrEmpty(provider.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(ActiveAIProvider?.Id, provider.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _userSettings.SetActiveAIProviderAsync(provider.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to activate AI provider", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateAIProviderActiveFlags();
|
||||
OnPropertyChanged(nameof(AIProviders));
|
||||
EnqueueRefreshPasteFormats();
|
||||
}
|
||||
|
||||
public async Task CancelPasteActionAsync()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h AdvancedPaste.base.rc AdvancedPaste.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted ..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h AdvancedPaste.base.rc AdvancedPaste.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>15.0</VCProjectVersion>
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/gpo.h>
|
||||
|
||||
#include <winrt/Windows.Security.Credentials.h>
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <vector>
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
|
||||
@@ -54,12 +55,14 @@ namespace
|
||||
const wchar_t JSON_KEY_ADVANCED_PASTE_UI_HOTKEY[] = L"advanced-paste-ui-hotkey";
|
||||
const wchar_t JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY[] = L"paste-as-markdown-hotkey";
|
||||
const wchar_t JSON_KEY_PASTE_AS_JSON_HOTKEY[] = L"paste-as-json-hotkey";
|
||||
const wchar_t JSON_KEY_IS_ADVANCED_AI_ENABLED[] = L"IsAdvancedAIEnabled";
|
||||
const wchar_t JSON_KEY_IS_AI_ENABLED[] = L"IsAIEnabled";
|
||||
const wchar_t JSON_KEY_IS_OPEN_AI_ENABLED[] = L"IsOpenAIEnabled";
|
||||
const wchar_t JSON_KEY_SHOW_CUSTOM_PREVIEW[] = L"ShowCustomPreview";
|
||||
const wchar_t JSON_KEY_PASTE_AI_CONFIGURATION[] = L"paste-ai-configuration";
|
||||
const wchar_t JSON_KEY_PROVIDERS[] = L"providers";
|
||||
const wchar_t JSON_KEY_SERVICE_TYPE[] = L"service-type";
|
||||
const wchar_t JSON_KEY_ENABLE_ADVANCED_AI[] = L"enable-advanced-ai";
|
||||
const wchar_t JSON_KEY_VALUE[] = L"value";
|
||||
|
||||
const wchar_t OPENAI_VAULT_RESOURCE[] = L"https://platform.openai.com/api-keys";
|
||||
const wchar_t OPENAI_VAULT_USERNAME[] = L"PowerToys_AdvancedPaste_OpenAIKey";
|
||||
}
|
||||
|
||||
class AdvancedPaste : public PowertoyModuleIface
|
||||
@@ -94,6 +97,7 @@ private:
|
||||
using CustomAction = ActionData<int>;
|
||||
std::vector<CustomAction> m_custom_actions;
|
||||
|
||||
bool m_is_ai_enabled = false;
|
||||
bool m_is_advanced_ai_enabled = false;
|
||||
bool m_preview_custom_format_output = true;
|
||||
|
||||
@@ -145,32 +149,11 @@ private:
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
static bool open_ai_key_exists()
|
||||
{
|
||||
try
|
||||
{
|
||||
winrt::Windows::Security::Credentials::PasswordVault().Retrieve(OPENAI_VAULT_RESOURCE, OPENAI_VAULT_USERNAME);
|
||||
return true;
|
||||
}
|
||||
catch (const winrt::hresult_error& ex)
|
||||
{
|
||||
// Looks like the only way to access the PasswordVault is through an API that throws an exception in case the resource doesn't exist.
|
||||
// If the debugger breaks here, just continue.
|
||||
// If you want to disable breaking here in a more permanent way, just add a condition in Visual Studio's Exception Settings to not break on win::hresult_error, but that might make you not hit other exceptions you might want to catch.
|
||||
if (ex.code() == HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
|
||||
{
|
||||
return false; // Credential doesn't exist.
|
||||
}
|
||||
Logger::error("Unexpected error while retrieving OpenAI key from vault: {}", winrt::to_string(ex.message()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_open_ai_enabled()
|
||||
bool is_ai_enabled()
|
||||
{
|
||||
return gpo_policy_enabled_configuration() != powertoys_gpo::gpo_rule_configured_disabled &&
|
||||
powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue() != powertoys_gpo::gpo_rule_configured_disabled &&
|
||||
open_ai_key_exists();
|
||||
m_is_ai_enabled;
|
||||
}
|
||||
|
||||
static std::wstring kebab_to_pascal_case(const std::wstring& kebab_str)
|
||||
@@ -201,6 +184,13 @@ private:
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::wstring to_lower_case(const std::wstring& value)
|
||||
{
|
||||
std::wstring result = value;
|
||||
std::transform(result.begin(), result.end(), result.begin(), [](wchar_t ch) { return std::towlower(ch); });
|
||||
return result;
|
||||
}
|
||||
|
||||
bool migrate_data_and_remove_data_file(Hotkey& old_paste_as_plain_hotkey)
|
||||
{
|
||||
const wchar_t OLD_JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
|
||||
@@ -267,6 +257,61 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
bool has_advanced_ai_provider(const winrt::Windows::Data::Json::JsonObject& propertiesObject)
|
||||
{
|
||||
if (!propertiesObject.HasKey(JSON_KEY_PASTE_AI_CONFIGURATION))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto configValue = propertiesObject.GetNamedValue(JSON_KEY_PASTE_AI_CONFIGURATION);
|
||||
if (configValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto configObject = configValue.GetObjectW();
|
||||
if (!configObject.HasKey(JSON_KEY_PROVIDERS))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto providersValue = configObject.GetNamedValue(JSON_KEY_PROVIDERS);
|
||||
if (providersValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Array)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto providers = providersValue.GetArray();
|
||||
for (const auto providerValue : providers)
|
||||
{
|
||||
if (providerValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto providerObject = providerValue.GetObjectW();
|
||||
if (!providerObject.GetNamedBoolean(JSON_KEY_ENABLE_ADVANCED_AI, false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!providerObject.HasKey(JSON_KEY_SERVICE_TYPE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::wstring serviceType = providerObject.GetNamedString(JSON_KEY_SERVICE_TYPE, L"").c_str();
|
||||
const auto normalizedServiceType = to_lower_case(serviceType);
|
||||
if (normalizedServiceType == L"openai" || normalizedServiceType == L"azureopenai")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void read_settings(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
const auto settingsObject = settings.get_raw_json();
|
||||
@@ -341,7 +386,7 @@ private:
|
||||
if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS))
|
||||
{
|
||||
const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE);
|
||||
if (customActions.Size() > 0 && is_open_ai_enabled())
|
||||
if (customActions.Size() > 0 && is_ai_enabled())
|
||||
{
|
||||
for (const auto& customAction : customActions)
|
||||
{
|
||||
@@ -365,9 +410,19 @@ private:
|
||||
{
|
||||
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_IS_ADVANCED_AI_ENABLED))
|
||||
m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
|
||||
{
|
||||
m_is_advanced_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_ADVANCED_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE);
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
|
||||
{
|
||||
m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_is_ai_enabled = false;
|
||||
}
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
|
||||
|
||||