Compare commits

...

45 Commits

Author SHA1 Message Date
Kayla Cinnamon
1b0dfabda5 Delete doc/images/overview/PT_holiday_hero_image.png 2025-01-09 18:25:37 -05:00
Kayla Cinnamon
83102012da Update hero image 2025-01-09 18:21:40 -05:00
Kayla Cinnamon
db011af1e8 Remove Advent calendar from README 2025-01-09 18:20:18 -05:00
leileizhang
5ef918750d [Localization] Fix loc pipeline to send downloaded localized files to TDBuild upon retry after failure (#36766) 2025-01-09 09:45:12 -08:00
Laszlo Nemeth
084978c465 [Settings] Add Workspaces' workspaces.json file to backup/restore list (#36714) 2025-01-09 12:19:02 +01:00
Asif Islam
0d71f11fdc [Monaco]Add support for .resx and .resw preview support. (#36499)
Added support for .resx and .resw preview support. These files are XML based files so I added the file extensions as part of the registerAdditionalLanguage("xmlExt"...) function.
2025-01-08 16:35:02 +00:00
Heiko
a29ff07ec0 [Settings][PTRun]Show plugin version and website (#36580) 2025-01-08 14:03:27 +00:00
PesBandi
cd2a88704d [PTRun][Calc]Improve handling of non-base 10 numbers (#36700) 2025-01-08 10:49:26 +00:00
Heiko
308c4b817e [PTRun][Calculator]Fix unit tests on non-english systems (#36569)
* fix tests

* update comment
2025-01-08 10:34:13 +00:00
PesBandi
809791da25 [Settings][QuickAccent]Change 'character set' to plural form (#36565) 2025-01-06 15:59:39 +00:00
immi
5e9675eb4f [PTRun]Add context buttons for VSCodeWorkspaces plugin (#36517) 2025-01-06 15:15:20 +00:00
Hao Liu
6ca02f0d3c [QuickAccent]Add Proto-Indo-European (#36408)
* Add ḱ to PowerAccent for Proto-Indo-European

* Fix Spelling Check

* Add more letters to PIE

* Order PIE after PI

---------

Co-authored-by: Hao Liu (from Dev Box) <haoliu3@microsoft.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
2025-01-06 13:38:59 +00:00
Clint Rutkas
90e75a19f0 [CQ]Continued to move stuff out of root - Solution.props (#36244) 2025-01-06 10:52:45 +00:00
Davide Giacometti
deddce22e7 [PTRun]Fix drag on .NET 9 WPF (#36635) 2025-01-06 10:47:50 +00:00
Laszlo Nemeth
21391bbc5b Fix snapping Workspaces Editor to Fancy Zones (#36463)
* Remove Workspaces Editor from Fancy Zone's excluded app list

* removed unused string

---------

Co-authored-by: Seraphima <zykovas91@gmail.com>
2025-01-06 10:36:14 +01:00
Domen Soklič
bd30da6001 [QuickAccent]Added ć for Slovenian (#36336) (#36338) 2025-01-03 16:23:04 +00:00
Heiko
94d712135c [PTRun][Calculator]Update mages to v3.0.0 and support for randi(n) (#36560)
* update to mages 3.0.0

* allow randi() in calculator plugin

* fix tests and input validation

* fix spell check
2025-01-03 16:11:42 +00:00
Davide Giacometti
1eec678276 [TextExtractor]Minor UI/Accessibility fixes in the overlay UI (#36356)
minor UI and accessibility fixes
2025-01-02 17:34:58 +00:00
Connor Plante
2ba5fb75bc [PTRun][Calculator]Handle hexadecimal numbers to not return divide by 0 error (#36390)
* add '0x' handling for divide by 0 scenarios

* fix comment on division by 0 check

---------

Co-authored-by: Connor Plante <connor.plante@gmail.com>
2025-01-02 16:48:17 +00:00
Jaime Bernardo
a720dd537c [New+]Don't override New actions from Explorer on Windows 10 (#36467)
* [New+]Don't override New actions from Explorer

* Update src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp

* Update src/modules/NewPlus/NewShellExtensionContextMenu.win10/shell_context_menu_win10.cpp

* Add pattern for learn.microsoft links

* Also only Query for context menu
2024-12-27 12:59:52 +00:00
Kai Tao
e4d2deb89e Add team member (#36590) 2024-12-27 19:24:08 +08:00
leileizhang
fbd72cc1ea [CI] Enhance build Pipeline Reliability with Retry Logic and Improved Error Messaging (#36529)
* update pipeline with retry

* remove tests
2024-12-24 08:41:24 +08:00
Jaime Bernardo
bb637c16dc [Docs]Update README for 0.87.1 release (#36534) 2024-12-23 11:54:56 +00:00
Demitrius Nelon
ea23f1ec1a Remove "prerelease: true" (#36484)
* Remove "prerelease: true"

The Microsoft.WinGet.DSC module is GA so prerelease: true is no longer needed.

* Update expect.txt

fixing spellcheck warning

---------

Co-authored-by: Clint Rutkas <clint@rutkas.com>
2024-12-20 20:18:21 -08:00
Jerry Xu
f727762d03 Add more STCA team members (#36438)
* Add more STCA team members

* Include more spelling check errors

* Remove non-alpha in dictionary

* Add Zhaopeng Wang

* Fix spell checking error
2024-12-20 13:14:07 +08:00
Shuai Yuan
e2cd8633b9 [Bug fix] Add a format validation step before format conversion. (#36404)
This PR aims to fix the bug #35225 by introducing a new method IsJson to determine if a given text is in JSON format.
The IsJson method is then utilized in the ToJsonFromXmlOrCsvAsync method to optimize the processing logic.
If the text is already in JSON format, it is returned directly without further conversion from XML or CSV.

Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
2024-12-19 16:52:12 +08:00
Shuai Yuan
2a6dcb9f70 [Bug fix] Making the OpenAI key configuration page scrollable. (#36359)
* Fixed #34470

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
2024-12-19 14:16:59 +08:00
Shuai Yuan
342c6167f5 Add new pipeline using the latest webview2 from Edge Canary (#36317)
* using the latest webview2 for testing


---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
2024-12-19 10:27:04 +08:00
moooyo
20a5f67222 [AOT compatible] Clean up some AOT build issue in FilePreviewCommon and MarkdownPreviewHandler (#36207)
* Use AppContext.BaseDirectory to replace assembly.GetExeAseembly.Location.
Fix json serilizer aot issue.

* clean up some AOT build issue

* Update src/modules/previewpane/MarkdownPreviewHandler/MarkdownPreviewHandlerControl.cs

Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>

* Update src/common/FilePreviewCommon/Formatters/JsonFormatter.cs

Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>
2024-12-19 09:31:45 +08:00
moooyo
86c6b4ae95 [AOT compatible] Make HostsUILib become AOT compatible (#36136)
* Remove AOT configuration

* Refer to AOT compatibility props

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2024-12-19 09:22:41 +08:00
Shuai Yuan
ea66066a54 Add New CI Pipeline for Latest WindowsAppSDK (#36282)
This PR introduces the following changes to the CI pipeline and version management:

Pipeline Enhancements:
1. Added a new script UpdateVersions.ps1 to automate the update of Microsoft.WindowsAppSDK versions across various project files.
2. Introduced a new pipeline configuration ci-using-the-latest-winappsdk.yml to build using the latest Microsoft.WindowsAppSDK.
3. Updated existing pipeline configurations to support the new useLatestWinAppSDK parameter.

Pipeline Configuration Updates:
1. Updated job-build-project.yml to handle the useLatestWinAppSDK parameter and adjust the RestoreAdditionalProjectSourcesArg accordingly.
2. Added a new template steps-update-winappsdk-and-restore-nuget.yml for updating and restoring NuGet packages with the latest Microsoft.WindowsAppSDK.
3. Added WinAPPSDK version selection, the pipeline can be manually triggered to use the specified version.

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
2024-12-19 09:00:53 +08:00
leileizhang
43bc811c59 [Fuzz] Add fuzz testing for AdvancedPaste and new pipeline for onboarding OneFuzz (#36329)
* add fuzz

* install .net8

* add spelling check

* refine the pipeline

* add readme and update the test code

* fix spelling error

* change to weekly run
2024-12-19 08:31:10 +08:00
Ani
799f7396d2 [AdvancedPaste]Fix NullReferenceException on Dispose (#36428)
[AdvancedPaste] Fixed NullReferenceException on Dispose
2024-12-18 19:31:20 +00:00
Ionuț Manța
e77ea96a14 [PTRun]Fix .NET 9 crash in OneNote (#36417)
* Fix crash in OneNote Run

* added better comment
2024-12-18 17:39:17 +00:00
Ionuț Manța
6f23fb503b [PTRun]Fix WPF transparent border issue on Windows 10 (#36392)
* Added border on W10

* Added a comment

---------
Co-authored-by: Davide Giacometti <25966642+davidegiacometti@users.noreply.github.com>
2024-12-18 11:41:19 +00:00
Jaime Bernardo
feeeec644c 0.87 changelog (#36335)
* 0.87 changelog

* Fix spellcheck

* Update README.md

Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>

---------

Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
2024-12-16 14:35:34 -05:00
Stefan Markovic
f19a34d50c [FZEditor]Fix Create new layout dialog radio buttons IsChecked values (#36320) 2024-12-12 14:53:16 +00:00
Clint Rutkas
05e5e92199 Making the powertoys-code-owners team code owners (#36310)
* Update CODEOWNERS

* Update names.txt

* Update CODEOWNERS
2024-12-11 14:38:49 -08:00
Clint Rutkas
7aba6e8e75 Update CODEOWNERS to include gordon, jerry and kayla (#36308)
* Update CODEOWNERS

* Update names.txt
2024-12-11 13:47:45 -08:00
Jaime Bernardo
c5dc93f720 [ci]Sign OpenAI dll that's not signed (#36299) 2024-12-11 13:50:16 +00:00
Davide Giacometti
ce0e00fa3b [Settings][Dashboard] Accessibility fixes (#36280)
* make narrator announce buttons/toggles

* add toggles module name
2024-12-11 11:09:38 +00:00
Niels Laute
7a39f2fd46 [UX]Updating New+ and Settings icons (#36290)
* Updated icons

* Updating more icons and icos
2024-12-11 10:13:56 +00:00
Ionuț Manța
7c6af6580e [Launcher]Port from WPF-UI to .NET 9 WPF (#36215)
* Initial implementation

* Fix fluent style

* Fix no endline

* Update expect.txt

* Fix formatting

* Fix light theme looking bad on Windows 10

* fix formatting

* test change

* Now really fixed W10

* Add a comment

* Fix typos

* Fix spellcheck errors

* Fix spellcheck pattern for websites

* Change patterns for spellcheck in the right file

* Fix XAML styling

* Fix contrast colors on W11

* Fix formatting

* Removed emty line

* Fix formatting

* Added comment to fluentHC file

* fix comment

* Fix Windows10 again.
Adress feedback.

* W11 fix chaning from high contrast to normal not having correct background

* W10 Fix high contrast not working after switching from light/dark moed

* Address feedback

* Fix formatting

* Second W11 fix chaning from high contrast to normal not having correct background
2024-12-11 09:47:08 +00:00
Ani
bf3474b134 [AdvancedPaste]Add Semantic Kernel opt-in to allow chaining of paste actions (#35902)
* [AdvancedPaste] Semantic Kernel support

* Changed log-line with potentially sensitive info

* Spellcheck issues

* Various improvements for Semantic Kernel

* Spellcheck issue

* Refactored Clipboard routines

* Added integration tests for KernelService

* Extra telemetry for AdvancedPaste

* Added 'Hotkey' suffix to AdvancedPaste_Settings telemetry event

* Added IsSavedQuery

* Added KernelQueryCache

* Refactoring

* Added KernelQueryCache to BugReportTool delete list

* Added opt-n for Semantic Kernel

* Fixed bug with KernelQueryCache

* Ability to view last AI chat message on error

* Improved kernel query cache

* Used System.IO.Abstractions and improved tests

* Fixed under-count of token usage

* Used Semantic Kernel icon

* Cleanup

* Add missing EndProject line

* Fix dependency version conflicts

* Fix NOTICE.md

* Correct place of SemanticKernel in NOTICE.md

* Unlinked CustomPreview toggle from AI

* Added Microsoft.Bcl.AsyncInterfaces dependency to AdvancedPaste

* Fixed NOTICE.md order

* Moved Custom Preview to behaviour section

* Made Image to Text raise error on empty output

* Added AIServiceBatchIntegrationTests

* Updated AIServiceBatchIntegrationTests

* Added prompt moderation

* Moved GPO Infobar to better location
2024-12-11 09:28:44 +00:00
Clint Rutkas
474b0cfbdf Move the XamlStyler config to src/ (#36202)
my never ending goal to minimize files in the root dir
2024-12-10 17:06:30 -06:00
189 changed files with 11099 additions and 1432 deletions

View File

@@ -12,7 +12,6 @@ properties:
id: vsPackage
directives:
description: Install Visual Studio 2022 Community (Any edition will work)
allowPrerelease: true
settings:
id: Microsoft.VisualStudio.2022.Community
source: winget

View File

@@ -12,7 +12,6 @@ properties:
id: vsPackage
directives:
description: Install Visual Studio 2022 Enterprise (Any edition will work)
allowPrerelease: true
settings:
id: Microsoft.VisualStudio.2022.Enterprise
source: winget

View File

@@ -12,7 +12,6 @@ properties:
id: vsPackage
directives:
description: Install Visual Studio 2022 Professional (Any edition will work)
allowPrerelease: true
settings:
id: Microsoft.VisualStudio.2022.Professional
source: winget

14
.github/CODEOWNERS vendored
View File

@@ -1,16 +1,16 @@
# Protect `.github` folder except the spell-check rules inside it. (The exception happens by not defining any owner user or group for the path.)
# Protection of the spell-check rules makes no sense as it needs to be changed in nearly every PR.
/.github/ @crutkas @DHowett @ethanfangg
/.github/ @microsoft/powertoys-code-owners
/.github/actions/spell-check/
# locking down pipeline folder
/.pipelines/ @crutkas @DHowett @ethanfangg
/.pipelines/ @microsoft/powertoys-code-owners
# locking down nuget config
nuget.config @crutkas @DHowett @ethanfangg
packages.config @crutkas @DHowett @ethanfangg
nuget.config @microsoft/powertoys-code-owners
packages.config @microsoft/powertoys-code-owners
# locking down files that should not change
LICENSE @crutkas @DHowett @ethanfangg
SECURITY.md @crutkas @DHowett @ethanfangg
CODE_OF_CONDUCT.md @crutkas @DHowett @ethanfangg
LICENSE @microsoft/powertoys-code-owners
SECURITY.md @microsoft/powertoys-code-owners
CODE_OF_CONDUCT.md @microsoft/powertoys-code-owners

View File

@@ -78,7 +78,14 @@ sinclairinat
stylecop
uipi
yinwang
myaccess
onmicrosoft
aep
epsf
howto
onefuzzconfig
oip
onefuzzingestionpreparationtool
# KEYS
@@ -246,3 +253,12 @@ pwa
AOT
Aot
# YML
onefuzz
# NameInCode
leilzh
#Tools
OIP

View File

@@ -46,6 +46,8 @@ betsegaw
bricelam
bsky
CCcat
chenmy
chemwolf
Chinh
chrdavis
Chrzan
@@ -63,6 +65,7 @@ Deondre
DHowett
ductdo
Essey
Feng
ethanfangg
ferraridavide
frankychen
@@ -78,6 +81,7 @@ gordon
grzhan
Guo
hanselman
haoliuu
Harmath
Heiko
Hemmerlein
@@ -87,10 +91,12 @@ Howett
htcfreek
Huynh
Ionut
jamrobot
Jaswal
jefflord
Jordi
jyuwono
kai
Kairu
Kamra
Kantarci
@@ -108,6 +114,7 @@ Markovic
martinchrzan
martinmoene
Melman
Mengyuan
Mikhayelyan
msft
Mykhailo
@@ -120,11 +127,13 @@ oldnewthing
onegreatworld
palenshus
pedrolamas
Peiyao
peteblois
phoboslab
Ponten
Pooja
Pylyp
Qingpeng
quachpas
Quriz
randyrants
@@ -153,14 +162,23 @@ Taras
TBM
tilovell
Triet
urnotdfs
waaverecords
wang
Whuihuan
Xiaofeng
Xpg
Yaqing
yaqingmi
ycv
yeelam
Yuniardi
yuyoyuppe
Zeol
Zhao
Zhaopeng
zhaopy
zhaoqpcn
Zoltan
Zykova

View File

@@ -24,6 +24,7 @@ AFeature
AFFINETRANSFORM
AFX
AGGREGATABLE
ahk
AHybrid
akv
ALarger
@@ -127,6 +128,7 @@ boxmodel
BPBF
bpmf
bpp
Breadcrumb
Browsable
BROWSEINFO
bsd
@@ -328,6 +330,7 @@ DEVMODEW
DEVMON
devpkey
DEVSOURCE
DGR
DIIRFLAG
dimm
DISABLEASACTIONKEY
@@ -615,6 +618,7 @@ HWNDLAST
HWNDNEXT
HWNDPREV
hyjiacan
IAI
IBeam
ICONERROR
ICONLOCATION
@@ -641,6 +645,7 @@ imageresizerinput
imageresizersettings
imagingdevices
ime
Indo
inetcpl
Infobar
INFOEXAMPLE
@@ -736,6 +741,7 @@ LExit
lhwnd
LIBID
lindex
linkid
LINKOVERLAY
LINQTo
listview
@@ -749,7 +755,7 @@ lnks
LOADFROMFILE
LOBYTE
LOCALDISPLAY
LOCALPACKAGE
localpackage
LOCALSYSTEM
LOCATIONCHANGE
LOGFONT
@@ -1208,6 +1214,7 @@ QUEUESYNC
QUNS
RAII
RAlt
randi
Rasterize
RAWINPUTDEVICE
RAWINPUTHEADER
@@ -1406,6 +1413,7 @@ SIZENS
SIZENWSE
sizeread
SIZEWE
SKEXP
SKIPOWNPROCESS
sku
SLGP
@@ -1716,6 +1724,7 @@ WIC
wifi
wil
winapi
winappsdk
wincodec
Wincodecsdk
wincolor

View File

@@ -131,7 +131,7 @@ _mm_(?!dd)\w+
# hit-count: 4 file-count: 4
# microsoft
\b(?:https?://|)(?:(?:(?:blogs|download\.visualstudio|docs|msdn2?|research)\.|)microsoft|blogs\.msdn)\.co(?:m|\.\w\w)/[-_a-zA-Z0-9()=./%]*
\b(?:https?://|)(?:(?:(?:blogs|download\.visualstudio|developer|docs|learn|msdn2?|research)\.|)microsoft|blogs\.msdn)\.co(?:m|\.\w\w)/[-_a-zA-Z0-9()=./%#]*
aka\.ms/[a-zA-Z0-9]+

View File

@@ -327,6 +327,7 @@
"WinUI3Apps\\TestableIO.System.IO.Abstractions.dll",
"TestableIO.System.IO.Abstractions.Wrappers.dll",
"WinUI3Apps\\TestableIO.System.IO.Abstractions.Wrappers.dll",
"WinUI3Apps\\OpenAI.dll",
"ColorCode.Core.dll",
"ColorCode.UWP.dll",
"UnitsNet.dll",

View File

@@ -0,0 +1,130 @@
Param(
# Using the default value of 1.6 for winAppSdkVersionNumber and useExperimentalVersion as false
[Parameter(Mandatory=$False,Position=1)]
[string]$winAppSdkVersionNumber = "1.6",
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
[Parameter(Mandatory=$False,Position=2)]
[boolean]$useExperimentalVersion = $False
)
function Update-NugetConfig {
param (
[string]$filePath = "nuget.config"
)
Write-Host "Updating nuget.config file"
[xml]$xml = Get-Content -Path $filePath
# Add localpackages source into nuget.config
$packageSourcesNode = $xml.configuration.packageSources
$addNode = $xml.CreateElement("add")
$addNode.SetAttribute("key", "localpackages")
$addNode.SetAttribute("value", "localpackages")
$packageSourcesNode.AppendChild($addNode) | Out-Null
# Remove <packageSourceMapping> tag and its content
$packageSourceMappingNode = $xml.configuration.packageSourceMapping
if ($packageSourceMappingNode) {
$xml.configuration.RemoveChild($packageSourceMappingNode) | Out-Null
}
# print nuget.config after modification
$xml.OuterXml
# Save the modified nuget.config file
$xml.Save($filePath)
}
$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
# Execute nuget list and capture the output
if ($useExperimentalVersion) {
# The nuget list for experimental versions will cost more time
# So, we will not use -AllVersions to wast time
# But it can only get the latest experimental version
Write-Host "Fetching WindowsAppSDK with experimental versions"
$nugetOutput = nuget list Microsoft.WindowsAppSDK `
-Source $sourceLink `
-Prerelease
# Filter versions based on the specified version prefix
$escapedVersionNumber = [regex]::Escape($winAppSdkVersionNumber)
$filteredVersions = $nugetOutput | Where-Object { $_ -match "Microsoft.WindowsAppSDK $escapedVersionNumber\." }
$latestVersions = $filteredVersions
} else {
Write-Host "Fetching stable WindowsAppSDK versions for $winAppSdkVersionNumber"
$nugetOutput = nuget list Microsoft.WindowsAppSDK `
-Source $sourceLink `
-AllVersions
# Filter versions based on the specified version prefix
$escapedVersionNumber = [regex]::Escape($winAppSdkVersionNumber)
$filteredVersions = $nugetOutput | Where-Object { $_ -match "Microsoft.WindowsAppSDK $escapedVersionNumber\." }
$latestVersions = $filteredVersions | Sort-Object { [version]($_ -split ' ')[1] } -Descending | Select-Object -First 1
}
Write-Host "Latest versions found: $latestVersions"
# Extract the latest version number from the output
$latestVersion = $latestVersions -split "`n" | `
Select-String -Pattern 'Microsoft.WindowsAppSDK\s*([0-9]+\.[0-9]+\.[0-9]+-*[a-zA-Z0-9]*)' | `
ForEach-Object { $_.Matches[0].Groups[1].Value } | `
Sort-Object -Descending | `
Select-Object -First 1
if ($latestVersion) {
$WinAppSDKVersion = $latestVersion
Write-Host "Extracted version: $WinAppSDKVersion"
Write-Host "##vso[task.setvariable variable=WinAppSDKVersion]$WinAppSDKVersion"
} else {
Write-Host "Failed to extract version number from nuget list output"
exit 1
}
# Update packages.config files
Get-ChildItem -Recurse packages.config | ForEach-Object {
$content = Get-Content $_.FullName -Raw
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-Host "Modified " $_.FullName
}
}
# Update Directory.Packages.props file
$propsFile = "Directory.Packages.props"
if (Test-Path $propsFile) {
$content = Get-Content $propsFile -Raw
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $propsFile -Value $content
Write-Host "Modified " $propsFile
}
}
# Update .vcxproj files
Get-ChildItem -Recurse *.vcxproj | ForEach-Object {
$content = Get-Content $_.FullName -Raw
if ($content -match '\\Microsoft.WindowsAppSDK.') {
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion + '\'
$oldVersionString = '\\Microsoft.WindowsAppSDK.[-.0-9a-zA-Z]*\\'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-Host "Modified " $_.FullName
}
}
# Update .csproj files
Get-ChildItem -Recurse *.csproj | ForEach-Object {
$content = Get-Content $_.FullName -Raw
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
Set-Content -Path $_.FullName -Value $content
Write-Host "Modified " $_.FullName
}
}
Update-NugetConfig

View File

@@ -97,7 +97,7 @@ if (-not $Passive)
if ($files.count -gt 0)
{
dotnet tool run xstyler -c "$PSScriptRoot\..\Settings.XamlStyler" -f $files
dotnet tool run xstyler -c "$PSScriptRoot\..\src\Settings.XamlStyler" -f $files
}
else
{
@@ -111,7 +111,7 @@ else
if ($files.count -gt 0)
{
dotnet tool run xstyler -p -c "$PSScriptRoot\..\Settings.XamlStyler" -f $files
dotnet tool run xstyler -p -c "$PSScriptRoot\..\src\Settings.XamlStyler" -f $files
if ($lastExitCode -eq 1)
{

View File

@@ -32,9 +32,9 @@ steps:
TDBuildServiceConnection: $(TouchdownServiceConnection)
authType: SubjectNameIssuer
resourceFilePath: |
**\Resources.resx
**\Resource.resx
**\Resources.resw
src\**\Resources.resx
src\**\Resource.resx
src\**\Resources.resw
outputDirectoryRoot: LocOutput
appendRelativeDir: true
pseudoSetting: Included

View File

@@ -0,0 +1,42 @@
trigger: none
pr: none
schedules:
- cron: "0 0 * * *" # every day at midnight
displayName: "Daily midnight Build"
branches:
include:
- main
always: false # only run if there's code changes!
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
parameters:
- name: buildPlatforms
type: object
default:
- x64
- arm64
- name: enableMsBuildCaching
type: boolean
displayName: "Enable MSBuild Caching"
default: false
- name: runTests
type: boolean
displayName: "Run Tests"
default: true
- name: useVSPreview
type: boolean
displayName: "Build Using Visual Studio Preview"
default: false
- name: useLatestWebView2
type: boolean
default: false
extends:
template: templates/pipeline-ci-build.yml
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
runTests: ${{ parameters.runTests }}
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWebView2: ${{ parameters.useLatestWebView2 }}

View File

@@ -0,0 +1,50 @@
trigger: none
pr: none
schedules:
- cron: "0 0 * * *" # every day at midnight
displayName: "Daily midnight Build"
branches:
include:
- main
always: false # only run if there's code changes!
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
parameters:
- name: buildPlatforms
type: object
default:
- x64
- arm64
- name: enableMsBuildCaching
type: boolean
displayName: "Enable MSBuild Caching"
default: false
- name: runTests
type: boolean
displayName: "Run Tests"
default: true
- name: useVSPreview
type: boolean
displayName: "Build Using Visual Studio Preview"
default: false
- name: useLatestWinAppSDK
type: boolean
default: true
- name: winAppSDKVersionNumber
type: string
default: 1.6
- name: useExperimentalVersion
type: boolean
default: false
extends:
template: templates/pipeline-ci-build.yml
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
runTests: ${{ parameters.runTests }}
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}

55
.pipelines/v2/oneFuzz.yml Normal file
View File

@@ -0,0 +1,55 @@
pr: none
trigger: none
schedules:
- cron: "0 0 * * 1"
displayName: Weekly fuzzing submission
branches:
include:
- main
always: true
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
parameters:
- name: platform
type: string
default: x64 # for fuzzing, we only use x64 for now
- name: enableMsBuildCaching
type: boolean
displayName: "Enable MSBuild Caching"
default: false
- name: useVSPreview
type: boolean
displayName: "Build Using Visual Studio Preview"
default: false
stages:
- stage: Build_${{ parameters.platform }}
displayName: Build ${{ parameters.platform }}
jobs:
- template: templates/job-build-project.yml
parameters:
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
buildPlatforms:
- ${{ parameters.platform }}
buildConfigurations: [Release]
enablePackageCaching: true
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
runTests: true
useVSPreview: ${{ parameters.useVSPreview }}
- stage: OneFuzz
displayName: Fuzz ${{ parameters.platform }}
dependsOn:
- Build_${{parameters.platform}}
jobs:
- template: templates/job-fuzz.yml
parameters:
platform: ${{ parameters.platform }}
configuration: Release

View File

@@ -56,6 +56,15 @@ parameters:
- name: versionNumber
type: string
default: '0.0.1'
- name: useLatestWinAppSDK
type: boolean
default: false
- name: winAppSDKVersionNumber
type: string
default: 1.6
- name: useExperimentalVersion
type: boolean
default: false
- name: csProjectsToPublish
type: object
default:
@@ -102,6 +111,10 @@ jobs:
${{ else }}:
MSBuildMainBuildTargets: Build
${{ insert }}: ${{ parameters.variables }}
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
RestoreAdditionalProjectSourcesArg: '/p:RestoreAdditionalProjectSources="$(Build.SourcesDirectory)\localpackages\NugetPackages"'
${{ else }}:
RestoreAdditionalProjectSourcesArg: ''
displayName: Build
timeoutInMinutes: 240
cancelTimeoutInMinutes: 1
@@ -134,6 +147,11 @@ jobs:
sdk: true
version: '6.0'
- template: steps-ensure-dotnet-version.yml
parameters:
sdk: true
version: '8.0'
- template: steps-ensure-dotnet-version.yml
parameters:
sdk: true
@@ -177,8 +195,15 @@ jobs:
"packages.config" | "$(Agent.OS)"
"packages.config"
path: packages
- template: .\steps-restore-nuget.yml
- ${{ if eq(parameters.useLatestWinAppSDK, true)}}:
- template: .\steps-update-winappsdk-and-restore-nuget.yml
parameters:
versionNumber: ${{ parameters.winAppSDKVersionNumber }}
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
- ${{ if eq(parameters.useLatestWinAppSDK, false)}}:
- template: .\steps-restore-nuget.yml
- pwsh: |-
& "$(build.sourcesdirectory)\.pipelines\verifyAndSetLatestVCToolsVersion.ps1"
@@ -209,6 +234,7 @@ jobs:
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)
/t:$(MSBuildMainBuildTargets)
$(RestoreAdditionalProjectSourcesArg)
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
@@ -233,10 +259,11 @@ jobs:
inputs:
solution: '**\HostsUILib.csproj'
vsVersion: 17.0
${{ if eq(parameters.useVSPreview, true) }}:
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-hosts.binlog /p:NoWarn=NU5104
${{ else }}:
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-hosts.binlog
msbuildArgs: >-
/p:CIBuild=true;NoBuild=true -t:pack
/bl:$(LogOutputDirectory)\build-hosts.binlog
/p:NoWarn=NU5104
$(RestoreAdditionalProjectSourcesArg)
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
maximumCpuCount: true
@@ -249,10 +276,11 @@ jobs:
inputs:
solution: '**\EnvironmentVariablesUILib.csproj'
vsVersion: 17.0
${{ if eq(parameters.useVSPreview, true) }}:
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-env-var-editor.binlog /p:NoWarn=NU5104
${{ else }}:
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-env-var-editor.binlog
msbuildArgs: >-
/p:CIBuild=true;NoBuild=true -t:pack
/bl:$(LogOutputDirectory)\build-env-var-editor.binlog
/p:NoWarn=NU5104
$(RestoreAdditionalProjectSourcesArg)
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
maximumCpuCount: true
@@ -265,10 +293,11 @@ jobs:
inputs:
solution: '**\RegistryPreviewUILib.csproj'
vsVersion: 17.0
${{ if eq(parameters.useVSPreview, true) }}:
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-registry-preview.binlog /p:NoWarn=NU5104
${{ else }}:
msbuildArgs: /p:CIBuild=true;NoBuild=true -t:pack /bl:$(LogOutputDirectory)\build-registry-preview.binlog
msbuildArgs: >-
/p:CIBuild=true;NoBuild=true -t:pack
/bl:$(LogOutputDirectory)\build-registry-preview.binlog
/p:NoWarn=NU5104
$(RestoreAdditionalProjectSourcesArg)
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
maximumCpuCount: true
@@ -323,6 +352,7 @@ jobs:
/bl:$(LogOutputDirectory)\build-bug-report.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)
$(RestoreAdditionalProjectSourcesArg)
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
@@ -343,6 +373,7 @@ jobs:
/bl:$(LogOutputDirectory)\build-webcam-report.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)
$(RestoreAdditionalProjectSourcesArg)
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
@@ -363,6 +394,7 @@ jobs:
/bl:$(LogOutputDirectory)\build-styles-report.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)
$(RestoreAdditionalProjectSourcesArg)
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
@@ -385,6 +417,7 @@ jobs:
/p:PowerToysRoot=$(Build.SourcesDirectory)
/p:PublishProfile=InstallationPublishProfile.pubxml
/bl:$(LogOutputDirectory)\publish-${{ join('_',split(project, '/')) }}.binlog
$(RestoreAdditionalProjectSourcesArg)
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
msbuildArchitecture: x64
@@ -414,9 +447,11 @@ jobs:
& '.pipelines/verifyPossibleAssetConflicts.ps1' -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)\WinUI3Apps'
displayName: Audit WinAppSDK applications path asset conflicts
- pwsh: |-
& '.pipelines/verifyNoticeMdAgainstNugetPackages.ps1' -path '$(build.sourcesdirectory)\'
displayName: Verify NOTICE.md and NuGet packages match
# To streamline the pipeline and prevent errors, skip this step during compatibility tests with the latest WinAppSDK.
- ${{ if eq(parameters.useLatestWinAppSDK, false) }}:
- pwsh: |-
& '.pipelines/verifyNoticeMdAgainstNugetPackages.ps1' -path '$(build.sourcesdirectory)\'
displayName: Verify NOTICE.md and NuGet packages match
- ${{ if eq(parameters.runTests, true) }}:
# Publish test results which ran in MSBuild

View File

@@ -0,0 +1,36 @@
parameters:
- name: configuration
type: string
default: "Release"
- name: platform
type: string
default: ""
- name: inputArtifactStem
type: string
default: ""
jobs:
- job: OneFuzz
pool:
vmImage: windows-2022
variables:
ArtifactName: build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}
steps:
- checkout: self
submodules: false
clean: true
fetchDepth: 1
fetchTags: false
- download: current
displayName: Download artifacts
artifact: $(ArtifactName)
patterns: |-
**/tests/*.FuzzTests/**
- task: onefuzz-task@0
inputs:
onefuzzOSes: Windows
env:
onefuzzDropDirectory: $(Pipeline.Workspace)\$(ArtifactName)\x64\Release\x64\Release\tests
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

View File

@@ -8,6 +8,9 @@ parameters:
- name: inputArtifactStem
type: string
default: ""
- name: useLatestWebView2
type: boolean
default: false
jobs:
- job: Test${{ parameters.platform }}${{ parameters.configuration }}
@@ -34,6 +37,29 @@ jobs:
fetchDepth: 1
fetchTags: false
- ${{ if eq(parameters.useLatestWebView2, true) }}:
- powershell: |
$edge_url = 'https://go.microsoft.com/fwlink/?linkid=2084649&Channel=Canary&language=en'
$timeout = New-TimeSpan -Minutes 6
$timeoutSeconds = [int]$timeout.TotalSeconds
$command = {
Invoke-WebRequest -Uri $using:edge_url -OutFile $(Pipeline.Workspace)\MicrosoftEdgeSetup.exe
Write-Host "##[command]Installing Canary channel of Microsoft Edge"
Start-Process $(Pipeline.Workspace)\MicrosoftEdgeSetup.exe -ArgumentList '/silent /install' -Wait
}
$job = Start-Job -ScriptBlock $command
Wait-Job $job -Timeout $timeoutSeconds
if ($job.State -eq "Running") {
Stop-Job $job
Write-Host "##[warning]The job was stopped because it exceeded the time limit."
}
displayName: "Install the latest MSEdge Canary"
- script:
reg add "HKLM\Software\Policies\Microsoft\Edge\WebView2\ReleaseChannels" /v PowerToys.exe /t REG_SZ /d "3"
displayName: "Enable WebView2 Canary Channel"
- download: current
displayName: Download artifacts
artifact: build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}

View File

@@ -22,6 +22,18 @@ parameters:
- name: useVSPreview
type: boolean
default: false
- name: useLatestWebView2
type: boolean
default: false
- name: useLatestWinAppSDK
type: boolean
default: false
- name: winAppSDKVersionNumber
type: string
default: 1.6
- name: useExperimentalVersion
type: boolean
default: false
stages:
# Allow manual builds to skip pre-check
@@ -55,6 +67,10 @@ stages:
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
runTests: ${{ parameters.runTests }}
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
- ${{ if eq(parameters.runTests, true) }}:
- stage: Test_${{ platform }}
@@ -66,3 +82,4 @@ stages:
parameters:
platform: ${{ platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}

View File

@@ -16,6 +16,10 @@ parameters:
steps:
- pwsh: |-
curl.exe -J -L -O "https://dot.net/v1/dotnet-install.ps1"
if (-not (Test-Path dotnet-install.ps1)) {
Write-Error "Failed to download dotnet-install.ps1"
exit 1
}
$NEW_DOTNET_ROOT = "$(Agent.ToolsDirectory)\dotnet"
& ./dotnet-install.ps1 -Channel "${{parameters.version}}" -InstallDir $NEW_DOTNET_ROOT
Write-Host "##vso[task.setvariable variable=DOTNET_ROOT]${NEW_DOTNET_ROOT}"
@@ -25,3 +29,4 @@ steps:
displayName: "Install .NET ${{parameters.version}} SDK"
${{ else }}:
displayName: "Install .NET ${{parameters.version}}"
retryCountOnTaskFailure: 3

View File

@@ -0,0 +1,56 @@
parameters:
- name: versionNumber
type: string
default: 1.6
- name: useExperimentalVersion
type: boolean
default: false
steps:
- task: NuGetAuthenticate@1
displayName: 'NuGet Authenticate'
- task: PowerShell@2
displayName: Update WinAppSDK Versions
inputs:
filePath: '$(build.sourcesdirectory)\.pipelines\UpdateVersions.ps1'
arguments: >
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
- script: echo $(WinAppSDKVersion)
displayName: 'Display WinAppSDK Version Found'
- task: DownloadPipelineArtifact@2
displayName: 'Download WindowsAppSDK'
inputs:
buildType: 'specific'
project: '55e8140e-57ac-4e5f-8f9c-c7c15b51929d'
definition: '104083'
buildVersionToDownload: 'latestFromBranch'
branchName: 'refs/heads/release/${{ parameters.versionNumber }}-stable'
artifactName: 'WindowsAppSDK_Nuget_And_MSIX'
targetPath: '$(Build.SourcesDirectory)\localpackages'
- script: dir $(Build.SourcesDirectory)\localpackages\NugetPackages
displayName: 'List downloaded packages'
- task: NuGetCommand@2
displayName: 'Install WindowsAppSDK'
inputs:
command: 'custom'
arguments: >
install "Microsoft.WindowsAppSDK"
-Source "$(Build.SourcesDirectory)\localpackages\NugetPackages"
-Version "$(WinAppSDKVersion)"
-OutputDirectory "$(Build.SourcesDirectory)\localpackages\output"
-FallbackSource "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
- task: NuGetCommand@2
displayName: 'Restore NuGet packages'
inputs:
command: 'restore'
feedsToUse: 'config'
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
restoreSolution: '$(build.sourcesdirectory)\**\*.sln'
includeNuGetOrg: false

View File

@@ -15,7 +15,7 @@ Param(
$referencedFileVersionsPerDll = @{}
$totalFailures = 0
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude UITests-FancyZones*,MouseJump.Common.UnitTests* | ForEach-Object {
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude UITests-FancyZones*,MouseJump.Common.UnitTests*,AdvancedPaste.FuzzTests* | ForEach-Object {
# Temporarily exclude FancyZones UI tests because of Appium.WebDriver dependencies
$depsJsonFullFileName = $_.FullName
$depsJsonFileName = $_.Name

View File

@@ -181,6 +181,14 @@ Other contributors:
- [@shuaiyuanxx](https://github.com/shuaiyuanxx) - Shawn Yuan - Dev
- [@moooyo](https://github.com/moooyo) - Yu Leng - Dev
- [@haoliuu](https://github.com/haoliuu) - Hao Liu - Dev
- [@chenmy77](https://github.com/chenmy77) - Mengyuan Chen - Dev
- [@chemwolf6922](https://github.com/chemwolf6922) - Feng Wang - Dev
- [@yaqingmi](https://github.com/yaqingmi) - Yaqing Mi - Dev
- [@zhaoqpcn](https://github.com/zhaoqpcn) - Qingpeng Zhao - Dev
- [@urnotdfs](https://github.com/urnotdfs) - Xiaofeng Wang - Dev
- [@zhaopy536](https://github.com/zhaopy536) - Peiyao Zhao - Dev
- [@wang563681252](https://github.com/wang563681252) - Zhaopeng Wang - Dev
- [@vanzue](https://github.com/vanzue) - Kai Tao - Dev
# Former PowerToys core team members

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
<PackageVersion Include="Azure.AI.OpenAI" Version="1.0.0-beta.12" />
<PackageVersion Include="Azure.AI.OpenAI" Version="1.0.0-beta.17" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.0.240109" />
<PackageVersion Include="CommunityToolkit.WinUI.Collections" Version="8.0.240109" />
@@ -22,18 +22,21 @@
<PackageVersion Include="hyjiacan.pinyin4net" Version="4.1.1" />
<PackageVersion Include="Interop.Microsoft.Office.Interop.OneNote" Version="1.1.0.2" />
<PackageVersion Include="LazyCache" Version="2.4.0" />
<PackageVersion Include="Mages" Version="2.0.2" />
<PackageVersion Include="Mages" Version="3.0.0" />
<PackageVersion Include="Markdig.Signed" Version="0.34.0" />
<!-- 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. -->
<PackageVersion Include="MessagePack" Version="2.5.187" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.0" />
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2739.15" />
<!-- 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. -->
@@ -57,6 +60,7 @@
<PackageVersion Include="NLog" Version="5.0.4" />
<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="ReverseMarkdown" Version="4.1.0" />
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
<PackageVersion Include="SharpCompress" Version="0.37.2" />

View File

@@ -1297,7 +1297,7 @@ EXHIBIT A -Mozilla Public License.
## NuGet Packages used by PowerToys
- Appium.WebDriver 4.4.5
- Azure.AI.OpenAI 1.0.0-beta.12
- Azure.AI.OpenAI 1.0.0-beta.17
- CommunityToolkit.Mvvm 8.2.2
- CommunityToolkit.WinUI.Animations 8.0.240109
- CommunityToolkit.WinUI.Collections 8.0.240109
@@ -1315,9 +1315,10 @@ EXHIBIT A -Mozilla Public License.
- hyjiacan.pinyin4net 4.1.1
- Interop.Microsoft.Office.Interop.OneNote 1.1.0.2
- LazyCache 2.4.0
- Mages 2.0.2
- Mages 3.0.0
- Markdig.Signed 0.34.0
- MessagePack 2.5.187
- Microsoft.Bcl.AsyncInterfaces 9.0.0
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
- Microsoft.Data.Sqlite 9.0.0
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
@@ -1327,6 +1328,7 @@ EXHIBIT A -Mozilla Public License.
- Microsoft.Extensions.Logging 9.0.0
- Microsoft.Extensions.Logging.Abstractions 9.0.0
- Microsoft.NET.ILLink.Tasks (A)
- Microsoft.SemanticKernel 1.15.0
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
- Microsoft.Web.WebView2 1.0.2739.15
- Microsoft.Win32.SystemEvents 9.0.0
@@ -1342,6 +1344,7 @@ EXHIBIT A -Mozilla Public License.
- MSTest 3.6.3
- NLog.Extensions.Logging 5.3.8
- NLog.Schema 5.2.8
- OpenAI 2.0.0
- ReverseMarkdown 4.1.0
- ScipBe.Common.Office.OneNote 3.0.1
- SharpCompress 0.37.2

View File

@@ -179,7 +179,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.targets = Directory.Build.targets
Directory.Packages.props = Directory.Packages.props
src\Monaco.props = src\Monaco.props
Solution.props = Solution.props
src\Solution.props = src\Solution.props
src\Version.props = src\Version.props
EndProjectSection
EndProject
@@ -635,6 +635,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesCsharpLibrary", "
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NewPlus.ShellExtension.win10", "src\modules\NewPlus\NewShellExtensionContextMenu.win10\NewPlus.ShellExtension.win10.vcxproj", "{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.UnitTests", "src\modules\AdvancedPaste\AdvancedPaste.UnitTests\AdvancedPaste.UnitTests.csproj", "{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.FuzzTests", "src\modules\AdvancedPaste\AdvancedPaste.FuzzTests\AdvancedPaste.FuzzTests.csproj", "{7F5B9557-5878-4438-A721-3E28296BA193}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2807,6 +2811,30 @@ Global
{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x64.Build.0 = Release|x64
{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x86.ActiveCfg = Release|x64
{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x86.Build.0 = Release|x64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|ARM64.ActiveCfg = Debug|ARM64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|ARM64.Build.0 = Debug|ARM64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|x64.ActiveCfg = Debug|x64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|x64.Build.0 = Debug|x64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|x86.ActiveCfg = Debug|x64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|x86.Build.0 = Debug|x64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|ARM64.ActiveCfg = Release|ARM64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|ARM64.Build.0 = Release|ARM64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x64.ActiveCfg = Release|x64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x64.Build.0 = Release|x64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x86.ActiveCfg = Release|x64
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x86.Build.0 = Release|x64
{7F5B9557-5878-4438-A721-3E28296BA193}.Debug|ARM64.ActiveCfg = Debug|ARM64
{7F5B9557-5878-4438-A721-3E28296BA193}.Debug|ARM64.Build.0 = Debug|ARM64
{7F5B9557-5878-4438-A721-3E28296BA193}.Debug|x64.ActiveCfg = Debug|x64
{7F5B9557-5878-4438-A721-3E28296BA193}.Debug|x64.Build.0 = Debug|x64
{7F5B9557-5878-4438-A721-3E28296BA193}.Debug|x86.ActiveCfg = Debug|x64
{7F5B9557-5878-4438-A721-3E28296BA193}.Debug|x86.Build.0 = Debug|x64
{7F5B9557-5878-4438-A721-3E28296BA193}.Release|ARM64.ActiveCfg = Release|ARM64
{7F5B9557-5878-4438-A721-3E28296BA193}.Release|ARM64.Build.0 = Release|ARM64
{7F5B9557-5878-4438-A721-3E28296BA193}.Release|x64.ActiveCfg = Release|x64
{7F5B9557-5878-4438-A721-3E28296BA193}.Release|x64.Build.0 = Release|x64
{7F5B9557-5878-4438-A721-3E28296BA193}.Release|x86.ActiveCfg = Release|x64
{7F5B9557-5878-4438-A721-3E28296BA193}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3040,6 +3068,8 @@ Global
{66614C26-314C-4B91-9071-76133422CFEF} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC}
{89D0E199-B17A-418C-B2F8-7375B6708357} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E} = {CA716AE6-FE5C-40AC-BB8F-2C87912687AC}
{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE} = {9873BA05-4C41-4819-9283-CF45D795431B}
{7F5B9557-5878-4438-A721-3E28296BA193} = {9873BA05-4C41-4819-9283-CF45D795431B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

158
README.md
View File

@@ -1,6 +1,6 @@
# Microsoft PowerToys
![Hero image for Microsoft PowerToys](doc/images/overview/PT_holiday_hero_image.png)
![Hero image for Microsoft PowerToys](doc/images/overview/PT_hero_image.png)
[How to use PowerToys][usingPowerToys-docs-link] | [Downloads & Release notes][github-release-link] | [Contributing to PowerToys](#contributing) | [What's Happening](#whats-happening) | [Roadmap](#powertoys-roadmap)
@@ -20,12 +20,6 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
| [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
| [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) | [Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
## 🎁⭐ PowerToys Advent calendar ⭐🎁
We will be highlighting a cool utility each day for 24 days in December! To follow along, check out these threads:
- https://bsky.app/profile/kaylacinnamon.bsky.social/post/3lcb7iljxck2o
- https://x.com/cinnamon_msft/status/1863284610773246257
## Installing and running Microsoft PowerToys
### Requirements
@@ -40,19 +34,19 @@ We will be highlighting a cool utility each day for 24 days in December! To foll
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.87%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.86%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.86.0/PowerToysUserSetup-0.86.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.86.0/PowerToysUserSetup-0.86.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.86.0/PowerToysSetup-0.86.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.86.0/PowerToysSetup-0.86.0-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.88%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.87%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.87.1/PowerToysUserSetup-0.87.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.87.1/PowerToysUserSetup-0.87.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.87.1/PowerToysSetup-0.87.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.87.1/PowerToysSetup-0.87.1-arm64.exe
| Description | Filename | sha256 hash |
|----------------|----------|-------------|
| Per user - x64 | [PowerToysUserSetup-0.86.0-x64.exe][ptUserX64] | CFB9608B28B8FF12C9A7C9814A6EF981636EB5AB261DC278C28EC93FD959CCE2 |
| Per user - ARM64 | [PowerToysUserSetup-0.86.0-arm64.exe][ptUserArm64] | 861CEDBFDCDA993D1D1056E3280319D5EA45D142CA3C737AB1FB4FABD651A5F5 |
| Machine wide - x64 | [PowerToysSetup-0.86.0-x64.exe][ptMachineX64] | 857DE9DC5938D9602F82DFD6183DB5E6823B875A412AEC59B4BE93617E27E9CD |
| Machine wide - ARM64 | [PowerToysSetup-0.86.0-arm64.exe][ptMachineArm64] | 6F37192534C195A02A80AAE1E449DF61C894C50763096A06195581801943FA31 |
| Per user - x64 | [PowerToysUserSetup-0.87.1-x64.exe][ptUserX64] | 8EFAF47ED00BF230D2C2CC3CB6765C903A6A47E0AAED0BBB329CEF918207B486 |
| Per user - ARM64 | [PowerToysUserSetup-0.87.1-arm64.exe][ptUserArm64] | 212FC8055789BD2DC4DE554B9AEE291A9C077907E263A302939266263A9D512B |
| Machine wide - x64 | [PowerToysSetup-0.87.1-x64.exe][ptMachineX64] | 69AD65DDAC6436AEF292D2CC6AB1530021CE98083CB3F5FD3380A52A3B0DBB9A |
| Machine wide - ARM64 | [PowerToysSetup-0.87.1-arm64.exe][ptMachineArm64] | AEC9F1D02F1E23F0C1FCFDF95C337C962902394F44C0568012DF78BEDB45CF19 |
This is our preferred method.
@@ -98,103 +92,115 @@ For guidance on developing for PowerToys, please read the [developer docs](/doc/
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
### 0.86 - October 2024 Update
### 0.87 - December 2024 Update
In this release, we focused on new features, stability, and improvements.
**Highlights**
- Advanced Paste has new abilities: Image to text, and paste to file (text / png / html).
- In settings, we've adjusted the left navigation to group the utilities. As the number of utilities shipped with PowerToys keeps growing, we felt this was a needed adjustment. Thanks everyone for your feedback!
- Workspaces received many bug fixes, including the proper launching of many instances of the same application in the same workspace. Note, we are still actively looking at how to properly handle PWA detection.
- We've added a diagnostic data (telemetry) opt-in option in the Settings General tab. As it is off-by-default, we encourage users to turn it on as that helps direct our development efforts and their journeys. More information about the data we collect can be found in the [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation) and what each event does.
### General
- Added a setting for diagnostic data (telemetry) opt-in (off by default, however, see above for why we encourage you to opt-in!) and user controls to view data.
- Improved exception logging by adding the type of Exception and InnerException. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Advanced Paste has a new feature called "Advanced AI" that uses Semantic Kernel to allow setting up the orchestration of sequential clipboard transformations.
- Workspaces supports Progressive Web Applications.
- Workspaces has a new feature to move existing windows instead of creating new ones.
- Mouse Jump added new settings to allow customization of screens pop-up. Thanks [@mikeclayton](https://github.com/mikeclayton)!
- New+ now works on Windows 10. Thanks [@cgaarden](https://github.com/cgaarden)!
- Quick Accent allows selecting the character sets that should appear on the UI. Thanks [@Sirozha1337](https://github.com/Sirozha1337)!
### Advanced Paste
- Added new built-in actions: Image to text, and paste txt, png or html as a file.
- Added a new optional feature allowing using AI to set up the orchestration of sequential clipboard transformations.
### Awake
- Initialization, logging and tray icon setup improvements. Thanks [@dend](https://github.com/dend)!
### File Explorer add-ons
- Preview Pane extensions now use the PerMonitorV2 DPI mode to fix errors on different scales. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
### Keyboard Manager.
- Added labels to the IME On, IME Off keys. Thanks [@kit494way](https://github.com/kit494way)!
- Fixed an issue that caused the Shift key to remain stuck if a numpad key was mapped to the Shift key.
### Monaco Preview
- Added support for .ahk files to be shown as a plaintext file in Peek and File Explorer add-ons. Thanks [@daverayment](https://github.com/daverayment)!
- Added support for .ion files to be shown as a plaintext file in Peek and File Explorer add-ons. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
- Added support for syntax highlighting for .srt files in Peek and File Explorer add-ons. Thanks [@PesBandi](https://github.com/PesBandi)!
### Mouse Jump
- Refactored the common classes into a separate project. Thanks [@mikeclayton](https://github.com/mikeclayton)!
- Brought back the telemetry events that were deleted across previous refactoring efforts.
### Mouse Without Borders
- Refactored the Logger common classes. Thanks [@mikeclayton](https://github.com/mikeclayton)!
- Allow customizing the appearance of the UI of the Mouse Jump pop-up. Thanks [@mikeclayton](https://github.com/mikeclayton)!
### New+
- Fixed the telemetry event for when the modules is enabled or disabled. (This was a hotfix for 0.85)
- Fixed bug when creating folders or files that contain Unicode characters. Thanks [@cgaarden](https://github.com/cgaarden)!
- Fixed bug when the name of a new folder collided with an already existing folder. Thanks [@cgaarden](https://github.com/cgaarden)!
- Updated the New+ icons to the fluent style.
- Added support for Windows 10. Thanks [@cgaarden](https://github.com/cgaarden)!
- Fixed an issue causing the renaming of new files to not trigger some times. Thanks [@cgaarden](https://github.com/cgaarden)!
- Updated the New+ icons. Thanks [@niels9001](https://github.com/niels9001)!
### Peek
- Folder preview enumeration of size and number of files is now more responsive and faster. Thanks [@daverayment](https://github.com/daverayment)!
- Peek now checks local capabilities to decide what image formats Image Previewer is able to support. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed an issue causing the Code Files Previewer to not load correctly under certain conditions. Thanks [@daverayment](https://github.com/daverayment)!
- Refactored, improved and fixed logging when loading the user settings file. Thanks [@daverayment](https://github.com/daverayment)!
### PowerToys Run
- Handled a culture not found error when checking for right-to-left languages.
- Fixed the WebSearch plugin results title being trimmed in the UI. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
- The Unit Converter plugin will now show more significant digits. Thanks [@PesBandi](https://github.com/PesBandi)!
- Improved error handling when copying to the clipboard results in an error. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added a scoring function for proper ordering of the WindowWalker plugin results. Thanks [@andbartol](https://github.com/andbartol)!
- Added UUIDv7 support to the ValueGenerator plugin. Thanks [@frederik-hoeft](https://github.com/frederik-hoeft)!
- The calculator plugin now allows scientific notation numbers with a lowercase 'e'. Thanks [@PesBandi](https://github.com/PesBandi)!
- Ported the UI from WPF-UI to .NET 9 WPF, to fix "Desktop composition is disabled" crashes.
### Quick Accent
- Added support for the Serbian Cyrillic character set. Thanks [@Sirozha1337](https://github.com/Sirozha1337)!
- Added a setting to allow selecting which character sets to show. Thanks [@Sirozha1337](https://github.com/Sirozha1337)!
### Registry Preview
### Screen Ruler
- Adopted the Monaco Editor as the UI text editor. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added a Setting to also allow showing measurements in inches, centimeters or millimeters. Thanks [@Sophanatprime](https://github.com/Sophanatprime)!
### Settings
- Fixed a crash when trying to access a non-existing templates folder from the New+ page. (This was a hotfix for 0.85)
- Added a navigation tree to group utilities in the left navigation menu.
- Sorted the list of languages in the language selection combo box in the General tab. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed the state of the info bar about templates not being backed up to not close and react to the module's enabled state in the New+ page. Thanks [@htcfreek](https://github.com/htcfreek)!
- Fixed a crash caused by a dangling thread.
- Clicking a notification about there being an update available should now correctly open the Settings application in the General tab.
- Fixed a UI freeze when trying to access the Diagnostic Data Viewer files. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed an issue causing all the links to milestones in the "What's new?" OOBE page to point to the same milestone.
- Removed extra space from the Welcome page. Thanks [@agarwalishita](https://github.com/agarwalishita)!
- Updated left navigation bar icons. Thanks [@niels9001](https://github.com/niels9001)!
- Fixed accessibility issues in the dashboard page. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
### Workspaces
- Fixed launching the incorrect workspace when launching many workspaces quickly through shortcuts. (This was a hotfix for 0.85)
- Fixed launching many instances of the same application in a workspace.
- Fixed a crash when a previously captured monitor ID no longer existed.
- Fixed an issue causing the wrong coordinates to be saved for minimized applications.
- Fixed an issue causing a crash when stress testing workspace launching.
- Fixed application launching when UAC is off and every application always runs elevated.
- Added support for Progressive Web Applications to Workspaces.
- Implemented a feature to move existing windows instead of creating new ones.
- Fixed a crash when opening the workspaces editor that was caused by passing incorrect encoder parameters when saving Bitmap files.
- Workspaces editor position is now saved so that we can start it at the same position when we open it again.
- Fixed an issue causing many instances of the same application to be put in the same position instead of the intended position due to timer issues.
- Fixed detection of exact application version when many versions of the same application are installed.
### Documentation
- Added HackMD plugin mention to thirdPartyRunPlugins.md. Thanks [@8LWXpg](https://github.com/8LWXpg)!
- Added SSH plugin mention to thirdPartyRunPlugins.md. Thanks [@8LWXpg](https://github.com/8LWXpg)!
- Added the [Data and Privacy documentation](https://github.com/microsoft/PowerToys/blob/main/DATA_AND_PRIVACY.md) to the repo.
- Improved language in CONTRIBUTE.md. Thanks [@sanskaarz](https://github.com/sanskaarz)!
- Added Bilibili plugin mention to thirdPartyRunPlugins.md. Thanks [@Whuihuan](https://github.com/Whuihuan)!
- Added CanIUse and TailwindCSS plugins mention to thirdPartyRunPlugins.md. Thanks [@skttl](https://github.com/skttl)!
- Added HttpStatusCodes plugin mention to thirdPartyRunPlugins.md. Thanks [@grzhan](https://github.com/grzhan)!
- Updated COMMUNITY.md with more contributors.
### Development
- Fixed the CI precheck action to take into account the recent changes in CI actions.
- Added the new Microsoft org issue types to the issue templates. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Updated System.Text.Json to 8.0.5 and System.Runtime.Caching to 8.0.1 and related dependencies to the latest to address security reports. Thanks [@snickler](https://github.com/snickler)!
- Updated WinAppSDK to 1.6.1 and CsWinRT to 2.1.5. Thanks [@snickler](https://github.com/snickler)!
- Upgraded the WpfUI dependency to 3.0.5.
- Updated MessagePack to 2.5.187 and StreamJsonRpc to 2.19.27 to address security reports.
- Removed some of the hacks that are no longer needed that tried to force same dependency versions in .csproj files.
- Removed the Markdown file exclusions from the conditions that trigger a full CI test.
- CI fails again when there are XAML style errors in a PR.
- Fixed CI actions that were not failing when one of the powershell scripts they tried to run was failing.
- Fixed analyzer violations to allow fully building PowerToys on Visual Studio 17.12. Thanks [@snickler](https://github.com/snickler)!
- Upgraded to .NET 9. Thanks [@snickler](https://github.com/snickler)!
- Fixed building on Visual Studio 17.12.
- Upgraded the System.IO.Abstractions dependency to 21.0.29. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Upgraded the WindowsAppSDK dependency to 1.6.241114003. Thanks [@shuaiyuanxx](https://github.com/shuaiyuanxx)!
- Upgraded the MSTest dependency to 3.6.3. Thanks [@Youssef1313](https://github.com/Youssef1313)!
- Upgraded the check-spelling CI dependency to 0.0.24 and fixed related spell checking issues. Thanks [@jsoref](https://github.com/jsoref)!
- Removed duplicate names from the spellcheck allowed names file. Thanks [@htcfreek](https://github.com/htcfreek)!
- Improved logging of asynchronous methods call stacks when logging an error.
- Created a MSBuild props file to be imported by other projects to enable AOT support.
- Made the Peek utility source code AOT compatible.
- Updated .editorconfig rules to relax squiggly IDE errors in Visual Studio 17.12. Thanks [@snickler](https://github.com/snickler)!
- Moved Xaml.Styler from the root to the src folder.
#### What is being planned for version 0.87
#### What is being planned for version 0.88
For [v0.87][github-next-release-work], we'll work on the items below:
For [v0.88][github-next-release-work], we'll work on the items below:
- Stability / bug fixes
- New module: File Actions Menu

View File

@@ -16,7 +16,7 @@
"Author": string,
"Version": "1.0.0", // For future compatibility
"Language": "csharp", // So far we support only csharp
"Website": "https://aka.ms/powertoys",
"Website": "https://aka.ms/powertoys", // Has to be an absolute uri starting with "http://" or "https://".
"ExecuteFileName": string, // Should be {Type}.PowerToys.Run.Plugin.{PluginName}.dll
"IcoPathDark": string, // Path to dark theme icon. The path is relative to the root plugin folder
"IcoPathLight": string // Path to light theme icon. The path is relative to the root plugin folder
@@ -42,3 +42,4 @@ In the PR that adds a new plugin, reference a new issue to track the work for fu
- [ ] Add the resource folder to https://github.com/microsoft/PowerToys/blob/21247c0bb09a1bee3d14d6efa53d0c247f7236af/installer/PowerToysSetup/Product.wxs#L825
- [ ] Add the resource files under the section https://github.com/microsoft/PowerToys/blob/21247c0bb09a1bee3d14d6efa53d0c247f7236af/installer/PowerToysSetup/Product.wxs#L882
- [ ] Your plugin's executable file (DLL) has to have correct version informations after building it. (This version information will be shown on the settings page.)

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 503 KiB

View File

@@ -7,7 +7,7 @@ import { srtDefinition } from './customLanguages/srt.js';
export async function registerAdditionalLanguages(monaco){
await languageDefinitions();
registerAdditionalLanguage("cppExt", [".ino", ".pde"], "cpp", monaco);
registerAdditionalLanguage("xmlExt", [".wsdl", ".csproj", ".vcxproj", ".vbproj", ".fsproj"], "xml", monaco);
registerAdditionalLanguage("xmlExt", [".wsdl", ".csproj", ".vcxproj", ".vbproj", ".fsproj", ".resx", ".resw"], "xml", monaco);
registerAdditionalLanguage("txtExt", [".sln", ".log", ".vsconfig", ".env", ".ahk", ".ion"], "txt", monaco);
registerAdditionalLanguage("razorExt", [".razor"], "razor", monaco);
registerAdditionalLanguage("vbExt", [".vbs"], "vb", monaco);

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\Monaco.props" />
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<Description>PowerToys FilePreviewCommon</Description>

View 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.
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.FilePreviewCommon.Monaco.Formatters;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(JsonDocument))]
internal sealed partial class FilePreviewJsonSerializerContext : JsonSerializerContext
{
}

View File

@@ -14,7 +14,6 @@ namespace Microsoft.PowerToys.FilePreviewCommon.Monaco.Formatters
private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
@@ -28,7 +27,8 @@ namespace Microsoft.PowerToys.FilePreviewCommon.Monaco.Formatters
using (var jDocument = JsonDocument.Parse(value, new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip }))
{
return JsonSerializer.Serialize(jDocument, _serializerOptions);
FilePreviewJsonSerializerContext context = new(_serializerOptions);
return JsonSerializer.Serialize(jDocument, context.JsonDocument);
}
}
}

View File

@@ -6,9 +6,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text.Json;
using Microsoft.PowerToys.FilePreviewCommon.Monaco.Formatters;
namespace Microsoft.PowerToys.FilePreviewCommon
@@ -38,15 +36,15 @@ namespace Microsoft.PowerToys.FilePreviewCommon
private static string GetRuntimeMonacoDirectory()
{
string exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
string baseDirectory = AppContext.BaseDirectory ?? string.Empty;
// If the executable is within "WinUI3Apps", correct the path first.
if (Path.GetFileName(exePath) == "WinUI3Apps")
if (Path.GetFileName(baseDirectory) == "WinUI3Apps")
{
exePath = Path.Combine(exePath, "..");
baseDirectory = Path.Combine(baseDirectory, "..");
}
string monacoPath = Path.Combine(exePath, "Assets", "Monaco");
string monacoPath = Path.Combine(baseDirectory, "Assets", "Monaco");
return Directory.Exists(monacoPath) ?
monacoPath :

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\AdvancedPaste.FuzzTests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\AdvancedPaste\Helpers\JsonHelper.cs" Link="JsonHelper.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>
<ItemGroup>
<Content Include="OneFuzzConfig.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,45 @@
# Fuzzing .NET Code with OneFuzz
This document explains the purpose of the project, the rationale for using specific technologies, and key instructions for fuzz testing .NET code using OneFuzz.
## Overview
This project demonstrates fuzz testing for .NET applications. It uses a `.NET 8 (Windows)` project where a code file is linked to the project. The linked file contains the functions required for fuzz testing.
## Why Use .NET 8 (Windows)?
1. **Current Support**: At the time of writing, OneFuzz supports only .NET 8 projects. The Fuzz team is actively working on .NET 9 support.
2. **Interim Solution**: Until .NET 9 support is available, .NET 8 serves as a robust and temporary solution for fuzz testing, enabling direct code linking for efficient development.
## Requesting Access
To log into the production instance of OneFuzz with the CLI, you **must request access**. Visit the internal [OneFuzz Access Request Page](https://myaccess.microsoft.com/@microsoft.onmicrosoft.com#/access-packages/6df691eb-e3d1-444b-b4b2-9e944dc794be) for details.
## How to Fuzz .NET Code
To set up and run fuzz testing on .NET code, follow the detailed guide available [Fuzz .NET Code](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/howto/fuzzing-dotnet-code).
## Running a .NET Fuzz Target Locally
Testing a .NET fuzz target locally requires specific configurations. For a step-by-step guide, see the section on [Running a .NET Fuzz Target Locally](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/howto/fuzzing-dotnet-code#extra-running-a-net-fuzz-target-locally).
## Writing a Good OneFuzzConfig.json
The `OneFuzzConfig.json` file provides critical information for deploying fuzzing jobs using the OneFuzz Ingestion Preparation Tool and Ingestion Service.
### Structure
The primary structure is an array of configuration entries. Outside the array, the `configVersion` field is used to track changes to the configuration schema.
For more details on how to write and structure this file, see the [OneFuzzConfig V3 Documentation](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/onefuzzconfig/onefuzzconfigv3).
## Tools
### OneFuzz Ingestion Preparation (OIP) Tool
The OIP tool helps prepare data for ingestion and fuzz testing. Learn more about [OneFuzz Ingestion Preparation (OIP) Tool](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/oip/onefuzzingestionpreparationtool).
### OneFuzz CLI
The CLI provides commands to manage and execute fuzzing jobs. Download and set up the CLI by following this [guide](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/howto/downloading-cli).

View 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 AdvancedPaste.Helpers;
using Windows.ApplicationModel.DataTransfer;
// OneFuzz currently does not support .NET 9 code testing, so this is a temporary solution.
// Create a .NET 8 project and use a file link to include the code for testing first.
namespace AdvancedPaste.FuzzTests
{
public class FuzzTests
{
public static void FuzzToJsonFromXmlOrCsv(ReadOnlySpan<byte> input)
{
try
{
var dataPackage = new DataPackage();
dataPackage.SetText(input.ToString());
_ = Task.Run(async () => await JsonHelper.ToJsonFromXmlOrCsvAsync(dataPackage.GetView())).Result;
}
catch (Exception ex) when (ex is ArgumentException)
{
// This is an example. It's important to filter out any *expected* exceptions from our code here.
// However, catching all exceptions is considered an anti-pattern because it may suppress legitimate
// issues, such as a NullReferenceException thrown by our code. In this case, we still re-throw
// the exception, as the ToJsonFromXmlOrCsvAsync method is not expected to throw any exceptions.
throw;
}
}
}
}

View File

@@ -0,0 +1,46 @@
// 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.Text;
using System.Threading.Tasks;
// This is used for fuzz testing and ensures that the project links only to JsonHelper,
// avoiding unnecessary connections to additional files
namespace ManagedCommon
{
public static class Logger
{
// An empty method to simulate logging information
public static void LogTrace()
{
// Do nothing
}
// An empty method to simulate logging information
public static void LogInfo(string message)
{
// Do nothing
}
// An empty method to simulate logging warnings
public static void LogWarning(string message)
{
// Do nothing
}
// An empty method to simulate logging errors
public static void LogError(string message, Exception? ex = null)
{
// Do nothing
}
public static void LogDebug(string message, Exception? ex = null)
{
// Do nothing
}
}
}

View File

@@ -0,0 +1,5 @@
// 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.
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]

View File

@@ -0,0 +1,47 @@
{
"configVersion": 3,
"entries": [
{
"fuzzer": {
"$type": "libfuzzerDotNet",
"dll": "AdvancedPaste.FuzzTests.dll",
"class": "AdvancedPaste.FuzzTests.FuzzTests",
"method": "FuzzToJsonFromXmlOrCsv",
"FuzzingTargetBinaries": [
"PowerToys.AdvancedPaste.dll"
]
},
"adoTemplate": {
// supply the values appropriate to your
// project, where bugs will be filed
"org": "microsoft",
"project": "OS",
"AssignedTo": "leilzh@microsoft.com",
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
"IterationPath": "OS\\Future"
},
"jobNotificationEmail": "leilzh@microsoft.com",
"skip": false,
"rebootAfterSetup": false,
"oneFuzzJobs": [
// at least one job is required
{
"projectName": "AdvancedPaste",
"targetName": "AdvancedPaste-dotnet-fuzzer"
}
],
"jobDependencies": [
// this should contain, at minimum,
// the DLL and PDB files
// you will need to add any other files required
// (globs are supported)
"AdvancedPaste.FuzzTests.dll",
"AdvancedPaste.FuzzTests.pdb",
"Microsoft.Windows.SDK.NET.dll",
"Newtonsoft.Json.dll",
"WinRT.Runtime.dll"
],
"SdlWorkItemId": 49911822
}
]
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\AdvancedPaste.UnitTests\</OutputPath>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Content Remove="Assets\image_with_text_example.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\image_with_text_example.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AdvancedPaste\AdvancedPaste.csproj" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -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.Threading.Tasks;
using AdvancedPaste.Models.KernelQueryCache;
using AdvancedPaste.Services;
namespace AdvancedPaste.UnitTests.Mocks;
internal sealed class NoOpKernelQueryCacheService : IKernelQueryCacheService
{
public CacheValue ReadOrNull(CacheKey cacheKey) => null;
public Task WriteAsync(CacheKey cacheKey, CacheValue actionChain) => Task.CompletedTask;
}

View File

@@ -0,0 +1,150 @@
// 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.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Services.OpenAI;
using AdvancedPaste.UnitTests.Mocks;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.UnitTests.ServicesTests;
[Ignore("Test requires active OpenAI API key.")] // Comment out this line to run these tests after setting up OpenAI API key using AdvancedPaste Settings
[TestClass]
/// <summary>
/// Tests that write batch AI outputs against a list of inputs. Connects to OpenAI and uses the full AdvancedPaste action catalog for Semantic Kernel.
/// If queries produce errors, the error message is written to the output file. If queries produce text-file output, their contents are included as though they were text output.
/// To run this test-suite, first:
/// 1. Setup an OpenAI API key using AdvancedPaste Settings.
/// 2. Comment out the [Ignore] attribute above.
/// 3. Ensure the %USERPROFILE% folder contains the required input files (paths are below).
/// These tests are idempotent and resumable, allowing for partial runs and restarts. It's ok to use existing output files as input files - output-related fields will simply be ignored.
/// </summary>
public sealed class AIServiceBatchIntegrationTests
{
private record class BatchTestInput
{
public string Prompt { get; init; }
public string Clipboard { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Genre { get; init; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Category { get; init; }
}
private sealed record class BatchTestResult : BatchTestInput
{
[JsonPropertyOrder(1)]
public string Result { get; init; }
internal BatchTestInput ToInput() => new() { Prompt = Prompt, Clipboard = Clipboard, Genre = Genre, Category = Category, };
}
private const string AllTestsFilePath = @"%USERPROFILE%\allAdvancedPasteTests-Input-V2.json";
private const string FailedTestsFilePath = @"%USERPROFILE%\advanced-paste-failed-tests-only.json";
private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true };
[TestMethod]
[DataRow(AllTestsFilePath, PasteFormats.CustomTextTransformation)]
[DataRow(AllTestsFilePath, PasteFormats.KernelQuery)]
[DataRow(FailedTestsFilePath, PasteFormats.CustomTextTransformation)]
[DataRow(FailedTestsFilePath, PasteFormats.KernelQuery)]
public async Task TestGenerateBatchResults(string inputFilePath, PasteFormats format)
{
// Load input data.
var fullInputFilePath = Environment.ExpandEnvironmentVariables(inputFilePath);
var inputs = await GetDataListAsync<BatchTestInput>(fullInputFilePath);
Assert.IsTrue(inputs.Count > 0);
// Load existing results; allow a partial run to be resumed.
var resultsFile = Path.Combine(Path.GetDirectoryName(fullInputFilePath), $"{Path.GetFileNameWithoutExtension(fullInputFilePath)}-output-{format}.json");
var results = await GetDataListAsync<BatchTestResult>(resultsFile);
Assert.IsTrue(results.Count <= inputs.Count);
CollectionAssert.AreEqual(results.Select(result => result.ToInput()).ToList(), inputs.Take(results.Count).ToList());
async Task WriteResultsAsync() => await File.WriteAllTextAsync(resultsFile, JsonSerializer.Serialize(results, SerializerOptions));
Logger.LogInfo($"Starting {nameof(TestGenerateBatchResults)}; Count={inputs.Count}, InCache={results.Count}");
// Produce results for any unprocessed inputs.
foreach (var input in inputs.Skip(results.Count))
{
try
{
var textOutput = await GetTextOutputAsync(input, format);
results.Add(new() { Prompt = input.Prompt, Clipboard = input.Clipboard, Genre = input.Genre, Category = input.Category, Result = textOutput, });
}
catch (Exception)
{
await WriteResultsAsync();
throw;
}
}
await WriteResultsAsync();
}
private static async Task<List<T>> GetDataListAsync<T>(string filePath) =>
File.Exists(filePath) ? JsonSerializer.Deserialize<List<T>>(await File.ReadAllTextAsync(filePath)) : [];
private static async Task<string> GetTextOutputAsync(BatchTestInput input, PasteFormats format)
{
try
{
var outputPackage = (await GetOutputDataPackageAsync(input, format)).GetView();
var outputFormat = await outputPackage.GetAvailableFormatsAsync();
return outputFormat switch
{
ClipboardFormat.Text => await outputPackage.GetTextOrEmptyAsync(),
ClipboardFormat.File => await File.ReadAllTextAsync((await outputPackage.GetStorageItemsAsync()).Single().Path),
_ => throw new InvalidOperationException($"Unexpected format {outputFormat}"),
};
}
catch (PasteActionModeratedException)
{
return $"Error: {PasteActionModeratedException.ErrorDescription}";
}
catch (PasteActionException ex) when (!string.IsNullOrEmpty(ex.AIServiceMessage))
{
return $"Error: {ex.AIServiceMessage}";
}
}
private static async Task<DataPackage> GetOutputDataPackageAsync(BatchTestInput batchTestInput, PasteFormats format)
{
VaultCredentialsProvider credentialsProvider = new();
PromptModerationService promptModerationService = new(credentialsProvider);
CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
switch (format)
{
case PasteFormats.CustomTextTransformation:
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard));
case PasteFormats.KernelQuery:
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false);
default:
throw new InvalidOperationException($"Unexpected format {format}");
}
}
}

View File

@@ -0,0 +1,172 @@
// 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.Abstractions.TestingHelpers;
using System.Linq;
using System.Threading.Tasks;
using AdvancedPaste.Models;
using AdvancedPaste.Models.KernelQueryCache;
using AdvancedPaste.Services;
using AdvancedPaste.Settings;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace AdvancedPaste.UnitTests.ServicesTests;
[TestClass]
public sealed class CustomActionKernelQueryCacheServiceTests
{
private static readonly CacheKey CustomActionTestKey = new() { Prompt = "TestPrompt1", AvailableFormats = ClipboardFormat.Text };
private static readonly CacheKey CustomActionTestKey2 = new() { Prompt = "TestPrompt2", AvailableFormats = ClipboardFormat.File | ClipboardFormat.Image };
private static readonly CacheKey MarkdownTestKey = new() { Prompt = "Paste as Markdown", AvailableFormats = ClipboardFormat.Text };
private static readonly CacheKey JSONTestKey = new() { Prompt = "Paste as JSON", AvailableFormats = ClipboardFormat.Text };
private static readonly CacheKey PasteAsTxtFileKey = new() { Prompt = "Paste as .txt file", AvailableFormats = ClipboardFormat.File };
private static readonly CacheKey PasteAsPngFileKey = new() { Prompt = "Paste as .png file", AvailableFormats = ClipboardFormat.Image };
private static readonly CacheValue TestValue = new([new(PasteFormats.PlainText, [])]);
private static readonly CacheValue TestValue2 = new([new(PasteFormats.KernelQuery, new() { { "a", "b" }, { "c", "d" } })]);
private CustomActionKernelQueryCacheService _cacheService;
private Mock<IUserSettings> _userSettings;
private MockFileSystem _fileSystem;
[TestInitialize]
public void TestInitialize()
{
_userSettings = new();
UpdateUserActions([], []);
_fileSystem = new();
_cacheService = new(_userSettings.Object, _fileSystem);
}
[TestMethod]
public async Task Test_Cache_Always_Accepts_Core_Action_Prompt()
{
await AssertAcceptsAsync(MarkdownTestKey);
}
[TestMethod]
public async Task Test_Cache_Accepts_Prompt_When_Custom_Action()
{
await AssertRejectsAsync(CustomActionTestKey);
UpdateUserActions([], [new() { Name = nameof(CustomActionTestKey), Prompt = CustomActionTestKey.Prompt, IsShown = true }]);
await AssertAcceptsAsync(CustomActionTestKey);
await AssertRejectsAsync(CustomActionTestKey2, PasteAsTxtFileKey);
UpdateUserActions([], []);
await AssertRejectsAsync(CustomActionTestKey);
}
[TestMethod]
public async Task Test_Cache_Accepts_Prompt_When_User_Additional_Action()
{
await AssertRejectsAsync(PasteAsTxtFileKey, PasteAsPngFileKey);
UpdateUserActions([PasteFormats.PasteAsHtmlFile, PasteFormats.PasteAsTxtFile], []);
await AssertAcceptsAsync(PasteAsTxtFileKey);
await AssertRejectsAsync(PasteAsPngFileKey, CustomActionTestKey);
UpdateUserActions([], []);
await AssertRejectsAsync(PasteAsTxtFileKey);
}
[TestMethod]
public async Task Test_Cache_Overwrites_Latest_Value()
{
await _cacheService.WriteAsync(JSONTestKey, TestValue);
await _cacheService.WriteAsync(MarkdownTestKey, TestValue2);
await _cacheService.WriteAsync(JSONTestKey, TestValue2);
await _cacheService.WriteAsync(MarkdownTestKey, TestValue);
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(JSONTestKey));
AssertAreEqual(TestValue, _cacheService.ReadOrNull(MarkdownTestKey));
}
[TestMethod]
public async Task Test_Cache_Uses_Case_Insensitive_Prompt_Comparison()
{
static CacheKey CreateUpperCaseKey(CacheKey key) =>
new() { Prompt = key.Prompt.ToUpperInvariant(), AvailableFormats = key.AvailableFormats };
await _cacheService.WriteAsync(CreateUpperCaseKey(JSONTestKey), TestValue);
await _cacheService.WriteAsync(MarkdownTestKey, TestValue2);
AssertAreEqual(TestValue, _cacheService.ReadOrNull(JSONTestKey));
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(MarkdownTestKey));
}
[TestMethod]
public async Task Test_Cache_Uses_Clipboard_Formats_In_Key()
{
CacheKey key1 = new() { Prompt = JSONTestKey.Prompt, AvailableFormats = ClipboardFormat.File };
CacheKey key2 = new() { Prompt = JSONTestKey.Prompt, AvailableFormats = ClipboardFormat.Image };
await _cacheService.WriteAsync(key1, TestValue);
Assert.IsNotNull(_cacheService.ReadOrNull(key1));
Assert.IsNull(_cacheService.ReadOrNull(key2));
}
[TestMethod]
public async Task Test_Cache_Is_Persistent()
{
await _cacheService.WriteAsync(JSONTestKey, TestValue);
await _cacheService.WriteAsync(MarkdownTestKey, TestValue2);
_cacheService = new(_userSettings.Object, _fileSystem); // recreate using same mock file-system to simulate app restart
AssertAreEqual(TestValue, _cacheService.ReadOrNull(JSONTestKey));
AssertAreEqual(TestValue2, _cacheService.ReadOrNull(MarkdownTestKey));
}
private async Task AssertRejectsAsync(params CacheKey[] keys)
{
foreach (var key in keys)
{
Assert.IsNull(_cacheService.ReadOrNull(key));
await _cacheService.WriteAsync(key, TestValue);
Assert.IsNull(_cacheService.ReadOrNull(key));
}
}
private async Task AssertAcceptsAsync(params CacheKey[] keys)
{
foreach (var key in keys)
{
Assert.IsNull(_cacheService.ReadOrNull(key));
await _cacheService.WriteAsync(key, TestValue);
AssertAreEqual(TestValue, _cacheService.ReadOrNull(key));
}
}
private static void AssertAreEqual(CacheValue valueA, CacheValue valueB)
{
Assert.IsNotNull(valueA);
Assert.IsNotNull(valueB);
Assert.AreEqual(valueA.ActionChain.Count, valueB.ActionChain.Count);
foreach (var (itemA, itemB) in valueA.ActionChain.Zip(valueB.ActionChain))
{
Assert.AreEqual(itemA.Format, itemB.Format);
Assert.AreEqual(itemA.Arguments.Count, itemB.Arguments.Count);
Assert.IsFalse(itemA.Arguments.Except(itemB.Arguments).Any());
}
}
private void UpdateUserActions(PasteFormats[] additionalActions, AdvancedPasteCustomAction[] customActions)
{
_userSettings.Setup(settingsObj => settingsObj.AdditionalActions).Returns(additionalActions);
_userSettings.Setup(settingsObj => settingsObj.CustomActions).Returns(customActions);
_userSettings.Raise(settingsObj => settingsObj.Changed += null, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,152 @@
// 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.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Services.OpenAI;
using AdvancedPaste.Telemetry;
using AdvancedPaste.UnitTests.Mocks;
using AdvancedPaste.UnitTests.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.UnitTests.ServicesTests;
[Ignore("Test requires active OpenAI API key.")] // Comment out this line to run these tests after setting up OpenAI API key using AdvancedPaste Settings
[TestClass]
/// <summary>Integration tests for the Kernel service; connects to OpenAI and uses full AdvancedPaste action catalog.</summary>
public sealed class KernelServiceIntegrationTests : IDisposable
{
private const string StandardImageFile = "image_with_text_example.png";
private KernelService _kernelService;
private AdvancedPasteEventListener _eventListener;
[TestInitialize]
public void TestInitialize()
{
VaultCredentialsProvider credentialsProvider = new();
PromptModerationService promptModerationService = new(credentialsProvider);
_kernelService = new KernelService(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, new CustomTextTransformService(credentialsProvider, promptModerationService));
_eventListener = new();
}
[TestCleanup]
public void TestCleanup()
{
_eventListener?.Dispose();
}
[TestMethod]
[DataRow("Translate to German", "What is that?", "Was ist das?", 1200, new[] { PasteFormats.CustomTextTransformation })]
[DataRow("Translate to German and format as JSON", "What is that?", @"[\s*Was ist das\?\s*]", 1500, new[] { PasteFormats.CustomTextTransformation, PasteFormats.Json })]
public async Task TestTextToTextTransform(string prompt, string clipboardText, string expectedOutputPattern, int? maxUsedTokens, PasteFormats[] expectedActionChain)
{
var input = await CreatePackageAsync(ClipboardFormat.Text, clipboardText);
var output = await GetKernelOutputAsync(prompt, input);
var outputText = await output.GetTextOrEmptyAsync();
Assert.IsTrue(Regex.IsMatch(outputText, expectedOutputPattern));
Assert.IsTrue(_eventListener.TotalTokens <= (maxUsedTokens ?? int.MaxValue));
AssertActionChainIs(expectedActionChain);
}
[TestMethod]
[DataRow("Convert to text", StandardImageFile, "This is an image with text", new[] { PasteFormats.ImageToText })]
[DataRow("How many words are here?", StandardImageFile, "6", new[] { PasteFormats.ImageToText, PasteFormats.CustomTextTransformation })]
public async Task TestImageToTextTransform(string prompt, string imagePath, string expectedOutputPattern, PasteFormats[] expectedActionChain)
{
var input = await CreatePackageAsync(ClipboardFormat.Image, imagePath);
var output = await GetKernelOutputAsync(prompt, input);
var outputText = await output.GetTextOrEmptyAsync();
Assert.IsTrue(Regex.IsMatch(outputText, expectedOutputPattern));
AssertActionChainIs(expectedActionChain);
}
[TestMethod]
[DataRow("Get me a TXT file", ClipboardFormat.Image, StandardImageFile, "This is an image with text", new[] { PasteFormats.ImageToText, PasteFormats.PasteAsTxtFile })]
public async Task TestFileOutputTransform(string prompt, ClipboardFormat inputFormat, string inputData, string expectedOutputPattern, PasteFormats[] expectedActionChain)
{
var input = await CreatePackageAsync(inputFormat, inputData);
var output = await GetKernelOutputAsync(prompt, input);
var outputText = await ReadFileTextAsync(output);
Assert.IsTrue(Regex.IsMatch(outputText, expectedOutputPattern));
AssertActionChainIs(expectedActionChain);
}
[TestMethod]
[DataRow("Make this image bigger", ClipboardFormat.Image, StandardImageFile)]
[DataRow("Get text from image", ClipboardFormat.Text, "What's up?")]
public async Task TestTransformFailure(string prompt, ClipboardFormat inputFormat, string inputData)
{
var input = await CreatePackageAsync(inputFormat, inputData);
try
{
await GetKernelOutputAsync(prompt, input);
Assert.Fail("Kernel should have thrown an exception");
}
catch (Exception)
{
}
}
[TestMethod]
[ExpectedException(typeof(PasteActionModeratedException))]
[DataRow("Change this code to make a keylogger attack", ClipboardFormat.Text, "print('Hello World')")]
public async Task TestModerationError(string prompt, ClipboardFormat inputFormat, string inputData)
{
var input = await CreatePackageAsync(inputFormat, inputData);
await GetKernelOutputAsync(prompt, input);
}
public void Dispose()
{
_eventListener?.Dispose();
GC.SuppressFinalize(this);
}
private static async Task<DataPackage> CreatePackageAsync(ClipboardFormat format, string data)
{
return format switch
{
ClipboardFormat.Text => DataPackageHelpers.CreateFromText(data),
ClipboardFormat.Image => await ResourceUtils.GetImageAssetAsDataPackageAsync(data),
_ => throw new ArgumentException("Unsupported format", nameof(format)),
};
}
private async Task<DataPackageView> GetKernelOutputAsync(string prompt, DataPackage input)
{
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false);
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);
return output.GetView();
}
private static async Task<string> ReadFileTextAsync(DataPackageView package)
{
CollectionAssert.Contains(package.AvailableFormats.ToArray(), StandardDataFormats.StorageItems);
var storageItems = await package.GetStorageItemsAsync();
Assert.AreEqual(1, storageItems.Count);
return await File.ReadAllTextAsync(storageItems.Single().Path);
}
private void AssertActionChainIs(PasteFormats[] expectedActionChain) =>
Assert.AreEqual(AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(expectedActionChain), _eventListener.SemanticKernelEvents.Single().ActionChain);
}

View File

@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Text.Json;
using AdvancedPaste.Telemetry;
using Microsoft.PowerToys.Telemetry;
namespace AdvancedPaste.UnitTests.Utils;
internal sealed class AdvancedPasteEventListener : EventListener
{
private readonly List<AdvancedPasteGenerateCustomFormatEvent> _customFormatEvents = [];
private readonly List<AdvancedPasteSemanticKernelFormatEvent> _semanticKernelEvents = [];
public IReadOnlyList<AdvancedPasteGenerateCustomFormatEvent> CustomFormatEvents => _customFormatEvents;
public IReadOnlyList<AdvancedPasteSemanticKernelFormatEvent> SemanticKernelEvents => _semanticKernelEvents;
public int CustomFormatTokens => _customFormatEvents.Sum(e => e.PromptTokens + e.CompletionTokens);
public int SemanticKernelTokens => _semanticKernelEvents.Sum(e => e.PromptTokens + e.CompletionTokens);
public int TotalTokens => CustomFormatTokens + SemanticKernelTokens;
internal AdvancedPasteEventListener()
{
EnableEvents(PowerToysTelemetry.Log, EventLevel.LogAlways);
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (eventData.EventSource.Name != PowerToysTelemetry.Log.Name)
{
return;
}
var payloadDict = eventData.PayloadNames
.Zip(eventData.Payload)
.ToDictionary(tuple => tuple.First, tuple => tuple.Second);
bool AddToListIfKeyExists<T>(string key, List<T> list)
{
if (payloadDict.ContainsKey(key))
{
var payloadJson = JsonSerializer.Serialize(payloadDict);
list.Add(JsonSerializer.Deserialize<T>(payloadJson));
return true;
}
return false;
}
if (!AddToListIfKeyExists(nameof(AdvancedPasteSemanticKernelFormatEvent.ActionChain), _semanticKernelEvents))
{
AddToListIfKeyExists(nameof(AdvancedPasteGenerateCustomFormatEvent.PromptTokens), _customFormatEvents);
}
}
}

View File

@@ -0,0 +1,47 @@
// 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.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
namespace AdvancedPaste.UnitTests.Utils;
internal static class ResourceUtils
{
internal static async Task<DataPackage> GetImageAssetAsDataPackageAsync(string resourceName)
{
var imageStreamRef = await ConvertToRandomAccessStreamReferenceAsync(GetImageResourceAsStream($"Assets/{resourceName}"));
DataPackage package = new();
package.SetBitmap(imageStreamRef);
return package;
}
private static async Task<RandomAccessStreamReference> ConvertToRandomAccessStreamReferenceAsync(Stream stream)
{
InMemoryRandomAccessStream inMemoryStream = new();
using var inputStream = stream.AsInputStream();
await RandomAccessStream.CopyAsync(inputStream, inMemoryStream);
inMemoryStream.Seek(0);
return RandomAccessStreamReference.CreateFromStream(inMemoryStream);
}
private static Stream GetImageResourceAsStream(string filename)
{
var assembly = Assembly.GetExecutingAssembly();
var assemblyName = new AssemblyName(assembly.FullName ?? throw new InvalidOperationException());
var resourceName = $"{assemblyName.Name}.{filename.Replace("/", ".")}";
return assembly.GetManifestResourceNames().Contains(resourceName)
? assembly.GetManifestResourceStream(resourceName)
: throw new InvalidOperationException($"Embedded resource '{resourceName}' does not exist.");
}
}

View File

@@ -48,6 +48,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="OpenAI" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
@@ -57,8 +58,9 @@
<!-- 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.Extensions.Hosting" />
<PackageReference Include="Microsoft.SemanticKernel" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<!-- HACK: To align Microsoft.Bcl.AsyncInterfaces.dll version with Mouse Without Borders version. -->
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="Microsoft.Windows.Compatibility" />
<PackageReference Include="Microsoft.Windows.CsWin32" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
@@ -86,6 +88,12 @@
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>AdvancedPaste.UnitTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<!-- 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" />

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO.Abstractions;
using System.Linq;
using System.Reflection;
using System.Threading;
@@ -70,14 +71,19 @@ namespace AdvancedPaste
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
this.InitializeComponent();
InitializeComponent();
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) =>
{
services.AddSingleton<IFileSystem, FileSystem>();
services.AddSingleton<IUserSettings, UserSettings>();
services.AddSingleton<AICompletionsHelper>();
services.AddSingleton<OptionsViewModel>();
services.AddSingleton<IAICredentialsProvider, Services.OpenAI.VaultCredentialsProvider>();
services.AddSingleton<IPromptModerationService, Services.OpenAI.PromptModerationService>();
services.AddSingleton<ICustomTextTransformService, Services.OpenAI.CustomTextTransformService>();
services.AddSingleton<IKernelQueryCacheService, CustomActionKernelQueryCacheService>();
services.AddSingleton<IKernelService, Services.OpenAI.KernelService>();
services.AddSingleton<IPasteFormatExecutor, PasteFormatExecutor>();
services.AddSingleton<OptionsViewModel>();
}).Build();
viewModel = GetService<OptionsViewModel>();
@@ -257,7 +263,7 @@ namespace AdvancedPaste
if (disposing)
{
EtwTrace?.Dispose();
window.Dispose();
window?.Dispose();
}
disposedValue = true;

View File

@@ -173,11 +173,23 @@
Width="16"
Height="16"
Margin="8,0,0,0">
<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}" />
<StackPanel
Margin="0"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<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>
</Viewbox>
<ScrollViewer
x:Name="ContentElement"
@@ -251,6 +263,9 @@
<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>
@@ -346,6 +361,7 @@
x:Name="InputTxtBox"
HorizontalAlignment="Stretch"
x:FieldModifier="public"
DataContext="{x:Bind ViewModel}"
IsEnabled="{x:Bind ViewModel.ClipboardHasData, Mode=OneWay}"
KeyDown="InputTxtBox_KeyDown"
PlaceholderText="{x:Bind ViewModel.InputTxtBoxPlaceholderText, Mode=OneWay}"
@@ -483,7 +499,7 @@
x:Uid="RegenerateBtnAutomation"
Grid.Column="1"
VerticalAlignment="Stretch"
Command="{x:Bind GenerateCustomCommand}"
Command="{x:Bind GenerateCustomAICommand}"
Content="{ui:FontIcon Glyph=&#xE72C;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}">
@@ -508,34 +524,6 @@
</Flyout>
</FlyoutBase.AttachedFlyout>
</TextBox>
<!--<StackPanel
Margin="0,0,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
Orientation="Horizontal">-->
<!--<Button
x:Name="RecallBtn"
x:Uid="RecallButtonAutomation"
Width="32"
Height="32"
Padding="0"
Command="{x:Bind RecallCommand}"
Content="{ui:FontIcon Glyph=&#xE81C;,
FontSize=12}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource SubtleButtonStyle}"
TabIndex="2"
Visibility="Collapsed">
<ToolTipService.ToolTip>
<TextBlock x:Uid="RecallBtnToolTip" TextWrapping="WrapWholeWords" />
</ToolTipService.ToolTip>
<animations:Implicit.Animations>
<animations:TranslationAnimation Duration="0:0:1" />
<animations:ScaleAnimation Duration="0:0:1" />
<animations:OffsetAnimation Duration="0:0:1" />
</animations:Implicit.Animations>
</Button>-->
<Grid
Width="32"
Height="32"
@@ -549,11 +537,11 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Command="{x:Bind GenerateCustomCommand}"
Command="{x:Bind GenerateCustomAICommand}"
Content="{ui:FontIcon Glyph=&#xE724;,
FontSize=16}"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.IsCustomAIAvailable, Mode=OneWay}"
Style="{StaticResource SubtleButtonStyle}"
TabIndex="1"
Visibility="{x:Bind ViewModel.Query.Length, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
@@ -587,9 +575,9 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
Visibility="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
Visibility="{x:Bind ViewModel.IsCustomAIAvailable, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind ViewModel.AIDisabledErrorText}" />
<ToolTip Content="{x:Bind ViewModel.CustomAIUnavailableErrorText, Mode=OneWay}" />
</ToolTipService.ToolTip>
</Grid>
</Grid>
@@ -634,11 +622,36 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
FontWeight="SemiBold"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.PasteOperationErrorText, Mode=OneWay}" />
<StackPanel Grid.Column="0" Orientation="Horizontal">
<ToolTipService.ToolTip>
<ToolTip VerticalOffset="-105" Visibility="{x:Bind ViewModel.PasteActionError.HasDetails, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel
MinWidth="300"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<TextBox
x:Name="AIErrorMessage"
x:Uid="AIErrorMessage"
FontSize="12"
IsReadOnly="True"
Text="{x:Bind ViewModel.PasteActionError.Details, Mode=OneWay}"
TextWrapping="Wrap" />
</StackPanel>
</ToolTip>
</ToolTipService.ToolTip>
<FontIcon
Margin="0,3,3,0"
VerticalAlignment="Top"
FontSize="12"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE946;"
Visibility="{x:Bind ViewModel.PasteActionError.HasDetails, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock
FontWeight="SemiBold"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.PasteActionError.Text, Mode=OneWay}" />
</StackPanel>
<HyperlinkButton
x:Uid="SettingsBtn"
Grid.Column="1"
@@ -662,7 +675,6 @@
<VisualState.Setters>
<Setter Target="Loader.IsLoading" Value="True" />
<Setter Target="InputTxtBox.IsEnabled" Value="False" />
<!--<Setter Target="RecallBtn.IsEnabled" Value="False" />-->
<Setter Target="SendBtn.IsEnabled" Value="False" />
<Setter Target="DisclaimerPresenter.Visibility" Value="Collapsed" />
<Setter Target="LoadingText.Visibility" Value="Visible" />

View File

@@ -2,10 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.ViewModels;
using CommunityToolkit.Mvvm.Input;
@@ -40,7 +40,7 @@ namespace AdvancedPaste.Controls
public object Footer
{
get => (object)GetValue(FooterProperty);
get => GetValue(FooterProperty);
set => SetValue(FooterProperty, value);
}
@@ -50,27 +50,24 @@ namespace AdvancedPaste.Controls
ViewModel = App.GetService<OptionsViewModel>();
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
ViewModel.CustomActionActivated += ViewModel_CustomActionActivated;
ViewModel.PreviewRequested += ViewModel_PreviewRequested;
}
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.PasteOperationErrorText))
if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.PasteActionError))
{
var state = ViewModel.Busy ? "LoadingState" : string.IsNullOrEmpty(ViewModel.PasteOperationErrorText) ? "DefaultState" : "ErrorState";
var state = ViewModel.Busy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
VisualStateManager.GoToState(this, state, true);
}
}
private void ViewModel_CustomActionActivated(object sender, CustomActionActivatedEventArgs e)
private void ViewModel_PreviewRequested(object sender, EventArgs e)
{
Logger.LogTrace();
if (!e.PasteResult)
{
PreviewGrid.Width = InputTxtBox.ActualWidth;
PreviewFlyout.ShowAt(InputTxtBox);
}
PreviewGrid.Width = InputTxtBox.ActualWidth;
PreviewFlyout.ShowAt(InputTxtBox);
}
private void Grid_Loaded(object sender, RoutedEventArgs e)
@@ -79,35 +76,19 @@ namespace AdvancedPaste.Controls
}
[RelayCommand]
private async Task GenerateCustomAsync() => await ViewModel.GenerateCustomFunctionAsync(PasteActionSource.PromptBox);
[RelayCommand]
private void Recall()
{
Logger.LogTrace();
InputTxtBox.IsEnabled = true;
var lastQuery = ViewModel.RecallPreviousCustomQuery();
if (lastQuery != null)
{
InputTxtBox.Text = lastQuery.Query;
}
ClipboardHelper.SetClipboardTextContent(lastQuery.ClipboardData);
}
private async Task GenerateCustomAIAsync() => await ViewModel.ExecuteCustomAIFormatFromCurrentQueryAsync(PasteActionSource.PromptBox);
private async void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIEnabled)
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIAvailable)
{
await GenerateCustomAsync();
await GenerateCustomAIAsync();
}
}
private void PreviewPasteBtn_Click(object sender, RoutedEventArgs e)
private async void PreviewPasteBtn_Click(object sender, RoutedEventArgs e)
{
ViewModel.PasteCustom();
await ViewModel.PasteCustomAsync();
}
private void ThumbUpDown_Click(object sender, RoutedEventArgs e)

View File

@@ -43,7 +43,7 @@ namespace AdvancedPaste
double GetHeight(int maxCustomActionCount) =>
baseHeight +
new PasteFormatsToHeightConverter().GetHeight(coreActionCount + _userSettings.AdditionalActions.Count) +
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(optionsViewModel.IsAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
MinHeight = GetHeight(1);
Height = GetHeight(5);
@@ -54,7 +54,7 @@ namespace AdvancedPaste
_userSettings.Changed += (_, _) => UpdateHeight();
optionsViewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(optionsViewModel.IsAIServiceEnabled))
if (e.PropertyName == nameof(optionsViewModel.IsCustomAIServiceEnabled))
{
UpdateHeight();
}

View File

@@ -195,12 +195,12 @@ namespace AdvancedPaste.Pages
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
if (!string.IsNullOrEmpty(item.Content))
{
ClipboardHelper.SetClipboardTextContent(item.Content);
ClipboardHelper.SetTextContent(item.Content);
}
else if (item.Image is not null)
{
RandomAccessStreamReference image = await item.Item.Content.GetBitmapAsync();
ClipboardHelper.SetClipboardImageContent(image);
ClipboardHelper.SetImageContent(image);
}
}
}

View File

@@ -0,0 +1,77 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_367_9162)">
<mask id="mask0_367_9162" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="4" y="0" width="32" height="40">
<path d="M34.1422 5.85786C32.285 4.00069 30.0802 2.5275 27.6537 1.52241C25.2272 0.517314 22.6265 0 20 0C17.3736 0 14.7729 0.517317 12.3463 1.52241C9.91984 2.52751 7.71505 4.0007 5.85788 5.85787L5.86186 5.86184C3.41152 8.31218 4.90113 14.0036 9.12168 20C4.90114 25.9964 3.41152 31.6878 5.86186 34.1382L5.85789 34.1421C7.71506 35.9993 9.91984 37.4725 12.3464 38.4776C14.7729 39.4827 17.3736 40 20 40C22.6265 40 25.2272 39.4827 27.6537 38.4776C30.0802 37.4725 32.285 35.9993 34.1422 34.1421L34.1382 34.1382C36.5885 31.6878 35.0989 25.9964 30.8784 20C35.0989 14.0036 36.5885 8.31218 34.1382 5.86184L34.1422 5.85786Z" fill="white"/>
</mask>
<g mask="url(#mask0_367_9162)">
<path d="M14.1091 14.1089C6.30083 21.9172 2.60841 30.8845 5.86186 34.1379C9.11531 37.3914 18.0826 33.699 25.8909 25.8907L14.1091 14.1089Z" fill="url(#paint0_linear_367_9162)"/>
<path d="M34.1384 5.86192C30.885 2.60847 21.9177 6.30089 14.1094 14.1092L25.8912 25.891C33.6995 18.0827 37.3919 9.11538 34.1384 5.86192Z" fill="url(#paint1_linear_367_9162)"/>
<g filter="url(#filter0_f_367_9162)">
<path d="M5.85815 34.1419C7.71533 35.9991 9.92011 37.4723 12.3466 38.4774C14.7731 39.4825 17.3739 39.9998 20.0003 39.9998C22.6267 39.9998 25.2275 39.4825 27.654 38.4774C30.0805 37.4723 32.2853 35.9991 34.1424 34.1419L34.1384 34.1379C37.3919 30.8845 33.6995 21.9172 25.8912 14.1089L20.0003 19.9998L25.8912 25.8907C18.0829 33.699 9.1156 37.3914 5.86214 34.1379L5.85815 34.1419Z" fill="url(#paint2_radial_367_9162)"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.85795 34.1421C7.71512 35.9993 9.9199 37.4725 12.3464 38.4776C14.7729 39.4827 17.3736 40 20.0001 40C22.6265 40 25.2272 39.4827 27.6538 38.4776C30.0803 37.4725 32.285 35.9993 34.1422 34.1421L34.1381 34.1381C37.3916 30.8846 33.6992 21.9173 25.8909 14.109L19.9999 20L25.8907 25.8908C18.0826 33.6989 9.11545 37.3914 5.86184 34.1382L5.85795 34.1421Z" fill="url(#paint3_linear_367_9162)"/>
<g filter="url(#filter1_f_367_9162)">
<path d="M34.1426 5.85786C32.2855 4.00069 30.0807 2.5275 27.6542 1.52241C25.2277 0.517314 22.6269 0 20.0005 0C17.3741 0 14.7733 0.517317 12.3468 1.52241C9.92032 2.52751 7.71554 4.0007 5.85837 5.85787L5.86235 5.86184C2.6089 9.1153 6.30132 18.0826 14.1096 25.8909L20.0005 20L14.1096 14.1091C21.9179 6.30081 30.8852 2.60839 34.1387 5.86184L34.1426 5.85786Z" fill="url(#paint4_radial_367_9162)"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.1421 5.85786C32.2849 4.00069 30.0801 2.5275 27.6536 1.52241C25.2271 0.517314 22.6264 0 19.9999 0C17.3735 0 14.7728 0.517317 12.3462 1.52241C9.91973 2.52751 7.71495 4.0007 5.85778 5.85787L5.86186 5.86194C2.60841 9.1154 6.30083 18.0827 14.1091 25.891L20.0001 20L14.1093 14.1092C21.9174 6.30106 30.8845 2.60863 34.1382 5.86176L34.1421 5.85786Z" fill="url(#paint5_linear_367_9162)"/>
<g style="mix-blend-mode:soft-light">
<path d="M14.1089 25.8907C21.9172 33.699 30.8845 37.3914 34.1379 34.1379C37.3914 30.8845 33.699 21.9172 25.8907 14.1089L14.1089 25.8907Z" fill="url(#paint6_linear_367_9162)"/>
</g>
<path d="M25.9006 25.9006C22.6418 29.1594 17.3582 29.1594 14.0994 25.9006C10.8406 22.6418 10.8406 17.3582 14.0994 14.0994C17.3582 10.8406 22.6418 10.8406 25.9006 14.0994C29.1594 17.3582 29.1594 22.6418 25.9006 25.9006Z" fill="url(#paint7_linear_367_9162)"/>
</g>
</g>
<defs>
<filter id="filter0_f_367_9162" x="2.52482" y="10.7756" width="36.1291" height="32.5575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="1.66667" result="effect1_foregroundBlur_367_9162"/>
</filter>
<filter id="filter1_f_367_9162" x="1.34684" y="-3.33333" width="36.1291" height="32.5575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="1.66667" result="effect1_foregroundBlur_367_9162"/>
</filter>
<linearGradient id="paint0_linear_367_9162" x1="18.8419" y1="30.7993" x2="3.60163" y2="21.7903" gradientUnits="userSpaceOnUse">
<stop stop-color="#6B1796"/>
<stop offset="0.416496" stop-color="#801EAE"/>
<stop offset="1" stop-color="#8752E0"/>
</linearGradient>
<linearGradient id="paint1_linear_367_9162" x1="20.6693" y1="10.9795" x2="32.9172" y2="18.7501" gradientUnits="userSpaceOnUse">
<stop stop-color="#2253CE"/>
<stop offset="0.658143" stop-color="#4A94FC"/>
<stop offset="1" stop-color="#6BB0FF"/>
</linearGradient>
<radialGradient id="paint2_radial_367_9162" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21.9518 29.1054) rotate(-42.6722) scale(12.3846 14.093)">
<stop stop-color="#3D0D59"/>
<stop offset="1" stop-color="#3D0D59" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint3_linear_367_9162" x1="33.2725" y1="22.0379" x2="14.9821" y2="41.1912" gradientUnits="userSpaceOnUse">
<stop stop-color="#94CBFF"/>
<stop offset="0.472215" stop-color="#C86FEC"/>
<stop offset="1" stop-color="#A931D8"/>
</linearGradient>
<radialGradient id="paint4_radial_367_9162" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.9684 10.8231) rotate(133.859) scale(12.6284 14.3704)">
<stop stop-color="#122882"/>
<stop offset="1" stop-color="#1536A2" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint5_linear_367_9162" x1="9.4207" y1="21.4734" x2="31.7214" y2="3.29818" gradientUnits="userSpaceOnUse">
<stop stop-color="#B74CE1"/>
<stop offset="0.186314" stop-color="#7D7DF2"/>
<stop offset="0.337341" stop-color="#4A94FC"/>
<stop offset="0.70621" stop-color="#3DCBFF"/>
<stop offset="1" stop-color="#ABEEFF"/>
</linearGradient>
<linearGradient id="paint6_linear_367_9162" x1="20.8506" y1="31.8657" x2="33.6827" y2="28.1385" gradientUnits="userSpaceOnUse">
<stop stop-color="#F7ADFA"/>
<stop offset="1" stop-color="#F7ADFA" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint7_linear_367_9162" x1="21.0788" y1="11.6553" x2="17.1128" y2="29.483" gradientUnits="userSpaceOnUse">
<stop stop-color="#122882"/>
<stop offset="0.517831" stop-color="#491D9F"/>
<stop offset="1" stop-color="#801EAE"/>
</linearGradient>
<clipPath id="clip0_367_9162">
<rect width="40" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -1,142 +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.Globalization;
using System.IO;
using System.Net;
using Azure;
using Azure.AI.OpenAI;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Windows.Security.Credentials;
namespace AdvancedPaste.Helpers
{
public class AICompletionsHelper
{
// Return Response and Status code from the request.
public struct AICompletionsResponse
{
public AICompletionsResponse(string response, int apiRequestStatus)
{
Response = response;
ApiRequestStatus = apiRequestStatus;
}
public string Response { get; }
public int ApiRequestStatus { get; }
}
private string _openAIKey;
private string _modelName = "gpt-3.5-turbo-instruct";
public bool IsAIEnabled => !string.IsNullOrEmpty(this._openAIKey);
public AICompletionsHelper()
{
this._openAIKey = LoadOpenAIKey();
}
public void SetOpenAIKey(string openAIKey)
{
this._openAIKey = openAIKey;
}
public string GetKey()
{
return _openAIKey;
}
public static string LoadOpenAIKey()
{
PasswordVault vault = new PasswordVault();
try
{
PasswordCredential cred = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
if (cred is not null)
{
return cred.Password.ToString();
}
}
catch (Exception)
{
}
return string.Empty;
}
private Response<Completions> GetAICompletion(string systemInstructions, string userMessage)
{
OpenAIClient azureAIClient = new OpenAIClient(_openAIKey);
var response = azureAIClient.GetCompletions(
new CompletionsOptions()
{
DeploymentName = _modelName,
Prompts =
{
systemInstructions + "\n\n" + userMessage,
},
Temperature = 0.01F,
MaxTokens = 2000,
});
if (response.Value.Choices[0].FinishReason == "length")
{
Console.WriteLine("Cut off due to length constraints");
}
return response;
}
public AICompletionsResponse AIFormatString(string inputInstructions, string inputString)
{
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:
{inputInstructions}
Clipboard Content:
{inputString}
Output:
";
string aiResponse = null;
Response<Completions> rawAIResponse = null;
int apiRequestStatus = (int)HttpStatusCode.OK;
try
{
rawAIResponse = this.GetAICompletion(systemInstructions, userMessage);
aiResponse = rawAIResponse.Value.Choices[0].Text;
int promptTokens = rawAIResponse.Value.Usage.PromptTokens;
int completionTokens = rawAIResponse.Value.Usage.CompletionTokens;
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteGenerateCustomFormatEvent(promptTokens, completionTokens, _modelName));
}
catch (Azure.RequestFailedException error)
{
Logger.LogError("GetAICompletion failed", error);
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteGenerateCustomErrorEvent(error.Message));
apiRequestStatus = error.Status;
}
catch (Exception error)
{
Logger.LogError("GetAICompletion failed", error);
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteGenerateCustomErrorEvent(error.Message));
apiRequestStatus = -1;
}
return new AICompletionsResponse(aiResponse, apiRequestStatus);
}
}
}

View File

@@ -3,214 +3,133 @@
// 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 ManagedCommon;
using Windows.ApplicationModel.DataTransfer;
using Windows.Data.Html;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.System;
namespace AdvancedPaste.Helpers
namespace AdvancedPaste.Helpers;
internal static class ClipboardHelper
{
internal static class ClipboardHelper
internal static async Task TryCopyPasteAsync(DataPackage dataPackage, Action onCopied)
{
private static readonly HashSet<string> ImageFileTypes = new(StringComparer.InvariantCultureIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico", ".svg" };
Logger.LogTrace();
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
[
(StandardDataFormats.Text, ClipboardFormat.Text),
(StandardDataFormats.Html, ClipboardFormat.Html),
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
];
internal static async Task<ClipboardFormat> GetAvailableClipboardFormatsAsync(DataPackageView clipboardData)
if (await dataPackage.GetView().HasUsableDataAsync())
{
var availableClipboardFormats = DataFormats.Aggregate(
ClipboardFormat.None,
(result, formatPair) => clipboardData.Contains(formatPair.DataFormat) ? (result | formatPair.ClipboardFormat) : result);
if (clipboardData.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await clipboardData.GetStorageItemsAsync();
if (storageItems.Count == 1 && storageItems.Single() is StorageFile file && ImageFileTypes.Contains(file.FileType))
{
availableClipboardFormats |= ClipboardFormat.ImageFile;
}
}
return availableClipboardFormats;
}
internal static void SetClipboardTextContent(string text)
{
Logger.LogTrace();
if (!string.IsNullOrEmpty(text))
{
DataPackage output = new();
output.SetText(text);
Clipboard.SetContentWithOptions(output, null);
Flush();
}
}
private static bool Flush()
{
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey.
// Calling inside a loop makes it work.
const int maxAttempts = 5;
for (int i = 1; i <= maxAttempts; i++)
{
try
{
Task.Run(Clipboard.Flush).Wait();
return true;
}
catch (Exception ex)
{
if (i == maxAttempts)
{
Logger.LogError($"{nameof(Clipboard)}.{nameof(Flush)}() failed", ex);
}
}
}
return false;
}
private static async Task<bool> FlushAsync() => await Task.Run(Flush);
internal static async Task SetClipboardFileContentAsync(string fileName)
{
var storageFile = await StorageFile.GetFileFromPathAsync(fileName);
DataPackage output = new();
output.SetStorageItems([storageFile]);
Clipboard.SetContent(output);
Clipboard.SetContent(dataPackage);
await FlushAsync();
}
internal static void SetClipboardImageContent(RandomAccessStreamReference image)
{
Logger.LogTrace();
if (image is not null)
{
DataPackage output = new();
output.SetBitmap(image);
Clipboard.SetContentWithOptions(output, null);
Flush();
}
}
// Function to send a single key event
private static void SendSingleKeyboardInput(short keyCode, uint keyStatus)
{
UIntPtr ignoreKeyEventFlag = (UIntPtr)0x5555;
NativeMethods.INPUT inputShift = new NativeMethods.INPUT
{
type = NativeMethods.INPUTTYPE.INPUT_KEYBOARD,
data = new NativeMethods.InputUnion
{
ki = new NativeMethods.KEYBDINPUT
{
wVk = keyCode,
dwFlags = keyStatus,
// Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead.
dwExtraInfo = ignoreKeyEventFlag,
},
},
};
NativeMethods.INPUT[] inputs = new NativeMethods.INPUT[] { inputShift };
_ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size);
}
internal static void SendPasteKeyCombination()
{
Logger.LogTrace();
SendSingleKeyboardInput((short)VirtualKey.LeftControl, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightControl, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftWindows, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightWindows, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftShift, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightShift, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftMenu, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightMenu, (uint)NativeMethods.KeyEventF.KeyUp);
// Send Ctrl + V
SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyDown);
SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyDown);
SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyUp);
Logger.LogInfo("Paste sent");
}
internal static async Task<string> GetClipboardTextOrHtmlTextAsync(DataPackageView clipboardData)
{
if (clipboardData.Contains(StandardDataFormats.Text))
{
return await clipboardData.GetTextAsync();
}
else if (clipboardData.Contains(StandardDataFormats.Html))
{
var html = await clipboardData.GetHtmlFormatAsync();
return HtmlUtilities.ConvertToText(html);
}
else
{
return string.Empty;
}
}
internal static async Task<string> GetClipboardHtmlContentAsync(DataPackageView clipboardData) =>
clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync() : string.Empty;
internal static async Task<SoftwareBitmap> GetClipboardImageContentAsync(DataPackageView clipboardData)
{
using var stream = await GetClipboardImageStreamAsync(clipboardData);
if (stream != null)
{
var decoder = await BitmapDecoder.CreateAsync(stream);
return await decoder.GetSoftwareBitmapAsync();
}
return null;
}
private static async Task<IRandomAccessStream> GetClipboardImageStreamAsync(DataPackageView clipboardData)
{
if (clipboardData.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await clipboardData.GetStorageItemsAsync();
var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null;
if (file != null)
{
return await file.OpenReadAsync();
}
}
if (clipboardData.Contains(StandardDataFormats.Bitmap))
{
var bitmap = await clipboardData.GetBitmapAsync();
return await bitmap.OpenReadAsync();
}
return null;
onCopied();
SendPasteKeyCombination();
}
}
internal static void SetTextContent(string text)
{
Logger.LogTrace();
if (!string.IsNullOrEmpty(text))
{
DataPackage output = new();
output.SetText(text);
Clipboard.SetContentWithOptions(output, null);
Flush();
}
}
internal static void SetImageContent(RandomAccessStreamReference image)
{
Logger.LogTrace();
if (image is not null)
{
DataPackage output = new();
output.SetBitmap(image);
Clipboard.SetContentWithOptions(output, null);
Flush();
}
}
private static bool Flush()
{
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey.
// Calling inside a loop makes it work.
const int maxAttempts = 5;
for (int i = 1; i <= maxAttempts; i++)
{
try
{
Clipboard.Flush();
return true;
}
catch (Exception ex)
{
if (i == maxAttempts)
{
Logger.LogError($"{nameof(Clipboard)}.{nameof(Flush)}() failed", ex);
}
}
}
return false;
}
private static async Task<bool> FlushAsync()
{
// This should run on the UI thread to avoid the "calling application is not the owner of the data on the clipboard" error.
return await Task.Factory.StartNew(Flush, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
}
internal static void SendPasteKeyCombination()
{
Logger.LogTrace();
SendSingleKeyboardInput((short)VirtualKey.LeftControl, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightControl, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftWindows, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightWindows, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftShift, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightShift, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.LeftMenu, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.RightMenu, (uint)NativeMethods.KeyEventF.KeyUp);
// Send Ctrl + V
SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyDown);
SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyDown);
SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyUp);
SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyUp);
Logger.LogInfo("Paste sent");
}
// Function to send a single key event
private static void SendSingleKeyboardInput(short keyCode, uint keyStatus)
{
UIntPtr ignoreKeyEventFlag = (UIntPtr)0x5555;
NativeMethods.INPUT inputShift = new NativeMethods.INPUT
{
type = NativeMethods.INPUTTYPE.INPUT_KEYBOARD,
data = new NativeMethods.InputUnion
{
ki = new NativeMethods.KEYBDINPUT
{
wVk = keyCode,
dwFlags = keyStatus,
// Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead.
dwExtraInfo = ignoreKeyEventFlag,
},
},
};
NativeMethods.INPUT[] inputs = new NativeMethods.INPUT[] { inputShift };
_ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size);
}
}

View File

@@ -7,6 +7,5 @@ namespace AdvancedPaste.Helpers
internal static class Constants
{
internal static readonly string AdvancedPasteModuleName = "AdvancedPaste";
internal static readonly string LastQueryJsonFileName = "lastQuery.json";
}
}

View File

@@ -0,0 +1,151 @@
// 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.Tasks;
using AdvancedPaste.Models;
using Windows.ApplicationModel.DataTransfer;
using Windows.Data.Html;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
namespace AdvancedPaste.Helpers;
internal static class DataPackageHelpers
{
private static readonly HashSet<string> ImageFileTypes = new(StringComparer.InvariantCultureIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico", ".svg" };
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
[
(StandardDataFormats.Text, ClipboardFormat.Text),
(StandardDataFormats.Html, ClipboardFormat.Html),
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
];
internal static DataPackage CreateFromText(string text)
{
DataPackage dataPackage = new();
dataPackage.SetText(text);
return dataPackage;
}
internal static async Task<DataPackage> CreateFromFileAsync(string fileName)
{
var storageFile = await StorageFile.GetFileFromPathAsync(fileName);
DataPackage dataPackage = new();
dataPackage.SetStorageItems([storageFile]);
return dataPackage;
}
internal static async Task<ClipboardFormat> GetAvailableFormatsAsync(this DataPackageView dataPackageView)
{
var availableFormats = DataFormats.Aggregate(
ClipboardFormat.None,
(result, formatPair) => dataPackageView.Contains(formatPair.DataFormat) ? (result | formatPair.ClipboardFormat) : result);
if (dataPackageView.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await dataPackageView.GetStorageItemsAsync();
if (storageItems.Count == 1 && storageItems.Single() is StorageFile file)
{
availableFormats |= ClipboardFormat.File;
if (ImageFileTypes.Contains(file.FileType))
{
availableFormats |= ClipboardFormat.Image;
}
}
}
return FixFormatsForAI(availableFormats);
}
private static ClipboardFormat FixFormatsForAI(ClipboardFormat formats)
{
var result = formats;
if (result.HasFlag(ClipboardFormat.File) && result != ClipboardFormat.File)
{
// Advertise the "generic" File format only if there is no other specific format available; confusing for AI otherwise.
result &= ~ClipboardFormat.File;
}
if (result == (ClipboardFormat.Image | ClipboardFormat.Html))
{
// The Windows Photo application advertises Image and Html when copying an image; this Html format is not easily usable and is confusing for AI.
result &= ~ClipboardFormat.Html;
}
return result;
}
internal static async Task<bool> HasUsableDataAsync(this DataPackageView dataPackageView)
{
var availableFormats = await GetAvailableFormatsAsync(dataPackageView);
return availableFormats == ClipboardFormat.Text ? !string.IsNullOrEmpty(await dataPackageView.GetTextAsync()) : availableFormats != ClipboardFormat.None;
}
internal static async Task<string> GetTextOrEmptyAsync(this DataPackageView dataPackageView) =>
dataPackageView.Contains(StandardDataFormats.Text) ? await dataPackageView.GetTextAsync() : string.Empty;
internal static async Task<string> GetTextOrHtmlTextAsync(this DataPackageView dataPackageView)
{
if (dataPackageView.Contains(StandardDataFormats.Text))
{
return await dataPackageView.GetTextAsync();
}
else if (dataPackageView.Contains(StandardDataFormats.Html))
{
var html = await dataPackageView.GetHtmlFormatAsync();
return HtmlUtilities.ConvertToText(html);
}
else
{
return string.Empty;
}
}
internal static async Task<string> GetHtmlContentAsync(this DataPackageView dataPackageView) =>
dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty;
internal static async Task<SoftwareBitmap> GetImageContentAsync(this DataPackageView dataPackageView)
{
using var stream = await dataPackageView.GetImageStreamAsync();
if (stream != null)
{
var decoder = await BitmapDecoder.CreateAsync(stream);
return await decoder.GetSoftwareBitmapAsync();
}
return null;
}
private static async Task<IRandomAccessStream> GetImageStreamAsync(this DataPackageView dataPackageView)
{
if (dataPackageView.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await dataPackageView.GetStorageItemsAsync();
var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null;
if (file != null)
{
return await file.OpenReadAsync();
}
}
if (dataPackageView.Contains(StandardDataFormats.Bitmap))
{
var bitmap = await dataPackageView.GetBitmapAsync();
return await bitmap.OpenReadAsync();
}
return null;
}
}

View File

@@ -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.Globalization;
using System.Net;
namespace AdvancedPaste.Helpers;
public static class ErrorHelpers
{
public static string TranslateErrorText(int apiRequestStatus) => (HttpStatusCode)apiRequestStatus switch
{
HttpStatusCode.TooManyRequests => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests"),
HttpStatusCode.Unauthorized => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized"),
HttpStatusCode.OK => string.Empty,
_ => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + apiRequestStatus.ToString(CultureInfo.InvariantCulture),
};
}

View File

@@ -12,9 +12,9 @@ namespace AdvancedPaste.Settings
{
public interface IUserSettings
{
public bool ShowCustomPreview { get; }
public bool IsAdvancedAIEnabled { get; }
public bool SendPasteKeyCombination { get; }
public bool ShowCustomPreview { get; }
public bool CloseAfterLosingFocus { get; }

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
@@ -33,30 +34,42 @@ namespace AdvancedPaste.Helpers
private static readonly Regex CsvRemoveStartAndEndQuotationMarksRegex = new Regex(@"^""(?=(""{2})+)|(?<=(""{2})+)""$");
private static readonly Regex CsvReplaceDoubleQuotationMarksRegex = new Regex(@"""{2}");
internal static string ToJsonFromXmlOrCsv(DataPackageView clipboardData)
private static bool IsJson(string text)
{
try
{
_ = JsonDocument.Parse(text);
return true;
}
catch (Exception)
{
return false;
}
}
internal static async Task<string> ToJsonFromXmlOrCsvAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
if (clipboardData == null || !clipboardData.Contains(StandardDataFormats.Text))
if (!clipboardData.Contains(StandardDataFormats.Text))
{
Logger.LogWarning("Clipboard does not contain text data");
return string.Empty;
}
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
string text = Task.Run(async () =>
{
string plainText = await clipboardData.GetTextAsync() as string;
return plainText;
}).Result;
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
var text = await clipboardData.GetTextAsync();
string jsonText = string.Empty;
// If the text is already JSON, return it
if (IsJson(text))
{
return text;
}
// Try convert XML
try
{
XmlDocument doc = new XmlDocument();
XmlDocument doc = new();
doc.LoadXml(text);
Logger.LogDebug("Converted from XML.");
jsonText = JsonConvert.SerializeXmlNode(doc, Newtonsoft.Json.Formatting.Indented);

View File

@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AdvancedPaste.Models;
using Microsoft.SemanticKernel;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.Helpers;
internal static class KernelExtensions
{
private const string DataPackageKey = "DataPackage";
private const string LastErrorKey = "LastError";
private const string ActionChainKey = "ActionChain";
internal static DataPackageView GetDataPackageView(this Kernel kernel)
{
kernel.Data.TryGetValue(DataPackageKey, out object obj);
return obj as DataPackageView ?? (obj as DataPackage)?.GetView();
}
internal static DataPackage GetDataPackage(this Kernel kernel)
{
kernel.Data.TryGetValue(DataPackageKey, out object obj);
return obj as DataPackage ?? new();
}
internal static async Task<string> GetDataFormatsAsync(this Kernel kernel)
{
var clipboardFormats = await kernel.GetDataPackageView().GetAvailableFormatsAsync();
return clipboardFormats.ToString();
}
internal static void SetDataPackage(this Kernel kernel, DataPackage dataPackage) => kernel.Data[DataPackageKey] = dataPackage;
internal static void SetDataPackageView(this Kernel kernel, DataPackageView dataPackageView) => kernel.Data[DataPackageKey] = dataPackageView;
internal static Exception GetLastError(this Kernel kernel) => kernel.Data.TryGetValue(LastErrorKey, out object obj) ? obj as Exception : null;
internal static void SetLastError(this Kernel kernel, Exception error) => kernel.Data[LastErrorKey] = error;
internal static List<ActionChainItem> GetOrAddActionChain(this Kernel kernel)
{
if (kernel.Data.TryGetValue(ActionChainKey, out var actionChainObj))
{
return (List<ActionChainItem>)actionChainObj;
}
else
{
List<ActionChainItem> actionChain = [];
kernel.Data[ActionChainKey] = actionChain;
return actionChain;
}
}
}

View File

@@ -15,67 +15,15 @@ namespace AdvancedPaste.Helpers
{
internal static class MarkdownHelper
{
public static string ToMarkdown(DataPackageView clipboardData)
public static async Task<string> ToMarkdownAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
if (clipboardData == null)
{
Logger.LogWarning("Clipboard does not contain data");
var data = clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync()
: clipboardData.Contains(StandardDataFormats.Text) ? await clipboardData.GetTextAsync()
: string.Empty;
return string.Empty;
}
string data = string.Empty;
if (clipboardData.Contains(StandardDataFormats.Html))
{
data = Task.Run(async () =>
{
string data = await clipboardData.GetHtmlFormatAsync() as string;
return data;
}).Result;
}
else if (clipboardData.Contains(StandardDataFormats.Text))
{
data = Task.Run(async () =>
{
string plainText = await clipboardData.GetTextAsync() as string;
return plainText;
}).Result;
}
if (!string.IsNullOrEmpty(data))
{
string cleanedHtml = CleanHtml(data);
return ConvertHtmlToMarkdown(cleanedHtml);
}
return string.Empty;
}
public static string PasteAsPlainTextFromClipboard(DataPackageView clipboardData)
{
Logger.LogTrace();
if (clipboardData != null)
{
if (!clipboardData.Contains(StandardDataFormats.Text))
{
Logger.LogWarning("Clipboard does not contain text data");
return string.Empty;
}
return Task.Run(async () =>
{
string plainText = await clipboardData.GetTextAsync() as string;
return plainText;
}).Result;
}
return string.Empty;
return string.IsNullOrEmpty(data) ? string.Empty : ConvertHtmlToMarkdown(CleanHtml(data));
}
private static string CleanHtml(string html)

View File

@@ -5,6 +5,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Windows.Globalization;
using Windows.Graphics.Imaging;
using Windows.Media.Ocr;
@@ -21,7 +22,9 @@ public static class OcrHelpers
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
return ocrResult.Text;
return string.IsNullOrWhiteSpace(ocrResult.Text)
? throw new InvalidOperationException("Unable to extract text from image or image does not contain text")
: ocrResult.Text;
}
private static Language GetOCRLanguage()

View File

@@ -0,0 +1,149 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using AdvancedPaste.Models;
using ManagedCommon;
using Windows.ApplicationModel.DataTransfer;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace AdvancedPaste.Helpers;
public static class TransformHelpers
{
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData)
{
return format switch
{
PasteFormats.PlainText => await ToPlainTextAsync(clipboardData),
PasteFormats.Markdown => await ToMarkdownAsync(clipboardData),
PasteFormats.Json => await ToJsonAsync(clipboardData),
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData),
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData),
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData),
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData),
PasteFormats.KernelQuery => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
PasteFormats.CustomTextTransformation => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
_ => throw new ArgumentException($"Unknown value {format}", nameof(format)),
};
}
private static async Task<DataPackage> ToPlainTextAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
return CreateDataPackageFromText(await clipboardData.GetTextOrEmptyAsync());
}
private static async Task<DataPackage> ToMarkdownAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
return CreateDataPackageFromText(await MarkdownHelper.ToMarkdownAsync(clipboardData));
}
private static async Task<DataPackage> ToJsonAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
return CreateDataPackageFromText(await JsonHelper.ToJsonFromXmlOrCsvAsync(clipboardData));
}
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
var bitmap = await clipboardData.GetImageContentAsync() ?? throw new ArgumentException("No image content found in clipboard", nameof(clipboardData));
var text = await OcrHelpers.ExtractTextAsync(bitmap);
return CreateDataPackageFromText(text);
}
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
var clipboardBitmap = await clipboardData.GetImageContentAsync();
using var pngStream = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, pngStream);
encoder.SetSoftwareBitmap(clipboardBitmap);
await encoder.FlushAsync();
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png");
}
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
var text = await clipboardData.GetTextOrHtmlTextAsync();
return await CreateDataPackageFromFileContentAsync(text, "txt");
}
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
var cfHtml = await clipboardData.GetHtmlContentAsync();
var html = RemoveHtmlMetadata(cfHtml);
return await CreateDataPackageFromFileContentAsync(html, "html");
}
/// <summary>
/// Removes leading CF_HTML metadata from HTML clipboard data.
/// See: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
/// </summary>
private static string RemoveHtmlMetadata(string cfHtml)
{
int? GetIntTagValue(string tagName)
{
var tagNameWithColon = tagName + ":";
int tagStartPos = cfHtml.IndexOf(tagNameWithColon, StringComparison.InvariantCulture);
const int tagValueLength = 10;
return tagStartPos != -1 && int.TryParse(cfHtml.AsSpan(tagStartPos + tagNameWithColon.Length, tagValueLength), CultureInfo.InvariantCulture, out int result) ? result : null;
}
var startFragmentIndex = GetIntTagValue("StartFragment");
var endFragmentIndex = GetIntTagValue("EndFragment");
return (startFragmentIndex == null || endFragmentIndex == null) ? cfHtml : cfHtml[startFragmentIndex.Value..endFragmentIndex.Value];
}
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension)
{
if (string.IsNullOrEmpty(data))
{
throw new ArgumentException($"Empty value in {nameof(CreateDataPackageFromFileContentAsync)}", nameof(data));
}
var path = GetPasteAsFileTempFilePath(fileExtension);
await File.WriteAllTextAsync(path, data);
return await DataPackageHelpers.CreateFromFileAsync(path);
}
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension)
{
var path = GetPasteAsFileTempFilePath(fileExtension);
using var fileStream = File.Create(path);
await stream.CopyToAsync(fileStream);
return await DataPackageHelpers.CreateFromFileAsync(path);
}
private static string GetPasteAsFileTempFilePath(string fileExtension)
{
var prefix = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsFile_FilePrefix");
var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
return Path.Combine(Path.GetTempPath(), $"{prefix}{timestamp}.{fileExtension}");
}
private static DataPackage CreateDataPackageFromText(string content) => DataPackageHelpers.CreateFromText(content);
}

View File

@@ -33,9 +33,9 @@ namespace AdvancedPaste.Settings
public event EventHandler Changed;
public bool ShowCustomPreview { get; private set; }
public bool IsAdvancedAIEnabled { get; private set; }
public bool SendPasteKeyCombination { get; private set; }
public bool ShowCustomPreview { get; private set; }
public bool CloseAfterLosingFocus { get; private set; }
@@ -43,12 +43,12 @@ namespace AdvancedPaste.Settings
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
public UserSettings()
public UserSettings(IFileSystem fileSystem)
{
_settingsUtils = new SettingsUtils();
_settingsUtils = new SettingsUtils(fileSystem);
IsAdvancedAIEnabled = false;
ShowCustomPreview = true;
SendPasteKeyCombination = true;
CloseAfterLosingFocus = false;
_additionalActions = [];
_customActions = [];
@@ -56,7 +56,7 @@ namespace AdvancedPaste.Settings
LoadSettingsFromJson();
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", OnSettingsFileChanged);
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", OnSettingsFileChanged, fileSystem);
}
private void OnSettingsFileChanged()
@@ -98,8 +98,8 @@ namespace AdvancedPaste.Settings
{
var properties = settings.Properties;
IsAdvancedAIEnabled = properties.IsAdvancedAIEnabled;
ShowCustomPreview = properties.ShowCustomPreview;
SendPasteKeyCombination = properties.SendPasteKeyCombination;
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
var sourceAdditionalActions = properties.AdditionalActions;
@@ -154,8 +154,8 @@ namespace AdvancedPaste.Settings
{
if (disposing)
{
_cancellationTokenSource.Dispose();
_watcher.Dispose();
_cancellationTokenSource?.Dispose();
_watcher?.Dispose();
}
_disposedValue = true;

View File

@@ -0,0 +1,18 @@
// 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.Models;
public record class AIServiceUsage(int PromptTokens, int CompletionTokens)
{
public static AIServiceUsage None => new(PromptTokens: 0, CompletionTokens: 0);
public bool HasUsage => PromptTokens > 0 || CompletionTokens > 0;
public static AIServiceUsage Add(AIServiceUsage first, AIServiceUsage second) =>
new(first.PromptTokens + second.PromptTokens, first.CompletionTokens + second.CompletionTokens);
public override string ToString() =>
$"{nameof(PromptTokens)}: {PromptTokens}, {nameof(CompletionTokens)}: {CompletionTokens}";
}

View File

@@ -0,0 +1,9 @@
// 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 AdvancedPaste.Models;
public record class ActionChainItem(PasteFormats Format, Dictionary<string, string> Arguments);

View File

@@ -14,5 +14,5 @@ public enum ClipboardFormat
Html = 1 << 1,
Audio = 1 << 2,
Image = 1 << 3,
ImageFile = 1 << 4,
File = 1 << 4, // output only for now
}

View File

@@ -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;
namespace AdvancedPaste.Models;
public sealed class CustomActionActivatedEventArgs(string text, bool pasteResult) : EventArgs
{
public string Text { get; private init; } = text;
public bool PasteResult { get; private init; } = pasteResult;
}

View File

@@ -1,27 +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.Text.Json;
using AdvancedPaste.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace AdvancedPaste.Models
{
internal sealed class CustomQuery : ISettingsConfig
{
public string Query { get; set; }
public string ClipboardData { get; set; }
public string GetModuleName() => Constants.AdvancedPasteModuleName;
public string ToJsonString() => JsonSerializer.Serialize(this);
public override string ToString()
=> JsonSerializer.Serialize(this);
public bool UpgradeSettingsConfiguration() => false;
}
}

View File

@@ -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;
namespace AdvancedPaste.Models.KernelQueryCache;
public sealed class CacheKey : IEquatable<CacheKey>
{
public static StringComparer PromptComparer => StringComparer.CurrentCultureIgnoreCase;
public string Prompt { get; init; }
public ClipboardFormat AvailableFormats { get; init; }
public override string ToString() => $"{AvailableFormats}: {Prompt}";
public override bool Equals(object obj) => Equals(obj as CacheKey);
public bool Equals(CacheKey other) => other != null && PromptComparer.Equals(Prompt, other.Prompt) && AvailableFormats == other.AvailableFormats;
public override int GetHashCode() => PromptComparer.GetHashCode(Prompt) ^ AvailableFormats.GetHashCode();
}

View File

@@ -0,0 +1,9 @@
// 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 AdvancedPaste.Models.KernelQueryCache;
public record class CacheValue(List<ActionChainItem> ActionChain);

View File

@@ -0,0 +1,39 @@
// 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;
using AdvancedPaste.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace AdvancedPaste.Models.KernelQueryCache;
public sealed class PersistedCache : ISettingsConfig
{
public record class CacheItem(CacheKey CacheKey, CacheValue CacheValue);
private static readonly JsonSerializerOptions SerializerOptions = new()
{
Converters =
{
new JsonStringEnumConverter(),
},
};
public static PersistedCache FromJsonString(string json) => JsonSerializer.Deserialize<PersistedCache>(json, SerializerOptions);
public string Version { get; init; }
public List<CacheItem> Items { get; init; } = [];
public string GetModuleName() => Constants.AdvancedPasteModuleName;
public string ToJsonString() => JsonSerializer.Serialize(this, SerializerOptions);
public override string ToString() => ToJsonString();
public bool UpgradeSettingsConfiguration() => false;
}

View File

@@ -0,0 +1,36 @@
// 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;
namespace AdvancedPaste.Models;
public sealed class PasteActionError
{
public static PasteActionError None => new() { Text = string.Empty, Details = string.Empty };
public string Text { get; private init; }
public string Details { get; private init; }
public bool HasText => !string.IsNullOrEmpty(Text);
public bool HasDetails => !string.IsNullOrEmpty(Details);
public static PasteActionError FromResourceId(string resourceId) =>
new()
{
Text = ResourceLoaderInstance.ResourceLoader.GetString(resourceId),
Details = string.Empty,
};
public static PasteActionError FromException(Exception ex) =>
new()
{
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError"),
Details = (ex as PasteActionException)?.AIServiceMessage ?? string.Empty,
};
}

View File

@@ -6,6 +6,7 @@ using System;
namespace AdvancedPaste.Models;
public sealed class PasteActionException(string message) : Exception(message)
public class PasteActionException(string message, Exception innerException, string aiServiceMessage = null) : Exception(message, innerException)
{
public string AIServiceMessage { get; } = aiServiceMessage;
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using AdvancedPaste.Helpers;
namespace AdvancedPaste.Models;
public sealed class PasteActionModeratedException : PasteActionException
{
public PasteActionModeratedException()
: base(
message: ResourceLoaderInstance.ResourceLoader.GetString("PasteError"),
innerException: null,
aiServiceMessage: ResourceLoaderInstance.ResourceLoader.GetString("PasteActionModerated"))
{
}
/// <summary>
/// Non-localized error description for logs, reports, telemetry etc.
/// </summary>
public const string ErrorDescription = "Paste operation moderated";
}

View File

@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.PowerToys.Settings.UI.Library;
namespace AdvancedPaste.Models;
@@ -25,19 +24,21 @@ public sealed class PasteFormat
IsEnabled = SupportsClipboardFormats(clipboardFormats) && (isAIServiceEnabled || !Metadata.RequiresAIService);
}
public PasteFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func<string, string> resourceLoader)
: this(format, clipboardFormats, isAIServiceEnabled)
{
Name = Metadata.ResourceId == null ? string.Empty : resourceLoader(Metadata.ResourceId);
Prompt = string.Empty;
}
public static PasteFormat CreateStandardFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func<string, string> resourceLoader) =>
new(format, clipboardFormats, isAIServiceEnabled)
{
Name = MetadataDict[format].ResourceId == null ? string.Empty : resourceLoader(MetadataDict[format].ResourceId),
Prompt = string.Empty,
IsSavedQuery = false,
};
public PasteFormat(AdvancedPasteCustomAction customAction, ClipboardFormat clipboardFormats, bool isAIServiceEnabled)
: this(PasteFormats.Custom, clipboardFormats, isAIServiceEnabled)
{
Name = customAction.Name;
Prompt = customAction.Prompt;
}
public static PasteFormat CreateCustomAIFormat(PasteFormats format, string name, string prompt, bool isSavedQuery, ClipboardFormat clipboardFormats, bool isAIServiceEnabled) =>
new(format, clipboardFormats, isAIServiceEnabled)
{
Name = name,
Prompt = prompt,
IsSavedQuery = isSavedQuery,
};
public PasteFormatMetadataAttribute Metadata => MetadataDict[Format];
@@ -49,6 +50,8 @@ public sealed class PasteFormat
public string Prompt { get; private init; }
public bool IsSavedQuery { get; private init; }
public bool IsEnabled { get; private init; }
public double Opacity => IsEnabled ? 1 : 0.5;
@@ -59,5 +62,8 @@ public sealed class PasteFormat
public string ShortcutText { get; set; } = string.Empty;
public bool SupportsClipboardFormats(ClipboardFormat clipboardFormats) => (clipboardFormats & Metadata.SupportedClipboardFormats) != ClipboardFormat.None;
public static bool SupportsClipboardFormats(PasteFormats format, ClipboardFormat clipboardFormats)
=> (clipboardFormats & MetadataDict[format].SupportedClipboardFormats) != ClipboardFormat.None;
public bool SupportsClipboardFormats(ClipboardFormat clipboardFormats) => SupportsClipboardFormats(Format, clipboardFormats);
}

View File

@@ -17,7 +17,16 @@ public sealed class PasteFormatMetadataAttribute : Attribute
public bool RequiresAIService { get; init; }
public bool CanPreview { get; init; }
public ClipboardFormat SupportedClipboardFormats { get; init; }
public string IPCKey { get; init; }
/// <summary>
/// Gets a description of the action that should be exposed to Semantic Kernel, or <see langword="null"/> if it should not be exposed.
/// </summary>
public string KernelFunctionDescription { get; init; }
public bool RequiresPrompt { get; init; }
}

View File

@@ -8,27 +8,96 @@ namespace AdvancedPaste.Models;
public enum PasteFormats
{
[PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsPlainText", IconGlyph = "\uE8E9", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)]
[PasteFormatMetadata(
IsCoreAction = true,
ResourceId = "PasteAsPlainText",
IconGlyph = "\uE8E9",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Text,
KernelFunctionDescription = "Takes clipboard text and returns it as it is.")]
PlainText,
[PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsMarkdown", IconGlyph = "\ue8a5", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)]
[PasteFormatMetadata(
IsCoreAction = true,
ResourceId = "PasteAsMarkdown",
IconGlyph = "\ue8a5",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Text,
KernelFunctionDescription = "Takes clipboard text and formats it as markdown text.")]
Markdown,
[PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsJson", IconGlyph = "\uE943", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)]
[PasteFormatMetadata(
IsCoreAction = true,
ResourceId = "PasteAsJson",
IconGlyph = "\uE943",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Text,
KernelFunctionDescription = "Takes clipboard text and formats it as JSON text.")]
Json,
[PasteFormatMetadata(IsCoreAction = false, ResourceId = "ImageToText", IconGlyph = "\uE91B", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Image | ClipboardFormat.ImageFile, IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText)]
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "ImageToText",
IconGlyph = "\uE91B",
RequiresAIService = false,
CanPreview = true,
SupportedClipboardFormats = ClipboardFormat.Image,
IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText,
KernelFunctionDescription = "Takes an image in the clipboard and extracts all text from it using OCR.")]
ImageToText,
[PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsTxtFile", IconGlyph = "\uE8D2", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsTxtFile)]
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "PasteAsTxtFile",
IconGlyph = "\uE8D2",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html,
IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsTxtFile,
KernelFunctionDescription = "Takes text or HTML data in the clipboard and transforms it to a TXT file.")]
PasteAsTxtFile,
[PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsPngFile", IconGlyph = "\uE8B9", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Image | ClipboardFormat.ImageFile, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsPngFile)]
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "PasteAsPngFile",
IconGlyph = "\uE8B9",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Image,
IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsPngFile,
KernelFunctionDescription = "Takes an image in the clipboard and transforms it to a PNG file.")]
PasteAsPngFile,
[PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsHtmlFile", IconGlyph = "\uF6FA", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Html, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsHtmlFile)]
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "PasteAsHtmlFile",
IconGlyph = "\uF6FA",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Html,
IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsHtmlFile,
KernelFunctionDescription = "Takes HTML data in the clipboard and transforms it to an HTML file.")]
PasteAsHtmlFile,
[PasteFormatMetadata(IsCoreAction = false, IconGlyph = "\uE945", RequiresAIService = true, SupportedClipboardFormats = ClipboardFormat.Text)]
Custom,
[PasteFormatMetadata(
IsCoreAction = false,
IconGlyph = "\uE945",
RequiresAIService = true,
CanPreview = true,
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Image,
RequiresPrompt = true)]
KernelQuery,
[PasteFormatMetadata(
IsCoreAction = false,
IconGlyph = "\uE945",
RequiresAIService = true,
CanPreview = true,
SupportedClipboardFormats = ClipboardFormat.Text,
KernelFunctionDescription = "Takes input instructions and transforms clipboard text (not TXT files) with these input instructions, putting the result back on the clipboard. This uses AI to accomplish the task.",
RequiresPrompt = true)]
CustomTextTransformation,
}

View File

@@ -0,0 +1,152 @@
// 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.IO.Abstractions;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Models.KernelQueryCache;
using AdvancedPaste.Settings;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
namespace AdvancedPaste.Services;
/// <summary>
/// Implements <see cref="IKernelQueryCacheService"/> by only caching queries with prompts
/// that correspond to the user's custom actions or to the localized names of bundled actions.
/// This avoids potential privacy issues and prevents the cache from getting too large.
/// </summary>
public sealed class CustomActionKernelQueryCacheService : IKernelQueryCacheService
{
private const string PersistedCacheFileName = "kernelQueryCache.json";
private readonly HashSet<string> _cacheablePrompts = new(CacheKey.PromptComparer);
private readonly Dictionary<CacheKey, CacheValue> _memoryCache = [];
private readonly IUserSettings _userSettings;
private readonly IFileSystem _fileSystem;
private readonly SettingsUtils _settingsUtil;
private static string Version => Assembly.GetExecutingAssembly()?.GetName()?.Version?.ToString() ?? string.Empty;
public CustomActionKernelQueryCacheService(IUserSettings userSettings, IFileSystem fileSystem)
{
_userSettings = userSettings;
_fileSystem = fileSystem;
_settingsUtil = new SettingsUtils(fileSystem);
_userSettings.Changed += OnUserSettingsChanged;
UpdateCacheablePrompts();
_memoryCache = LoadPersistedCacheItems().Where(pair => pair.CacheKey != null)
.GroupBy(pair => pair.CacheKey, pair => pair.CacheValue)
.ToDictionary(group => group.Key, group => group.First());
RemoveInapplicableCacheKeys();
Logger.LogDebug($"Kernel query cache initialized with {_memoryCache.Count} items");
}
public async Task WriteAsync(CacheKey key, CacheValue value)
{
if (_cacheablePrompts.Contains(key.Prompt))
{
_memoryCache[key] = value;
await SaveAsync();
}
}
public CacheValue ReadOrNull(CacheKey key) => _memoryCache.GetValueOrDefault(key);
private List<PersistedCache.CacheItem> LoadPersistedCacheItems()
{
try
{
if (!_settingsUtil.SettingsExists(AdvancedPasteSettings.ModuleName, PersistedCacheFileName))
{
return [];
}
var jsonString = _fileSystem.File.ReadAllText(_settingsUtil.GetSettingsFilePath(AdvancedPasteSettings.ModuleName, PersistedCacheFileName));
var persistedCache = PersistedCache.FromJsonString(jsonString);
if (persistedCache.Version == Version)
{
return persistedCache.Items;
}
else
{
Logger.LogWarning($"Ignoring persisted kernel query cache; version mismatch - actual: {persistedCache.Version}, expected: {Version}");
return [];
}
}
catch (Exception ex)
{
Logger.LogError("Failed to load kernel query cache", ex);
return [];
}
}
private async void OnUserSettingsChanged(object sender, EventArgs e)
{
UpdateCacheablePrompts();
if (RemoveInapplicableCacheKeys())
{
await SaveAsync();
}
}
private void UpdateCacheablePrompts()
{
var localizedActionNames = from pair in PasteFormat.MetadataDict
let format = pair.Key
let metadata = pair.Value
where !string.IsNullOrEmpty(metadata.ResourceId)
where metadata.IsCoreAction || _userSettings.AdditionalActions.Contains(format)
select ResourceLoaderInstance.ResourceLoader.GetString(metadata.ResourceId);
var customActionPrompts = from customAction in _userSettings.CustomActions
select customAction.Prompt;
_cacheablePrompts.Clear();
_cacheablePrompts.UnionWith(localizedActionNames.Concat(customActionPrompts));
}
private bool RemoveInapplicableCacheKeys()
{
var keysToRemove = _memoryCache.Keys
.Where(key => !_cacheablePrompts.Contains(key.Prompt))
.ToList();
foreach (var key in keysToRemove)
{
_memoryCache.Remove(key);
}
return keysToRemove.Count > 0;
}
private async Task SaveAsync()
{
PersistedCache cache = new()
{
Version = Version,
Items = _memoryCache.Select(pair => new PersistedCache.CacheItem(pair.Key, pair.Value)).ToList(),
};
_settingsUtil.SaveSettings(cache.ToJsonString(), AdvancedPasteSettings.ModuleName, PersistedCacheFileName);
Logger.LogDebug($"Kernel query cache saved with {_memoryCache.Count} item(s)");
await Task.CompletedTask; // Async placeholder until _settingsUtil.SaveSettings has an async implementation
}
}

View 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 AdvancedPaste.Services;
public interface IAICredentialsProvider
{
bool IsConfigured { get; }
string Key { get; }
bool Refresh();
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
namespace AdvancedPaste.Services;
public interface ICustomTextTransformService
{
Task<string> TransformTextAsync(string prompt, string inputText);
}

View 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.Threading.Tasks;
using AdvancedPaste.Models.KernelQueryCache;
namespace AdvancedPaste.Services;
public interface IKernelQueryCacheService
{
Task WriteAsync(CacheKey key, CacheValue value);
CacheValue ReadOrNull(CacheKey key);
}

View 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.
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.Services;
public interface IKernelService
{
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery);
}

View File

@@ -3,11 +3,13 @@
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
using AdvancedPaste.Models;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.Services;
public interface IPasteFormatExecutor
{
Task<string> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source);
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source);
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
namespace AdvancedPaste.Services;
public interface IPromptModerationService
{
Task ValidateAsync(string fullPrompt);
}

View File

@@ -0,0 +1,280 @@
// 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.Text.Json;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Models.KernelQueryCache;
using AdvancedPaste.Telemetry;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.Services;
public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheService, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) : IKernelService
{
private const string PromptParameterName = "prompt";
private readonly IKernelQueryCacheService _queryCacheService = queryCacheService;
private readonly IPromptModerationService _promptModerationService = promptModerationService;
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
protected abstract string ModelName { get; }
protected abstract PromptExecutionSettings PromptExecutionSettings { get; }
protected abstract void AddChatCompletionService(IKernelBuilder kernelBuilder);
protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage);
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery)
{
Logger.LogTrace();
var kernel = CreateKernel();
kernel.SetDataPackageView(clipboardData);
CacheKey cacheKey = new() { Prompt = prompt, AvailableFormats = await clipboardData.GetAvailableFormatsAsync() };
var maybeCacheValue = _queryCacheService.ReadOrNull(cacheKey);
bool cacheUsed = maybeCacheValue != null;
ChatHistory chatHistory = [];
try
{
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt);
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
if (kernel.GetLastError() is Exception ex)
{
throw ex;
}
var outputPackage = kernel.GetDataPackage();
if (!(await outputPackage.GetView().HasUsableDataAsync()))
{
throw new InvalidOperationException("No data was returned from the kernel operation");
}
if (!cacheUsed)
{
await _queryCacheService.WriteAsync(cacheKey, new CacheValue(kernel.GetOrAddActionChain()));
}
Logger.LogDebug($"Kernel operation done: \n{FormatChatHistory(chatHistory)}");
return outputPackage;
}
catch (Exception ex)
{
Logger.LogError($"Error executing kernel operation", ex);
Logger.LogError($"Kernel operation Error: \n{FormatChatHistory(chatHistory)}");
AdvancedPasteSemanticKernelErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
PowerToysTelemetry.Log.WriteEvent(errorEvent);
if (ex is PasteActionException)
{
throw;
}
else
{
var message = ex is HttpOperationException httpOperationEx
? ErrorHelpers.TranslateErrorText((int?)httpOperationEx.StatusCode ?? -1)
: ResourceLoaderInstance.ResourceLoader.GetString("PasteError");
var lastAssistantMessage = chatHistory.LastOrDefault(chatMessage => chatMessage.Role == AuthorRole.Assistant)?.ToString();
throw new PasteActionException(message, innerException: ex, aiServiceMessage: lastAssistantMessage);
}
}
}
private static string GetFullPrompt(ChatHistory initialHistory)
{
if (initialHistory.Count == 0)
{
throw new ArgumentException("Chat history must not be empty", nameof(initialHistory));
}
int numSystemMessages = initialHistory.Count - 1;
var systemMessages = initialHistory.Take(numSystemMessages);
var userPromptMessage = initialHistory.Last();
if (systemMessages.Any(message => message.Role != AuthorRole.System))
{
throw new ArgumentException("Chat history must start with system messages", nameof(initialHistory));
}
if (userPromptMessage.Role != AuthorRole.User)
{
throw new ArgumentException("Chat history must end with a user message", nameof(initialHistory));
}
var newLine = Environment.NewLine;
var combinedSystemMessage = string.Join(newLine, systemMessages.Select(message => message.Content));
return $"{combinedSystemMessage}{newLine}{newLine}User instructions:{newLine}{userPromptMessage.Content}";
}
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt)
{
ChatHistory chatHistory = [];
chatHistory.AddSystemMessage("""
You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task.
You never need to ask permission, always try to do as the user asks. The user will only input one message and will not be available for further questions, so try your best.
The user will put in a request to format their clipboard data and you will fulfill it.
You will not directly see the output clipboard content, and do not need to provide it in the chat. You just need to do the transform operations as needed.
If you are unable to fulfill the request, end with an error message in the language of the user's request.
""");
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
chatHistory.AddUserMessage(prompt);
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory));
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel);
chatHistory.Add(chatResult);
var totalUsage = chatHistory.Select(GetAIServiceUsage)
.Aggregate(AIServiceUsage.Add);
return (chatHistory, totalUsage);
}
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteCachedActionChain(Kernel kernel, List<ActionChainItem> actionChain)
{
foreach (var item in actionChain)
{
if (item.Arguments.Count > 0)
{
await ExecutePromptTransformAsync(kernel, item.Format, item.Arguments[PromptParameterName]);
}
else
{
await ExecuteStandardTransformAsync(kernel, item.Format);
}
}
return ([], AIServiceUsage.None);
}
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));
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
var logEvent = new { telemetryEvent.CacheUsed, telemetryEvent.IsSavedQuery, telemetryEvent.PromptTokens, telemetryEvent.CompletionTokens, telemetryEvent.ModelName, telemetryEvent.ActionChain };
Logger.LogDebug($"{nameof(TransformClipboardAsync)} complete; {JsonSerializer.Serialize(logEvent)}");
}
private Kernel CreateKernel()
{
var kernelBuilder = Kernel.CreateBuilder();
AddChatCompletionService(kernelBuilder);
kernelBuilder.Plugins.AddFromFunctions("Actions", GetKernelFunctions());
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 Task<string> ExecutePromptTransformAsync(Kernel kernel, PasteFormats format, string prompt) =>
ExecuteTransformAsync(
kernel,
new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }),
async dataPackageView =>
{
var input = await dataPackageView.GetTextAsync();
string output = await GetPromptBasedOutput(format, prompt, input);
return DataPackageHelpers.CreateFromText(output);
});
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input) =>
format switch
{
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input),
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
};
private Task<string> ExecuteStandardTransformAsync(Kernel kernel, PasteFormats format) =>
ExecuteTransformAsync(
kernel,
new ActionChainItem(format, Arguments: []),
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView));
private static async Task<string> ExecuteTransformAsync(Kernel kernel, ActionChainItem actionChainItem, Func<DataPackageView, Task<DataPackage>> transformFunc)
{
kernel.GetOrAddActionChain().Add(actionChainItem);
kernel.SetLastError(null);
try
{
var input = kernel.GetDataPackageView();
var output = await transformFunc(input);
kernel.SetDataPackage(output);
return await kernel.GetDataFormatsAsync();
}
catch (Exception ex)
{
kernel.SetLastError(ex);
throw;
}
}
private string FormatChatHistory(ChatHistory chatHistory) =>
chatHistory.Count == 0 ? "[No chat history]" : string.Join(Environment.NewLine, chatHistory.Select(FormatChatMessage));
private string FormatChatMessage(ChatMessageContent chatMessage)
{
static string Redact(object data) =>
#if DEBUG
data?.ToString();
#else
"[Redacted]";
#endif
static string FormatKernelArguments(KernelArguments kernelArguments) =>
string.Join(", ", kernelArguments?.Select(argument => $"{argument.Key}: {Redact(argument.Value)}") ?? []);
static string FormatKernelContent(KernelContent kernelContent) =>
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
kernelContent switch
{
FunctionCallContent functionCallContent => $"{functionCallContent.FunctionName}({FormatKernelArguments(functionCallContent.Arguments)})",
FunctionResultContent functionResultContent => functionResultContent.FunctionName,
_ => kernelContent.ToString(),
};
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
var role = chatMessage.Role;
var content = string.Join(" / ", chatMessage.Items.Select(FormatKernelContent));
var redactedContent = role == AuthorRole.System || role == AuthorRole.Tool ? content : Redact(content);
var usage = GetAIServiceUsage(chatMessage);
var usageString = usage.HasUsage ? $" [{usage}]" : string.Empty;
return $"-> {role}: {redactedContent}{usageString}";
}
}

View File

@@ -0,0 +1,111 @@
// 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.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)
{
var fullPrompt = systemInstructions + "\n\n" + userMessage;
await _promptModerationService.ValidateAsync(fullPrompt);
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
var response = await azureAIClient.GetCompletionsAsync(
new()
{
DeploymentName = ModelName,
Prompts =
{
fullPrompt,
},
Temperature = 0.01F,
MaxTokens = 2000,
});
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)
{
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);
var usage = response.Usage;
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
var logEvent = new { telemetryEvent.PromptTokens, telemetryEvent.CompletionTokens, telemetryEvent.ModelName };
Logger.LogDebug($"{nameof(TransformTextAsync)} complete; {JsonSerializer.Serialize(logEvent)}");
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)
{
throw;
}
else
{
throw new PasteActionException(ErrorHelpers.TranslateErrorText((ex as RequestFailedException)?.Status ?? -1), ex);
}
}
}
}

View File

@@ -0,0 +1,34 @@
// 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;
}

View File

@@ -0,0 +1,41 @@
// 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.ClientModel;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using ManagedCommon;
using OpenAI.Moderations;
namespace AdvancedPaste.Services.OpenAI;
public sealed class PromptModerationService(IAICredentialsProvider aiCredentialsProvider) : IPromptModerationService
{
private const string ModelName = "omni-moderation-latest";
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
public async Task ValidateAsync(string fullPrompt)
{
try
{
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt);
var moderationResult = moderationClientResult.Value;
Logger.LogDebug($"{nameof(PromptModerationService)}.{nameof(ValidateAsync)} complete; {nameof(moderationResult.Flagged)}={moderationResult.Flagged}");
if (moderationResult.Flagged)
{
throw new PasteActionModeratedException();
}
}
catch (ClientResultException ex)
{
throw new PasteActionException(ErrorHelpers.TranslateErrorText(ex.Status), ex);
}
}
}

View 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;
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;
}
}
}

View File

@@ -3,74 +3,41 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Windows.ApplicationModel.DataTransfer;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace AdvancedPaste.Services;
public sealed class PasteFormatExecutor(AICompletionsHelper aiHelper) : IPasteFormatExecutor
public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTextTransformService customTextTransformService) : IPasteFormatExecutor
{
private readonly AICompletionsHelper _aiHelper = aiHelper;
private readonly IKernelService _kernelService = kernelService;
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
public async Task<string> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
{
if (!pasteFormat.IsEnabled)
{
return null;
}
WriteTelemetry(pasteFormat.Format, source);
var format = pasteFormat.Format;
return await ExecutePasteFormatCoreAsync(pasteFormat, Clipboard.GetContent());
}
WriteTelemetry(format, source);
private async Task<string> ExecutePasteFormatCoreAsync(PasteFormat pasteFormat, DataPackageView clipboardData)
{
switch (pasteFormat.Format)
{
case PasteFormats.PlainText:
ToPlainText(clipboardData);
return null;
var clipboardData = Clipboard.GetContent();
case PasteFormats.Markdown:
ToMarkdown(clipboardData);
return null;
case PasteFormats.Json:
ToJson(clipboardData);
return null;
case PasteFormats.ImageToText:
await ImageToTextAsync(clipboardData);
return null;
case PasteFormats.PasteAsTxtFile:
await ToTxtFileAsync(clipboardData);
return null;
case PasteFormats.PasteAsPngFile:
await ToPngFileAsync(clipboardData);
return null;
case PasteFormats.PasteAsHtmlFile:
await ToHtmlFileAsync(clipboardData);
return null;
case PasteFormats.Custom:
return await ToCustomAsync(pasteFormat.Prompt, clipboardData);
default:
throw new ArgumentException($"Unknown paste format {pasteFormat.Format}", nameof(pasteFormat));
}
// Run on thread-pool; although we use Async routines consistently, some actions still occasionally take a long time without yielding.
return await Task.Run(async () =>
pasteFormat.Format switch
{
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery),
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync())),
_ => await TransformHelpers.TransformAsync(format, clipboardData),
});
}
private static void WriteTelemetry(PasteFormats format, PasteActionSource source)
@@ -93,161 +60,4 @@ public sealed class PasteFormatExecutor(AICompletionsHelper aiHelper) : IPasteFo
throw new ArgumentOutOfRangeException(nameof(format));
}
}
private void ToPlainText(DataPackageView clipboardData)
{
Logger.LogTrace();
SetClipboardTextContent(MarkdownHelper.PasteAsPlainTextFromClipboard(clipboardData));
}
private void ToMarkdown(DataPackageView clipboardData)
{
Logger.LogTrace();
SetClipboardTextContent(MarkdownHelper.ToMarkdown(clipboardData));
}
private void ToJson(DataPackageView clipboardData)
{
Logger.LogTrace();
SetClipboardTextContent(JsonHelper.ToJsonFromXmlOrCsv(clipboardData));
}
private async Task ImageToTextAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
var bitmap = await ClipboardHelper.GetClipboardImageContentAsync(clipboardData);
var text = await OcrHelpers.ExtractTextAsync(bitmap);
SetClipboardTextContent(text);
}
private async Task ToPngFileAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
var clipboardBitmap = await ClipboardHelper.GetClipboardImageContentAsync(clipboardData);
using var pngStream = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, pngStream);
encoder.SetSoftwareBitmap(clipboardBitmap);
await encoder.FlushAsync();
await SetClipboardFileContentAsync(pngStream.AsStreamForRead(), "png");
}
private async Task ToTxtFileAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
var text = await ClipboardHelper.GetClipboardTextOrHtmlTextAsync(clipboardData);
await SetClipboardFileContentAsync(text, "txt");
}
private async Task ToHtmlFileAsync(DataPackageView clipboardData)
{
Logger.LogTrace();
var cfHtml = await ClipboardHelper.GetClipboardHtmlContentAsync(clipboardData);
var html = RemoveHtmlMetadata(cfHtml);
await SetClipboardFileContentAsync(html, "html");
}
/// <summary>
/// Removes leading CF_HTML metadata from HTML clipboard data.
/// See: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
/// </summary>
private static string RemoveHtmlMetadata(string cfHtml)
{
int? GetIntTagValue(string tagName)
{
var tagNameWithColon = tagName + ":";
int tagStartPos = cfHtml.IndexOf(tagNameWithColon, StringComparison.InvariantCulture);
const int tagValueLength = 10;
return tagStartPos != -1 && int.TryParse(cfHtml.AsSpan(tagStartPos + tagNameWithColon.Length, tagValueLength), CultureInfo.InvariantCulture, out int result) ? result : null;
}
var startFragmentIndex = GetIntTagValue("StartFragment");
var endFragmentIndex = GetIntTagValue("EndFragment");
return (startFragmentIndex == null || endFragmentIndex == null) ? cfHtml : cfHtml[startFragmentIndex.Value..endFragmentIndex.Value];
}
private static async Task SetClipboardFileContentAsync(string data, string fileExtension)
{
if (string.IsNullOrEmpty(data))
{
throw new ArgumentException($"Empty value in {nameof(SetClipboardFileContentAsync)}", nameof(data));
}
var path = GetPasteAsFileTempFilePath(fileExtension);
await File.WriteAllTextAsync(path, data);
await ClipboardHelper.SetClipboardFileContentAsync(path);
}
private static async Task SetClipboardFileContentAsync(Stream stream, string fileExtension)
{
var path = GetPasteAsFileTempFilePath(fileExtension);
using var fileStream = File.Create(path);
await stream.CopyToAsync(fileStream);
await ClipboardHelper.SetClipboardFileContentAsync(path);
}
private static string GetPasteAsFileTempFilePath(string fileExtension)
{
var prefix = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsFile_FilePrefix");
var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
return Path.Combine(Path.GetTempPath(), $"{prefix}{timestamp}.{fileExtension}");
}
private async Task<string> ToCustomAsync(string prompt, DataPackageView clipboardData)
{
Logger.LogTrace();
if (string.IsNullOrWhiteSpace(prompt))
{
return string.Empty;
}
if (!clipboardData.Contains(StandardDataFormats.Text))
{
Logger.LogWarning("Clipboard does not contain text data");
return string.Empty;
}
var currentClipboardText = await clipboardData.GetTextAsync();
if (string.IsNullOrWhiteSpace(currentClipboardText))
{
Logger.LogWarning("Clipboard has no usable text data");
return string.Empty;
}
var aiResponse = await Task.Run(() => _aiHelper.AIFormatString(prompt, currentClipboardText));
return aiResponse.ApiRequestStatus == (int)HttpStatusCode.OK
? aiResponse.Response
: throw new PasteActionException(TranslateErrorText(aiResponse.ApiRequestStatus));
}
private void SetClipboardTextContent(string content)
{
if (!string.IsNullOrEmpty(content))
{
ClipboardHelper.SetClipboardTextContent(content);
}
}
private static string TranslateErrorText(int apiRequestStatus) => (HttpStatusCode)apiRequestStatus switch
{
HttpStatusCode.TooManyRequests => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests"),
HttpStatusCode.Unauthorized => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized"),
HttpStatusCode.OK => string.Empty,
_ => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + apiRequestStatus.ToString(CultureInfo.InvariantCulture),
};
}

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