mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-09 03:49:52 +02:00
Compare commits
3 Commits
main
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
380669dd81 | ||
|
|
d24569d49c | ||
|
|
94cbea91a0 |
@@ -1 +0,0 @@
|
||||
../.github/copilot-instructions.md
|
||||
@@ -1 +0,0 @@
|
||||
../.github/agents
|
||||
@@ -1 +0,0 @@
|
||||
../.github/prompts
|
||||
@@ -1 +0,0 @@
|
||||
../.github/instructions
|
||||
@@ -1 +0,0 @@
|
||||
../.github/skills
|
||||
@@ -1,5 +1,5 @@
|
||||
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
|
||||
properties:
|
||||
resources:
|
||||
- resource: Microsoft.Windows.Settings/WindowsSettings
|
||||
@@ -13,11 +13,11 @@ properties:
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
id: vsPackage
|
||||
directives:
|
||||
description: Install Visual Studio 2026 Enterprise (Any edition will work)
|
||||
description: Install Visual Studio 2022 Enterprise (Any edition will work)
|
||||
# Requires elevation for the set operation
|
||||
securityContext: elevated
|
||||
settings:
|
||||
id: Microsoft.VisualStudio.Enterprise
|
||||
id: Microsoft.VisualStudio.2022.Enterprise
|
||||
source: winget
|
||||
- resource: Microsoft.VisualStudio.DSC/VSComponents
|
||||
dependsOn:
|
||||
@@ -29,7 +29,7 @@ properties:
|
||||
securityContext: elevated
|
||||
settings:
|
||||
productId: Microsoft.VisualStudio.Product.Enterprise
|
||||
channelId: VisualStudio.18.Release
|
||||
channelId: VisualStudio.17.Release
|
||||
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
|
||||
configurationVersion: 0.2.0
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
|
||||
properties:
|
||||
resources:
|
||||
- resource: Microsoft.Windows.Settings/WindowsSettings
|
||||
@@ -13,11 +13,11 @@ properties:
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
id: vsPackage
|
||||
directives:
|
||||
description: Install Visual Studio 2026 Professional (Any edition will work)
|
||||
description: Install Visual Studio 2022 Professional (Any edition will work)
|
||||
# Requires elevation for the set operation
|
||||
securityContext: elevated
|
||||
settings:
|
||||
id: Microsoft.VisualStudio.Professional
|
||||
id: Microsoft.VisualStudio.2022.Professional
|
||||
source: winget
|
||||
- resource: Microsoft.VisualStudio.DSC/VSComponents
|
||||
dependsOn:
|
||||
@@ -29,7 +29,7 @@ properties:
|
||||
securityContext: elevated
|
||||
settings:
|
||||
productId: Microsoft.VisualStudio.Product.Professional
|
||||
channelId: VisualStudio.18.Release
|
||||
channelId: VisualStudio.17.Release
|
||||
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
|
||||
configurationVersion: 0.2.0
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#getting-started
|
||||
# Reference: https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md#compiling-powertoys
|
||||
properties:
|
||||
resources:
|
||||
- resource: Microsoft.Windows.Settings/WindowsSettings
|
||||
@@ -13,11 +13,11 @@ properties:
|
||||
- resource: Microsoft.WinGet.DSC/WinGetPackage
|
||||
id: vsPackage
|
||||
directives:
|
||||
description: Install Visual Studio 2026 Community (Any edition will work)
|
||||
description: Install Visual Studio 2022 Community (Any edition will work)
|
||||
# Requires elevation for the set operation
|
||||
securityContext: elevated
|
||||
settings:
|
||||
id: Microsoft.VisualStudio.Community
|
||||
id: Microsoft.VisualStudio.2022.Community
|
||||
source: winget
|
||||
- resource: Microsoft.VisualStudio.DSC/VSComponents
|
||||
dependsOn:
|
||||
@@ -29,7 +29,7 @@ properties:
|
||||
securityContext: elevated
|
||||
settings:
|
||||
productId: Microsoft.VisualStudio.Product.Community
|
||||
channelId: VisualStudio.18.Release
|
||||
channelId: VisualStudio.17.Release
|
||||
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
|
||||
configurationVersion: 0.2.0
|
||||
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -40,6 +40,7 @@ body:
|
||||
- Other (please specify in "Steps to Reproduce")
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Area(s) with issue?
|
||||
@@ -57,7 +58,6 @@ body:
|
||||
- Environment Variables
|
||||
- FancyZones
|
||||
- FancyZones Editor
|
||||
- Grab And Move
|
||||
- File Locksmith
|
||||
- "File Explorer: Preview Pane"
|
||||
- "File Explorer: Thumbnail preview"
|
||||
@@ -70,7 +70,6 @@ body:
|
||||
- Mouse Without Borders
|
||||
- New+
|
||||
- Peek
|
||||
- Power Display
|
||||
- PowerRename
|
||||
- PowerToys Run
|
||||
- Quick Accent
|
||||
@@ -107,13 +106,7 @@ body:
|
||||
placeholder: What happened instead?
|
||||
validations:
|
||||
required: false
|
||||
- type: upload
|
||||
id: bugreportfile
|
||||
attributes:
|
||||
label: Upload Bug Report ZIP-file
|
||||
description: Right-clicking the PowerToys tray icon in the taskbar and selecting “Report bug” generates a ZIP file containing diagnostic information about your setup and PowerToys logs, helping us better understand and troubleshoot the issue.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- id: additionalInfo
|
||||
type: textarea
|
||||
attributes:
|
||||
|
||||
97
.github/actions/spell-check/allow/code.txt
vendored
97
.github/actions/spell-check/allow/code.txt
vendored
@@ -1,7 +1,6 @@
|
||||
# COLORS
|
||||
|
||||
argb
|
||||
Bgr
|
||||
bgra
|
||||
BLACKONWHITE
|
||||
BLUEGRAY
|
||||
@@ -17,9 +16,8 @@ LIGHTTURQUOISE
|
||||
NCol
|
||||
OLIVEGREEN
|
||||
PALEBLUE
|
||||
pargb
|
||||
pbgra
|
||||
SRGBTo
|
||||
PArgb
|
||||
Pbgra
|
||||
WHITEONBLACK
|
||||
|
||||
|
||||
@@ -30,7 +28,6 @@ RUS
|
||||
|
||||
AYUV
|
||||
bak
|
||||
HDP
|
||||
Bcl
|
||||
bgcode
|
||||
Deflatealgorithm
|
||||
@@ -41,7 +38,6 @@ Gbps
|
||||
gcode
|
||||
Heatshrink
|
||||
Mbits
|
||||
Kbits
|
||||
MBs
|
||||
mkv
|
||||
msix
|
||||
@@ -51,7 +47,6 @@ resw
|
||||
resx
|
||||
srt
|
||||
Stereolithography
|
||||
taskmgr
|
||||
terabyte
|
||||
UYVY
|
||||
xbf
|
||||
@@ -102,7 +97,6 @@ Yubico
|
||||
Perplexity
|
||||
Groq
|
||||
svgl
|
||||
devhome
|
||||
|
||||
# KEYS
|
||||
|
||||
@@ -128,7 +122,6 @@ HOLDSPACE
|
||||
HOLDBACKSPACE
|
||||
IDIGNORE
|
||||
KBDLLHOOKSTRUCT
|
||||
keydowns
|
||||
keyevent
|
||||
LAlt
|
||||
LBUTTON
|
||||
@@ -187,12 +180,6 @@ xmlutil
|
||||
# Prefix
|
||||
pcs
|
||||
|
||||
# EXPRTK / C++ MATH
|
||||
|
||||
ifunction
|
||||
isinf
|
||||
isnan
|
||||
|
||||
# User32.SYSTEM_METRICS_INDEX.cs
|
||||
|
||||
CLEANBOOT
|
||||
@@ -308,8 +295,6 @@ pwa
|
||||
|
||||
AOT
|
||||
Aot
|
||||
ify
|
||||
TFM
|
||||
|
||||
# YML
|
||||
onefuzz
|
||||
@@ -317,7 +302,6 @@ onefuzz
|
||||
# NameInCode
|
||||
leilzh
|
||||
mengyuanchen
|
||||
contoso
|
||||
|
||||
# DllName
|
||||
testhost
|
||||
@@ -329,7 +313,6 @@ xef
|
||||
xes
|
||||
PACKAGEVERSIONNUMBER
|
||||
APPXMANIFESTVERSION
|
||||
PROGMAN
|
||||
|
||||
# MRU lists
|
||||
CACHEWRITE
|
||||
@@ -337,61 +320,8 @@ MRUCMPPROC
|
||||
MRUINFO
|
||||
REGSTR
|
||||
|
||||
#Xaml
|
||||
NVI
|
||||
Storyboards
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
DEFAULTTONEAREST
|
||||
INVOKEIDLIST
|
||||
LCMAP
|
||||
MEMORYSTATUSEX
|
||||
ABE
|
||||
Mdt
|
||||
HTCAPTION
|
||||
POSCHANGED
|
||||
QPC
|
||||
QUERYPOS
|
||||
SETAUTOHIDEBAR
|
||||
ULW
|
||||
WINDOWPOS
|
||||
WINEVENTPROC
|
||||
WORKERW
|
||||
FULLSCREENAPP
|
||||
ACLO
|
||||
CACLI
|
||||
DOENVSUBST
|
||||
FILESYSONLY
|
||||
URLIS
|
||||
WAITTIMEOUT
|
||||
DEFAULTTONEAREST
|
||||
DWRITE
|
||||
LWIN
|
||||
VCENTER
|
||||
VREDRAW
|
||||
|
||||
# COM/WinRT interface prefixes and type fragments
|
||||
BAlt
|
||||
BShift
|
||||
Cmanifest
|
||||
Cmodule
|
||||
Cuuid
|
||||
Dng
|
||||
IApplication
|
||||
IDisposable
|
||||
IEnum
|
||||
IFolder
|
||||
IInitialize
|
||||
IMemory
|
||||
IOle
|
||||
ipreview
|
||||
IProperty
|
||||
IShell
|
||||
ithumbnail
|
||||
IVirtual
|
||||
|
||||
# Test frameworks
|
||||
MSTEST
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
@@ -403,12 +333,6 @@ YYY
|
||||
# Unicode
|
||||
precomposed
|
||||
|
||||
# names of characters
|
||||
zwsp
|
||||
|
||||
# mermaid
|
||||
autonumber
|
||||
|
||||
# GitHub issue/PR commands
|
||||
azp
|
||||
feedbackhub
|
||||
@@ -418,20 +342,3 @@ reportbug
|
||||
#ffmpeg
|
||||
crf
|
||||
nostdin
|
||||
|
||||
# Performance counter keys
|
||||
engtype
|
||||
Nonpaged
|
||||
|
||||
# XAML
|
||||
Untargeted
|
||||
|
||||
# Program names
|
||||
SEARCHHOST
|
||||
SHELLEXPERIENCEHOST
|
||||
SHELLHOST
|
||||
STARTMENUEXPERIENCEHOST
|
||||
WIDGETBOARD
|
||||
|
||||
# URIs
|
||||
actioncenter
|
||||
|
||||
8
.github/actions/spell-check/allow/names.txt
vendored
8
.github/actions/spell-check/allow/names.txt
vendored
@@ -178,9 +178,7 @@ Taras
|
||||
TBM
|
||||
Teutsch
|
||||
tilovell
|
||||
traies
|
||||
Triet
|
||||
udit
|
||||
urnotdfs
|
||||
vednig
|
||||
waaverecords
|
||||
@@ -194,7 +192,6 @@ ycv
|
||||
yeelam
|
||||
Yuniardi
|
||||
yuyoyuppe
|
||||
zadjii
|
||||
Zeol
|
||||
Zhao
|
||||
Zhaopeng
|
||||
@@ -209,8 +206,6 @@ Bilibili
|
||||
BVID
|
||||
capturevideosample
|
||||
cmdow
|
||||
contoso
|
||||
Contoso
|
||||
Controlz
|
||||
cortana
|
||||
devhints
|
||||
@@ -226,7 +221,6 @@ Moq
|
||||
mozilla
|
||||
mspaint
|
||||
Newtonsoft
|
||||
NVIDIA
|
||||
onenote
|
||||
openai
|
||||
Quickime
|
||||
@@ -234,7 +228,6 @@ regedit
|
||||
roslyn
|
||||
Skia
|
||||
Spotify
|
||||
taskmgr
|
||||
tldr
|
||||
Vanara
|
||||
wangyi
|
||||
@@ -250,3 +243,4 @@ xamlstyler
|
||||
Xavalon
|
||||
Xbox
|
||||
Youdao
|
||||
zadjii
|
||||
|
||||
422
.github/actions/spell-check/allow/zoomit.txt
vendored
422
.github/actions/spell-check/allow/zoomit.txt
vendored
@@ -1,422 +0,0 @@
|
||||
accelscroll
|
||||
acq
|
||||
ADDTO
|
||||
ADDTOOL
|
||||
adr
|
||||
Adr
|
||||
ALWAYSTIP
|
||||
APPLYTOSUBMENUS
|
||||
ARCHMASK
|
||||
archs
|
||||
AUDCLNT
|
||||
autocorr
|
||||
avx
|
||||
axisdefer
|
||||
axisflip
|
||||
axisstart
|
||||
backlight
|
||||
BEOS
|
||||
bfi
|
||||
BFIN
|
||||
bfly
|
||||
BGRX
|
||||
bitmaps
|
||||
bitrev
|
||||
blits
|
||||
Borgerding
|
||||
Borland
|
||||
breakc
|
||||
BREAKSCR
|
||||
BUFFERFLAGS
|
||||
bugzilla
|
||||
Cands
|
||||
capturepath
|
||||
cbs
|
||||
centiseconds
|
||||
cexp
|
||||
cfx
|
||||
cfy
|
||||
cgem
|
||||
cifx
|
||||
cify
|
||||
CLASSW
|
||||
coeffs
|
||||
colblocks
|
||||
constantbuffer
|
||||
coprime
|
||||
cpuid
|
||||
cpx
|
||||
CREATEDIBSECTION
|
||||
CREATESTRUCTW
|
||||
crossfades
|
||||
Ctl
|
||||
CTLCOLOR
|
||||
CTLCOLORBTN
|
||||
CTLCOLORDLG
|
||||
CTLCOLOREDIT
|
||||
CTLCOLORLISTBOX
|
||||
CTrim
|
||||
CVTEPI
|
||||
DBuffer
|
||||
dcl
|
||||
dct
|
||||
ddx
|
||||
ddy
|
||||
Deinterleave
|
||||
denoise
|
||||
denoised
|
||||
DEVSOURCE
|
||||
DFCS
|
||||
DIVSCALAR
|
||||
DJGPP
|
||||
dlg
|
||||
dlu
|
||||
dnn
|
||||
DONTCARE
|
||||
downsample
|
||||
DRAWITEM
|
||||
DRAWITEMSTRUCT
|
||||
droppedband
|
||||
Droppedband
|
||||
DSPs
|
||||
dsum
|
||||
dupburst
|
||||
dupsegments
|
||||
DWLP
|
||||
eband
|
||||
ebx
|
||||
ECX
|
||||
EDITCONTROL
|
||||
EDSP
|
||||
emmintrin
|
||||
EMX
|
||||
ENABLEHOOK
|
||||
endloop
|
||||
ENDOFSTREAM
|
||||
ener
|
||||
enh
|
||||
ettings
|
||||
expectedlock
|
||||
expf
|
||||
fabs
|
||||
fabsf
|
||||
facbuf
|
||||
fastscroll
|
||||
FDE
|
||||
ffast
|
||||
FIXDIV
|
||||
floorf
|
||||
fmadd
|
||||
fout
|
||||
fstride
|
||||
fxc
|
||||
GETCHANNELRECT
|
||||
GETCHECK
|
||||
GETCOUNT
|
||||
GETDISPINFO
|
||||
GETSCREENSAVEACTIVE
|
||||
GETSCREENSAVETIMEOUT
|
||||
GETTHUMBRECT
|
||||
GIFs
|
||||
glu
|
||||
groupshared
|
||||
gru
|
||||
hcfdark
|
||||
hcfwhitespace
|
||||
hlsl
|
||||
Hsieh
|
||||
hstride
|
||||
HTBOTTOMRIGHT
|
||||
HTHEME
|
||||
htol
|
||||
ICONINFORMATION
|
||||
ICONWARNING
|
||||
idct
|
||||
IDIn
|
||||
IDISHWND
|
||||
ifft
|
||||
igc
|
||||
ilog
|
||||
imad
|
||||
imax
|
||||
imin
|
||||
immintrin
|
||||
Inj
|
||||
interp
|
||||
inttypes
|
||||
ishl
|
||||
itof
|
||||
jumprecover
|
||||
kfft
|
||||
kheight
|
||||
kissfft
|
||||
KSDATAFORMAT
|
||||
ksize
|
||||
ktime
|
||||
lastg
|
||||
latestcapture
|
||||
ldx
|
||||
LEFTNOWORDWRAP
|
||||
legitjumps
|
||||
lenmem
|
||||
letterbox
|
||||
lld
|
||||
lldx
|
||||
llu
|
||||
llums
|
||||
logfont
|
||||
lookback
|
||||
lpc
|
||||
lpcnet
|
||||
LPNMHDR
|
||||
LPNMTTDISPINFO
|
||||
lround
|
||||
lte
|
||||
luma
|
||||
Luma
|
||||
maj
|
||||
manualdrop
|
||||
maskcache
|
||||
maxabs
|
||||
maxcorr
|
||||
MAXFACTORS
|
||||
maxperiod
|
||||
maxstep
|
||||
memalign
|
||||
memid
|
||||
memneeded
|
||||
MENUINFO
|
||||
MFSTARTUP
|
||||
mfxhw
|
||||
mic
|
||||
middledrop
|
||||
minperiod
|
||||
MIPSr
|
||||
MJPEG
|
||||
MMRESULT
|
||||
momentumreversal
|
||||
movc
|
||||
mrate
|
||||
mrt
|
||||
MULBYSCALAR
|
||||
MULC
|
||||
MWERKS
|
||||
mycfg
|
||||
narrowstrip
|
||||
nbak
|
||||
nbytes
|
||||
ncapture
|
||||
nchw
|
||||
ncm
|
||||
nduplicates
|
||||
nfft
|
||||
NHWC
|
||||
niterations
|
||||
nmonitor
|
||||
nnet
|
||||
NONCLIENTMETRICS
|
||||
NONOTIFY
|
||||
nonvle
|
||||
normf
|
||||
nredraw
|
||||
nstop
|
||||
nsubpixel
|
||||
ntorn
|
||||
numthreads
|
||||
nvw
|
||||
Octasic
|
||||
osc
|
||||
OSCE
|
||||
ovflw
|
||||
OWNERDRAW
|
||||
PBGRA
|
||||
periodictrap
|
||||
pfdc
|
||||
pillarbox
|
||||
playhead
|
||||
pnmh
|
||||
pointerreuse
|
||||
PPW
|
||||
prereq
|
||||
PSHR
|
||||
pstdint
|
||||
PSWA
|
||||
pwfx
|
||||
QCONST
|
||||
qpc
|
||||
Qpc
|
||||
quantums
|
||||
qweight
|
||||
RCSEGMODEL
|
||||
RCZOOMITSCR
|
||||
readback
|
||||
READERF
|
||||
realcapture
|
||||
REFKNOWNFOLDERID
|
||||
relu
|
||||
reposted
|
||||
RETURNCMD
|
||||
rnn
|
||||
rnnoise
|
||||
rotateleft
|
||||
rsqrt
|
||||
rtcd
|
||||
RTEXT
|
||||
RTH
|
||||
rtvs
|
||||
SCALEIN
|
||||
SCALEOUT
|
||||
SCREENSAVE
|
||||
SCRNSAVE
|
||||
SCRNSAVECONFIGURE
|
||||
scrnsavw
|
||||
Scrnsavw
|
||||
scrollramp
|
||||
SCROLLSIZEGRIP
|
||||
selfie
|
||||
selftest
|
||||
SETBARCOLOR
|
||||
SETBKCOLOR
|
||||
SETDEFID
|
||||
SETRECT
|
||||
SETSCREENSAVETIMEOUT
|
||||
SETTIPSIDE
|
||||
sgem
|
||||
sgemv
|
||||
sgv
|
||||
SHAREMODE
|
||||
SHAREVIOLATION
|
||||
shortlist
|
||||
simde
|
||||
siv
|
||||
slowthenfast
|
||||
smallstart
|
||||
SNIPOCR
|
||||
softmax
|
||||
sqrtf
|
||||
SROUND
|
||||
srvs
|
||||
ssi
|
||||
startuprecovery
|
||||
stdint
|
||||
stf
|
||||
stopafter
|
||||
STREAMFLAGS
|
||||
SUBFROM
|
||||
subias
|
||||
submix
|
||||
sxx
|
||||
sxy
|
||||
symbian
|
||||
synthesising
|
||||
syy
|
||||
tallportal
|
||||
TBTS
|
||||
tci
|
||||
tcsicmp
|
||||
TEXTCALLBACK
|
||||
TEXTMETRIC
|
||||
tgsm
|
||||
THIRDPARTY
|
||||
tinystep
|
||||
tme
|
||||
toolbars
|
||||
TOOLINFO
|
||||
TRACKMOUSEEVENT
|
||||
TRIANGLELIST
|
||||
TTM
|
||||
TTN
|
||||
TWID
|
||||
UADD
|
||||
uav
|
||||
uavs
|
||||
uge
|
||||
Unadvise
|
||||
upscaled
|
||||
upscales
|
||||
USUB
|
||||
utof
|
||||
vad
|
||||
vaddq
|
||||
vaddvq
|
||||
valgrind
|
||||
Valin
|
||||
vandq
|
||||
vblank
|
||||
vcgeq
|
||||
vdup
|
||||
vectorizer
|
||||
VERTID
|
||||
VIDCAP
|
||||
vld
|
||||
vle
|
||||
Vle
|
||||
VLE
|
||||
vminq
|
||||
vmlal
|
||||
vmull
|
||||
vqaddq
|
||||
VSHR
|
||||
vshrn
|
||||
vsntprintf
|
||||
vsnwprintf
|
||||
vsync
|
||||
WASAPI
|
||||
WAVEFORMATEX
|
||||
WAVEFORMATEXTENSIBLE
|
||||
webcam
|
||||
Webcam
|
||||
webcams
|
||||
Wextra
|
||||
wfopen
|
||||
WGC
|
||||
wideportal
|
||||
wil
|
||||
WMU
|
||||
wrapjump
|
||||
wtol
|
||||
WTSSESSION
|
||||
WTSUn
|
||||
wxyz
|
||||
xchg
|
||||
xcorr
|
||||
XEnd
|
||||
Xfl
|
||||
Xiang
|
||||
Xiph
|
||||
xmmintrin
|
||||
xptr
|
||||
xshift
|
||||
XStart
|
||||
XStep
|
||||
xxxy
|
||||
xxyx
|
||||
xxyz
|
||||
xyw
|
||||
xywx
|
||||
xyxx
|
||||
xyxz
|
||||
xyzw
|
||||
xyzx
|
||||
xzwx
|
||||
xzxx
|
||||
Yfl
|
||||
YInternal
|
||||
yshift
|
||||
YUV
|
||||
yyyx
|
||||
yyzw
|
||||
yzw
|
||||
yzwy
|
||||
yzyy
|
||||
Zhou
|
||||
Zhu
|
||||
ZMBS
|
||||
zncc
|
||||
Zncc
|
||||
ZNCC
|
||||
zrh
|
||||
zwzz
|
||||
zyzw
|
||||
zzwz
|
||||
zzzw
|
||||
73
.github/actions/spell-check/candidate.patterns
vendored
73
.github/actions/spell-check/candidate.patterns
vendored
@@ -1,3 +1,6 @@
|
||||
# D2D
|
||||
#D?2D
|
||||
|
||||
# Repeated letters
|
||||
\b([a-z])\g{-1}{2,}\b
|
||||
|
||||
@@ -11,7 +14,7 @@
|
||||
^.*\b[Cc][Ss][Pp][Ee][Ll]{2}:\s*[Dd][Ii][Ss][Aa][Bb][Ll][Ee]-[Ll][Ii][Nn][Ee]\b
|
||||
|
||||
# copyright
|
||||
Copyright (?:\([Cc]\)|©|)(?:[-\d, ]|and)+(?: [A-Z][a-z]+ [A-Z][a-z]+,?)+
|
||||
Copyright (?:\([Cc]\)|)(?:[-\d, ]|and)+(?: [A-Z][a-z]+ [A-Z][a-z]+,?)+
|
||||
|
||||
# patch hunk comments
|
||||
^@@ -\d+(?:,\d+|) \+\d+(?:,\d+|) @@ .*
|
||||
@@ -19,10 +22,10 @@ Copyright (?:\([Cc]\)|©|)(?:[-\d, ]|and)+(?: [A-Z][a-z]+ [A-Z][a-z]+,?)+
|
||||
index (?:[0-9a-z]{7,40},|)[0-9a-z]{7,40}\.\.[0-9a-z]{7,40}
|
||||
|
||||
# file permissions
|
||||
(?:^|['"`\s])(?!-+\s)[-bcdLlpsw](?:[-r][-w][-Ssx]){2}[-r][-w][-SsTtx]\+?['"`\s]
|
||||
['"`\s][-bcdLlpsw](?:[-r][-w][-Ssx]){2}[-r][-w][-SsTtx]\+?['"`\s]
|
||||
|
||||
# css fonts
|
||||
\bfont(?:-family(?:[-\w+]*)|):[^;}]+
|
||||
\bfont(?:-family|):[^;}]+
|
||||
|
||||
# css url wrappings
|
||||
\burl\([^)]+\)
|
||||
@@ -87,9 +90,6 @@ arn:aws:[-/:\w]+
|
||||
# AWS VPC
|
||||
vpc-\w+
|
||||
|
||||
# Azure AD
|
||||
\baad\.\w{48}\b
|
||||
|
||||
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
|
||||
# YouTube url
|
||||
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
|
||||
@@ -171,7 +171,7 @@ themes\.googleusercontent\.com/static/fonts/[^/\s"]+/v\d+/[^.]+.
|
||||
GHSA(?:-[0-9a-z]{4}){3}
|
||||
|
||||
# GitHub actions
|
||||
\buses:\s+(['"]?)[-\w.]+/[-\w./]+@[-\w.]+\g{-1}
|
||||
\buses:\s+[-\w.]+/[-\w./]+@[-\w.]+
|
||||
|
||||
# GitLab commit
|
||||
\bgitlab\.[^/\s"]*/\S+/\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\b
|
||||
@@ -240,7 +240,7 @@ accounts\.binance\.com/[a-z/]*oauth/authorize\?[-0-9a-zA-Z&%]*
|
||||
\bmedium\.com/@?[^/\s"]+/[-\w]+
|
||||
|
||||
# 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|docs|msdn2?|research)\.|)microsoft|blogs\.msdn)\.co(?:m|\.\w\w)/[-_a-zA-Z0-9()=./%]*
|
||||
# powerbi
|
||||
\bapp\.powerbi\.com/reportEmbed/[^"' ]*
|
||||
# vs devops
|
||||
@@ -414,7 +414,7 @@ ipfs://[0-9a-zA-Z]{3,}
|
||||
\bgetopts\s+(?:"[^"]+"|'[^']+')
|
||||
|
||||
# ANSI color codes
|
||||
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[(?:\d+(?:;\d+)*|)m
|
||||
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+)*m
|
||||
|
||||
# URL escaped characters
|
||||
%[0-9A-F][A-F](?=[A-Za-z])
|
||||
@@ -431,7 +431,7 @@ sha\d+:[0-9a-f]*?[a-f]{3,}[0-9a-f]*
|
||||
# sha-... -- uses a fancy capture
|
||||
(\\?['"]|")[0-9a-f]{40,}\g{-1}
|
||||
# hex runs
|
||||
\b(?=(?:[a-fA-F]{0,2}\d)*[a-fA-F]{3})[0-9a-fA-F]{16,}\b
|
||||
\b[0-9a-fA-F]{16,}\b
|
||||
# hex in url queries
|
||||
=[0-9a-fA-F]*?(?:[A-F]{3,}|[a-f]{3,})[0-9a-fA-F]*?&
|
||||
# ssh
|
||||
@@ -455,11 +455,7 @@ LS0tLS1CRUdJT.*
|
||||
|
||||
# uuid:
|
||||
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
|
||||
|
||||
# unicode escaped characters (4)
|
||||
\\u[0-9a-fA-F]{4}
|
||||
|
||||
# hex digits including css/html color classes
|
||||
# hex digits including css/html color classes:
|
||||
(?:[\\0][xX]|\\u\{?|[uU]\+|#x?|%23|&H)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|[iu]\d+)\b
|
||||
|
||||
# integrity
|
||||
@@ -482,7 +478,7 @@ Name\[[^\]]+\]=.*
|
||||
(?:(?:\b|_|(?<=[a-z]))I|(?:\b|_)(?:nsI|isA))(?=(?:[A-Z][a-z]{2,})+(?:[A-Z\d]|\b))
|
||||
|
||||
# python
|
||||
#\b(?i)py(?!gment|gmy|lon|ramid|ro|th)(?=[a-z]{2,})
|
||||
#\b(?i)py(?!gments|gmy|lon|ramid|ro|th)(?=[a-z]{2,})
|
||||
|
||||
# crypt
|
||||
(['"])\$2[ayb]\$.{56}\g{-1}
|
||||
@@ -502,21 +498,12 @@ Name\[[^\]]+\]=.*
|
||||
# go.sum
|
||||
\bh1:\S+
|
||||
|
||||
# golang print-f-style functions
|
||||
#(?i)(?<=append|comma|debug|equal|err|error|exit|fatal|format|info|log|name|panic|print|skip|scan|string|trace|true|warn|warning|wrap|write)(?:f|ln)(?:[ (]|$)
|
||||
|
||||
# golang regular expression
|
||||
(?<!")\br".+?"
|
||||
|
||||
# imports
|
||||
^import\s+(?:(?:static|type)\s+|)(?:[\w.]|\{\s*\w*?(?:,\s*(?:\w*|\*))+\s*\})+(?:\s+from (['"]).*?\g{-1}|)
|
||||
^import\s+(?:(?:static|type)\s+|)(?:[\w.]|\{\s*\w*?(?:,\s*(?:\w*|\*))+\s*\})+
|
||||
|
||||
# scala modules
|
||||
#("[^"]+"\s*%%?\s*){2,3}"[^"]+"
|
||||
|
||||
# Dataframes / NumPy
|
||||
#\b(?:df|np)\.\w{3,}
|
||||
|
||||
# container images
|
||||
image: [-\w./:@]+
|
||||
|
||||
@@ -546,18 +533,12 @@ content: (['"])[-a-zA-Z=;:/0-9+]*=\g{-1}
|
||||
# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings
|
||||
(?<!['"])\b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)['"](?=[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})
|
||||
|
||||
# Regular expression for word breaks
|
||||
#\\b(?=[a-z]{2})
|
||||
|
||||
# Regular expressions for (P|p)assword
|
||||
\([A-Z]\|[a-z]\)[a-z]+
|
||||
|
||||
# Java regular expressions
|
||||
Pattern\.(?:compile|matches)\(".*"
|
||||
|
||||
# JavaScript regular expressions
|
||||
# javascript exec/test regex
|
||||
/.{3,}?/[gim]*\.(?:exec|test)\(
|
||||
# javascript test regex
|
||||
/.{3,}/[gim]*\.test\(
|
||||
# javascript match regex
|
||||
\.match\(/[^/\s"]{3,}/[gim]*\s*
|
||||
# javascript match regex
|
||||
@@ -584,7 +565,7 @@ perl(?:\s+-[a-zA-Z]\w*)+
|
||||
regexp?\.MustCompile\((?:`[^`]*`|".*"|'.*')\)
|
||||
|
||||
# regex choice
|
||||
#\((?:\?:|)[^)|]+(?<! )\|(?!(?:jq|xargs)\b)[^)| ][^)]*\)
|
||||
\(\?:[^)]+\|[^)]+\)
|
||||
|
||||
# proto
|
||||
^\s*(\w+)\s\g{-1} =
|
||||
@@ -607,9 +588,6 @@ urn:shemas-jetbrains-com
|
||||
# Debian changelog severity
|
||||
[-\w]+ \(.*\) (?:\w+|baseline|unstable|experimental); urgency=(?:low|medium|high|emergency|critical)\b
|
||||
|
||||
# Red Hat Package management spec file dependencies
|
||||
^(?:Build|)Requires: [-.\w]+
|
||||
|
||||
# kubernetes pod status lists
|
||||
# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
|
||||
\w+(?:-\w+)+\s+\d+/\d+\s+(?:Running|Pending|Succeeded|Failed|Unknown)\s+
|
||||
@@ -664,8 +642,6 @@ PrependWithABINamepsace
|
||||
>[-a-zA-Z=;:/0-9+]{3,}=</
|
||||
# base64 encoded content, possibly wrapped in mime
|
||||
#(?:^|[\s=;:?])[-a-zA-Z=;:/0-9+]{50,}(?:[\s=;:?]|$)
|
||||
# jwt
|
||||
(?:\be[wy][-a-zA-Z=;:/0-9+]+\.){2}[-_\w]+
|
||||
# base64 encoded json
|
||||
\beyJ[-a-zA-Z=;:/0-9+]+
|
||||
# base64 encoded pkcs
|
||||
@@ -703,9 +679,9 @@ systemd.*?running in system mode \([-+].*\)$
|
||||
|
||||
# Non-English
|
||||
# Even repositories expecting pure English content can unintentionally have Non-English content... People will occasionally mistakenly enter [homoglyphs](https://en.wikipedia.org/wiki/Homoglyph) which are essentially typos, and using this pattern will mean check-spelling will not complain about them.
|
||||
# .
|
||||
#
|
||||
# If the content to be checked should be written in English and the only Non-English items will be people's names, then you can consider adding this.
|
||||
# .
|
||||
#
|
||||
# Alternatively, if you're using check-spelling v0.0.25+, and you would like to _check_ the Non-English content for spelling errors, you can. For information on how to do so, see:
|
||||
# https://docs.check-spelling.dev/Feature:-Configurable-word-characters.html#unicode
|
||||
[a-zA-Z]*[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*|[a-zA-Z]{3,}[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]|[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3,}
|
||||
@@ -717,7 +693,7 @@ systemd.*?running in system mode \([-+].*\)$
|
||||
# This corpus only had capital letters, but you probably want lowercase ones as well.
|
||||
\b[LN]'+[a-z]{2,}\b
|
||||
|
||||
# LaTeX
|
||||
# latex (check-spelling >= 0.0.22)
|
||||
\\\w{2,}\{
|
||||
|
||||
# American Mathematical Society (AMS) / Doxygen
|
||||
@@ -744,6 +720,7 @@ nolint:\s*[\w,]+
|
||||
# cygwin paths
|
||||
/cygdrive/[a-zA-Z]/(?:Program Files(?: \(.*?\)| ?)(?:/[-+.~\\/()\w ]+)*|[-+.~\\/()\w])+
|
||||
|
||||
# in check-spelling@v0.0.22+, printf markers aren't automatically consumed
|
||||
# printf markers
|
||||
#(?<!\\)\\[nrt](?=[a-z]{2,})
|
||||
# alternate printf markers if you run into latex and friends
|
||||
@@ -772,12 +749,12 @@ W/"[^"]+"
|
||||
|
||||
# Compiler flags (Unix, Java/Scala)
|
||||
# Use if you have things like `-Pdocker` and want to treat them as `docker`
|
||||
#(?:^|[\t ,>"'`=\[(#])-(?:(?:J-|)[DPWXY]|[Llf])(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
|
||||
#(?:^|[\t ,>"'`=(#])-(?:(?:J-|)[DPWXY]|[Llf])(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
|
||||
|
||||
# Compiler flags (Windows / PowerShell)
|
||||
# This is a subset of the more general compiler flags pattern.
|
||||
# It avoids matching `-Path` to prevent it from being treated as `ath`
|
||||
#(?:^|[\t ,"'`=\[(#])-(?:[DPL](?=[A-Z]{2,})|[WXYlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}))
|
||||
#(?:^|[\t ,"'`=(#])-(?:[DPL](?=[A-Z]{2,})|[WXYlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}))
|
||||
|
||||
# Compiler flags (linker)
|
||||
,-B
|
||||
@@ -785,7 +762,7 @@ W/"[^"]+"
|
||||
# Library prefix
|
||||
# e.g., `lib`+`archive`, `lib`+`raw`, `lib`+`unwind`
|
||||
# (ignores some words that happen to start with `lib`)
|
||||
(?:\b|_)[Ll]ib(?!era[lt])(?:re(?=office)|era|)(?!ero|erty|rar(?:i(?:an|es)|y))(?=[a-z])
|
||||
(?:\b|_)[Ll]ib(?:re(?=office)|)(?!era[lt]|ero|erty|rar(?:i(?:an|es)|y))(?=[a-z])
|
||||
|
||||
# iSCSI iqn (approximate regex)
|
||||
\biqn\.[0-9]{4}-[0-9]{2}(?:[\.-][a-z][a-z0-9]*)*\b
|
||||
@@ -796,9 +773,9 @@ W/"[^"]+"
|
||||
# curl arguments
|
||||
\b(?:\\n|)curl(?:\.exe|)(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)*
|
||||
# set arguments
|
||||
\b(?:bash|(?<!\.)sh|set)(?:\s+[-+][abefimouxE]{1,2})*\s+[-+][abefimouxE]{3,}(?:\s+[-+][abefimouxE]+)*
|
||||
\b(?:bash|sh|set)(?:\s+[-+][abefimouxE]{1,2})*\s+[-+][abefimouxE]{3,}(?:\s+[-+][abefimouxE]+)*
|
||||
# tar arguments
|
||||
\b(?:\\n|)g?tar(?:\.exe|)(?:\s-C \S+|(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
|
||||
\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
|
||||
# tput arguments -- https://man7.org/linux/man-pages/man5/terminfo.5.html -- technically they can be more than 5 chars long...
|
||||
\btput\s+(?:(?:-[SV]|-T\s*\w+)\s+)*\w{3,5}\b
|
||||
# macOS temp folders
|
||||
|
||||
14
.github/actions/spell-check/excludes.txt
vendored
14
.github/actions/spell-check/excludes.txt
vendored
@@ -93,6 +93,7 @@
|
||||
\.xz$
|
||||
\.zip$
|
||||
^\.github/actions/spell-check/
|
||||
^\.github/skills/
|
||||
^\.github/workflows/spelling\d*\.yml$
|
||||
^\.gitmodules$
|
||||
^\Q.pipelines/ESRPSigning_core.json\E$
|
||||
@@ -101,17 +102,11 @@
|
||||
^doc/devdocs/akaLinks\.md$
|
||||
^NOTICE\.md$
|
||||
^src/common/CalculatorEngineCommon/exprtk\.hpp$
|
||||
^src/common/UnitTests-CommonUtils/
|
||||
^src/common/ManagedCommon/ColorFormatHelper\.cs$
|
||||
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
|
||||
^src/common/sysinternals/Eula/
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
|
||||
^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$
|
||||
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
|
||||
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/.*\.TestData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/Text/.*\.cs$
|
||||
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
|
||||
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
||||
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
|
||||
@@ -135,14 +130,11 @@
|
||||
^src/modules/previewpane/SvgPreviewHandler/SvgHTMLPreviewGenerator\.cs$
|
||||
^src/modules/previewpane/UnitTests-MarkdownPreviewHandler/HelperFiles/MarkdownWithHTMLImageTag\.txt$
|
||||
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
|
||||
^src/modules/ZoomIt/ZoomIt/rnnoise/
|
||||
^src/modules/ZoomIt/ZoomIt/selfie_segmentation\.onnx$
|
||||
^src/modules/ZoomIt/ZoomIt/ZoomIt\.idc$
|
||||
^src/Monaco/
|
||||
^tools/project_template/ModuleTemplate/resource\.h$
|
||||
^tools/Verification scripts/Check preview handler registration\.ps1$
|
||||
ignore$
|
||||
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
|
||||
^src/common/CalculatorEngineCommon/exprtk\.hpp$
|
||||
src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage.cs
|
||||
^src/modules/powerrename/unittests/testdata/avif_test\.avif$
|
||||
^src/modules/powerrename/unittests/testdata/heif_test\.heic$
|
||||
^deps/spdlog-msvc-fix/
|
||||
|
||||
451
.github/actions/spell-check/expect.txt
vendored
451
.github/actions/spell-check/expect.txt
vendored
File diff suppressed because it is too large
Load Diff
495
.github/actions/spell-check/line_forbidden.patterns
vendored
495
.github/actions/spell-check/line_forbidden.patterns
vendored
File diff suppressed because one or more lines are too long
75
.github/actions/spell-check/patterns.txt
vendored
75
.github/actions/spell-check/patterns.txt
vendored
@@ -1,30 +1,10 @@
|
||||
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
|
||||
|
||||
Inno Setup
|
||||
|
||||
FFmpeg
|
||||
|
||||
# https://github.com/MicrosoftEdge/edge-launcher
|
||||
MIcrosoftEdgeLauncherCsharp
|
||||
|
||||
# x64
|
||||
(?:(?<=[a-df-z])x|(?<=[A-Z]X))64
|
||||
|
||||
# reversed irreversible binomials
|
||||
\b(?:mouse down and up|low and high)\b
|
||||
|
||||
# marker to ignore all code on line
|
||||
^.*/\* #no-spell-check-line \*/.*$
|
||||
# marker for ignoring a comment to the end of the line
|
||||
// #no-spell-check.*$
|
||||
|
||||
# JavaScript regex literals that start with \b can be reported as "b..." words.
|
||||
# Example: /\bclass\s+.../
|
||||
^\s*/\\[b].{3,}?/[gim]*\s*(?:\)(?:;|$)|,$)
|
||||
|
||||
# GitHub API header token used in code (not natural language).
|
||||
\bx-ratelimit-reset\b
|
||||
|
||||
# Gaelic
|
||||
Gàidhlig
|
||||
|
||||
@@ -91,14 +71,11 @@ StringComparer.OrdinalIgnoreCase\) \{.*\}
|
||||
# the last line of mimetype="application/x-microsoft.net.object.bytearray.base64" things in .resx files
|
||||
^\s*[-a-zA-Z=;:/0-9+]*[-a-zA-Z;:/0-9+][-a-zA-Z=;:/0-9+]*=$
|
||||
|
||||
# DateTime Formats
|
||||
Get-Date -Format \w+|DateTime\.Now(?::|\.ToString\(")\w+
|
||||
|
||||
# Automatically suggested patterns
|
||||
|
||||
# hit-count: 5402 file-count: 1339
|
||||
# IServiceProvider / isAThing
|
||||
(?:(?:\b|_|(?<=[a-z]))[A-Z]|(?:\b|_)(?:nsI|isA))(?=(?:[A-Z][a-z]{2,})+(?:[A-Z\d]|\b))
|
||||
(?:(?:\b|_|(?<=[a-z]))[IT]|(?:\b|_)(?:nsI|isA))(?=(?:[A-Z][a-z]{2,})+(?:[A-Z\d]|\b))
|
||||
|
||||
# hit-count: 2073 file-count: 842
|
||||
# #includes
|
||||
@@ -182,10 +159,6 @@ aka\.ms/[a-zA-Z0-9]+
|
||||
# kubernetes crd patterns
|
||||
^\s*pattern: .*$
|
||||
|
||||
# hit-count: 7 file-count: 3
|
||||
# unicode escaped characters (4)
|
||||
\\u[0-9a-fA-F]{4}
|
||||
|
||||
# hit-count: 5 file-count: 3
|
||||
# URL escaped characters
|
||||
%[0-9A-F][A-F](?=[A-Za-z])
|
||||
@@ -198,10 +171,6 @@ aka\.ms/[a-zA-Z0-9]+
|
||||
# medium
|
||||
\bmedium\.com/@?[^/\s"]+/[-\w:/*.]+
|
||||
|
||||
# hit-count: 2 file-count: 2
|
||||
# tar arguments
|
||||
\b(?:\\n|)g?tar(?:\.exe|)(?:\s-C \S+|(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
|
||||
|
||||
# hit-count: 2 file-count: 1
|
||||
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
|
||||
# YouTube url
|
||||
@@ -215,9 +184,22 @@ aka\.ms/[a-zA-Z0-9]+
|
||||
# curl arguments
|
||||
\b(?:\\n|)curl(?:\.exe|)(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)*
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# tar arguments
|
||||
\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
|
||||
|
||||
# #pragma lib
|
||||
^\s*#pragma comment\(lib, ".*?"\)
|
||||
|
||||
# UnitTests
|
||||
\[DataRow\(.*\)\]
|
||||
|
||||
# AdditionalDependencies
|
||||
<AdditionalDependencies>.*<
|
||||
|
||||
# the last line of mimetype="application/x-microsoft.net.object.bytearray.base64" things in .resx files
|
||||
^\s*[-a-zA-Z=;:/0-9+]*[-a-zA-Z;:/0-9+][-a-zA-Z=;:/0-9+]*=$
|
||||
|
||||
RegExp\(@?([`'"]).*?\g{-1}\)|(?:escapes|regEx):\s*(?:/.*/|([`'"]).*?\g{-1})|return/.*?/
|
||||
|
||||
# Questionably acceptable forms of `in to`
|
||||
@@ -237,15 +219,13 @@ RegExp\(@?([`'"]).*?\g{-1}\)|(?:escapes|regEx):\s*(?:/.*/|([`'"]).*?\g{-1})|retu
|
||||
# mount
|
||||
\bmount\s+-t\s+(\w+)\s+\g{-1}\b
|
||||
# C types and repeated CSS values
|
||||
\s(auto|await|buffalo|center|div|inherit|long|LONG|none|normal|solid|thin|transparent|very)(?:\s\g{-1})+\s
|
||||
\s(auto|buffalo|center|div|inherit|long|LONG|none|normal|solid|thin|transparent|very)(?:\s\g{-1})+\s
|
||||
# C enum and struct
|
||||
\b(?:enum|struct)\s+(\w+)\s+\g{-1}\b
|
||||
# go templates
|
||||
\s(\w+)\s+\g{-1}\s+\`(?:graphql|inject|json|yaml):
|
||||
# doxygen / javadoc / .net
|
||||
(?:[\\@](?:brief|defgroup|groupname|link|t?param|return|retval)|(?:public|private|\[Parameter(?:\(.+\)|)\])(?:\s+(?:static|override|readonly|required|virtual))*)(?:\s+\{\w+\}|)\s+(\w+)\s+\g{-1}\s
|
||||
# C# getter/setter
|
||||
\s(\w+)\s+\g{-1}\s*\{\s*[gs]et;
|
||||
|
||||
# macOS file path
|
||||
(?:Contents\W+|(?!iOS)/)MacOS\b
|
||||
@@ -293,28 +273,3 @@ St&yle
|
||||
# Usernames with numbers
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
\bx6f677548\b
|
||||
|
||||
# Windows API constants and hardware interface terms
|
||||
\bCOINIT[_A-Z]*\b
|
||||
\bEOAC[_A-Z]*\b
|
||||
\b(?:RPC_C_AUTHN_)?WINNT\b
|
||||
\bUPDATEREGISTRY\b
|
||||
\b(?:CDS_)?UPDATEREGISTRY\b
|
||||
|
||||
# Display interface terms (HDMI, DVI, DisplayPort)
|
||||
\b(?:HDMI|DVI|DisplayPort)(?:-\d+)?\b
|
||||
|
||||
# 2D Region struct names
|
||||
\bDisplayConfig2?D?Region\b
|
||||
|
||||
# Microsoft Store URLs and product IDs
|
||||
ms-windows-store://\S+
|
||||
|
||||
# ANSI color codes
|
||||
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+)*m
|
||||
|
||||
# Special licenses text from RNNoise (BSD-style disclaimer: ``AS IS'')
|
||||
``AS IS''
|
||||
|
||||
# Old school moniker for macOS from RNNoise
|
||||
MacOS
|
||||
|
||||
47
.github/actions/spell-check/reject.txt
vendored
47
.github/actions/spell-check/reject.txt
vendored
@@ -1,30 +1,23 @@
|
||||
attache
|
||||
aroynt.*
|
||||
bellows?
|
||||
^attache$
|
||||
^bellows?$
|
||||
benefitting
|
||||
occurences?
|
||||
.*dnt
|
||||
dependan.*
|
||||
developement
|
||||
developp?e
|
||||
Devers?
|
||||
devex.*
|
||||
devide
|
||||
Devinn?[ae]
|
||||
devisals?
|
||||
devisors?
|
||||
diables?
|
||||
hasta?
|
||||
hastat.*
|
||||
immediatly
|
||||
inisle
|
||||
inital
|
||||
linge
|
||||
oer
|
||||
^dependan.*
|
||||
^develope$
|
||||
^developement$
|
||||
^developpe
|
||||
^Devers?$
|
||||
^devex
|
||||
^devide
|
||||
^Devinn?[ae]
|
||||
^devisal
|
||||
^devisor
|
||||
^diables?$
|
||||
^oer$
|
||||
Sorce
|
||||
[Ss]pae.*
|
||||
Teh
|
||||
untill
|
||||
untilling
|
||||
venders?
|
||||
wether.*
|
||||
^[Ss]pae.*
|
||||
^Teh$
|
||||
^untill$
|
||||
^untilling$
|
||||
^venders?$
|
||||
^wether.*
|
||||
|
||||
92
.github/agents/FixIssue.agent.md
vendored
92
.github/agents/FixIssue.agent.md
vendored
@@ -1,92 +0,0 @@
|
||||
---
|
||||
description: 'Implements fixes for GitHub issues based on implementation plans'
|
||||
name: 'FixIssue'
|
||||
tools: ['read', 'edit', 'search', 'execute', 'agent', 'usages', 'problems', 'changes', 'testFailure', 'github/*', 'github.vscode-pull-request-github/*']
|
||||
argument-hint: 'GitHub issue number (e.g., #12345)'
|
||||
infer: true
|
||||
---
|
||||
|
||||
# FixIssue Agent
|
||||
|
||||
You are an **IMPLEMENTATION AGENT** specialized in executing implementation plans to fix GitHub issues.
|
||||
|
||||
## Identity & Expertise
|
||||
|
||||
- Expert at translating plans into working code
|
||||
- Deep knowledge of PowerToys codebase patterns and conventions
|
||||
- Skilled at writing tests, handling edge cases, and validating builds
|
||||
- You follow plans precisely while handling ambiguity gracefully
|
||||
|
||||
## Goal
|
||||
|
||||
For the given **issue_number**, execute the implementation plan and produce:
|
||||
1. Working code changes applied directly to the repository
|
||||
2. `Generated Files/issueFix/{{issue_number}}/pr-description.md` — PR-ready description
|
||||
3. `Generated Files/issueFix/{{issue_number}}/manual-steps.md` — Only if human action needed
|
||||
|
||||
## Core Directive
|
||||
|
||||
**Follow the implementation plan in `Generated Files/issueReview/{{issue_number}}/implementation-plan.md` as the single source of truth.**
|
||||
|
||||
If the plan doesn't exist, invoke PlanIssue agent first via `runSubagent`.
|
||||
|
||||
## Working Principles
|
||||
|
||||
- **Plan First**: Read and understand the entire implementation plan before coding
|
||||
- **Validate Always**: For each change: Edit → Build → Verify → Commit. Never proceed if build fails.
|
||||
- **Atomic Commits**: Each commit must be self-contained, buildable, and meaningful
|
||||
- **Ask, Don't Guess**: When uncertain, insert `// TODO(Human input needed): <question>` and document in manual-steps.md
|
||||
|
||||
## Strategy
|
||||
|
||||
**Core Loop** — For every unit of work:
|
||||
1. **Edit**: Make focused changes to implement one logical piece
|
||||
2. **Build**: Run `tools\build\build.cmd` and check for exit code 0
|
||||
3. **Verify**: Use `problems` tool for lint/compile errors; run relevant tests
|
||||
4. **Commit**: Only after build passes — use `.github/prompts/create-commit-title.prompt.md`
|
||||
|
||||
Never skip steps. Never commit broken code. Never proceed if build fails.
|
||||
|
||||
**Feature-by-Feature E2E**: For big scenarios with multiple features, complete each feature end-to-end before moving to the next:
|
||||
- Settings UI → Functionality → Logging → Tests (for Feature 1)
|
||||
- Then repeat for Feature 2
|
||||
- Benefits: Each feature is self-contained, testable, easier to review, can ship incrementally
|
||||
|
||||
**Large Changes** (3+ files or cross-module):
|
||||
- Use `tools\build\New-WorktreeFromBranch.ps1` for isolated worktrees
|
||||
- Create separate branches per feature (e.g., `issue/{{issue_number}}-export`, `issue/{{issue_number}}-import`)
|
||||
- Merge feature branches back after each is validated
|
||||
|
||||
**Recovery**: If implementation goes wrong:
|
||||
- Create a checkpoint branch before risky changes
|
||||
- On failure: branch from last known-good state, cherry-pick working changes, abandon broken branch
|
||||
- For complex changes, consider multiple smaller PRs
|
||||
|
||||
## Guidelines
|
||||
|
||||
**DO**:
|
||||
- Follow the plan exactly
|
||||
- Validate build before every commit — **NEVER commit broken code**
|
||||
- Use `.github/prompts/create-commit-title.prompt.md` for commit messages
|
||||
- Add comprehensive tests for changed behavior
|
||||
- Use worktrees for large changes (3+ files or cross-module)
|
||||
- Document deviations from plan
|
||||
|
||||
**DON'T**:
|
||||
- Implement everything in a single massive commit
|
||||
- Continue after a failed build without fixing
|
||||
- Make drive-by refactors outside issue scope
|
||||
- Skip tests for behavioral changes
|
||||
- Add noisy logs in hot paths
|
||||
- Break IPC/JSON contracts without updating both sides
|
||||
- Introduce dependencies without documenting in NOTICE.md
|
||||
|
||||
## References
|
||||
|
||||
- [Build Guidelines](../../tools/build/BUILD-GUIDELINES.md) — Build commands and validation
|
||||
- [Coding Style](../../doc/devdocs/development/style.md) — Formatting and conventions
|
||||
- [AGENTS.md](../../AGENTS.md) — Full contributor guide
|
||||
|
||||
## Parameter
|
||||
|
||||
- **issue_number**: Extract from `#123`, `issue 123`, or plain number. If missing, ask user.
|
||||
65
.github/agents/PlanIssue.agent.md
vendored
65
.github/agents/PlanIssue.agent.md
vendored
@@ -1,65 +0,0 @@
|
||||
---
|
||||
description: 'Analyzes GitHub issues to produce overview and implementation plans'
|
||||
name: 'PlanIssue'
|
||||
tools: ['execute', 'read', 'edit', 'search', 'web', 'github/*', 'agent', 'github-artifacts/*', 'todo']
|
||||
argument-hint: 'GitHub issue number (e.g., #12345)'
|
||||
handoffs:
|
||||
- label: Start Implementation
|
||||
agent: FixIssue
|
||||
prompt: 'Fix issue #{{issue_number}} using the implementation plan'
|
||||
- label: Open Plan in Editor
|
||||
agent: agent
|
||||
prompt: 'Open Generated Files/issueReview/{{issue_number}}/overview.md and implementation-plan.md'
|
||||
showContinueOn: false
|
||||
send: true
|
||||
infer: true
|
||||
---
|
||||
|
||||
# PlanIssue Agent
|
||||
|
||||
You are a **PLANNING AGENT** specialized in analyzing GitHub issues and producing comprehensive planning documentation.
|
||||
|
||||
## Identity & Expertise
|
||||
|
||||
- Expert at issue triage, priority scoring, and technical analysis
|
||||
- Deep knowledge of PowerToys architecture and codebase patterns
|
||||
- Skilled at breaking down problems into actionable implementation steps
|
||||
- You research thoroughly before planning, gathering 80% confidence before drafting
|
||||
|
||||
## Goal
|
||||
|
||||
For the given **issue_number**, produce two deliverables:
|
||||
1. `Generated Files/issueReview/{{issue_number}}/overview.md` — Issue analysis with scoring
|
||||
2. `Generated Files/issueReview/{{issue_number}}/implementation-plan.md` — Technical implementation plan
|
||||
Above is the core interaction with the end user. If you cannot produce the files above, you fail the task. Each time, you must check whether the files exist or have been modified by the end user, without assuming you know their contents.
|
||||
3. `Generated Files/issueReview/{{issue_number}}/logs/**` — logs for your diagnostic of root cause, research steps, and reasoning
|
||||
|
||||
## Core Directive
|
||||
|
||||
**Follow the template in `.github/prompts/review-issue.prompt.md` exactly.** Read it first, then apply every section as specified.
|
||||
|
||||
- Fetch issue details: reactions, comments, linked PRs, images, logs
|
||||
- Search related code and similar past fixes
|
||||
- Ask clarifying questions when ambiguous
|
||||
- Identify subject matter experts via git history
|
||||
|
||||
<stopping_rules>
|
||||
You are a PLANNING agent, NOT an implementation agent.
|
||||
|
||||
STOP if you catch yourself:
|
||||
- Writing code or editing source files outside `Generated Files/issueReview/`
|
||||
- Making assumptions without researching
|
||||
- Skipping the scoring/assessment phase
|
||||
|
||||
Plans describe what the USER or FixIssue agent will execute later.
|
||||
</stopping_rules>
|
||||
|
||||
## References
|
||||
|
||||
- [Review Issue Prompt](../.github/prompts/review-issue.prompt.md) — Template for plan structure
|
||||
- [Architecture Overview](../../doc/devdocs/core/architecture.md) — System design context
|
||||
- [AGENTS.md](../../AGENTS.md) — Full contributor guide
|
||||
|
||||
## Parameter
|
||||
|
||||
- **issue_number**: Extract from `#123`, `issue 123`, or plain number. If missing, ask user.
|
||||
77
.github/copilot-instructions.md
vendored
77
.github/copilot-instructions.md
vendored
@@ -1,42 +1,59 @@
|
||||
---
|
||||
description: 'PowerToys AI contributor guidance'
|
||||
description: PowerToys AI contributor guidance.
|
||||
applyTo: pullRequests
|
||||
---
|
||||
|
||||
# PowerToys – Copilot Instructions
|
||||
# PowerToys - Copilot guide (concise)
|
||||
|
||||
Concise guidance for AI contributions. For complete details, see [AGENTS.md](../AGENTS.md).
|
||||
This is the top-level guide for AI changes. Keep edits small, follow existing patterns, and cite exact paths in PRs.
|
||||
|
||||
## Key Rules
|
||||
# Repo map (1-line per area)
|
||||
- Core apps: `src/runner/**` (tray/loader), `src/settings-ui/**` (Settings app)
|
||||
- Shared libs: `src/common/**`
|
||||
- Modules: `src/modules/*` (one per utility; Command Palette in `src/modules/cmdpal/**`)
|
||||
- Build tools/docs: `tools/**`, `doc/devdocs/**`
|
||||
|
||||
- Atomic PRs: one logical change, no drive-by refactors
|
||||
- Add tests when changing behavior
|
||||
- Keep hot paths quiet (no logging in hooks/tight loops)
|
||||
# Build and test (defaults)
|
||||
- Prerequisites: Visual Studio 2022 17.4+, minimal Windows 10 1803+.
|
||||
- Build discipline:
|
||||
- One terminal per operation (build -> test). Do not switch or open new ones mid-flow.
|
||||
- After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`).
|
||||
- Use scripts to build, synchronously block and wait in foreground for completion: `tools/build/build.ps1|.cmd` (current folder), `build-essentials.*` (once per brand new build for missing nuget packages).
|
||||
- Treat build exit code 0 as success; any non-zero exit code is a failure. Read the errors log in the build folder (such as `build.*.*.errors.log`) and surface problems.
|
||||
- Do not start tests or launch Runner until the previous step succeeded.
|
||||
- Tests (fast and targeted):
|
||||
- Find the test project by product code prefix (for example FancyZones, AdvancedPaste). Look for a sibling folder or one to two levels up named like `<Product>*UnitTests` or `<Product>*UITests`.
|
||||
- Build the test project, wait for exit, then run only those tests via VS Test Explorer or `vstest.console.exe` with filters. Avoid `dotnet test` in this repo.
|
||||
- Add or adjust tests when changing behavior; if skipped, state why (for example comment-only or string rename).
|
||||
|
||||
## Style Enforcement
|
||||
# Pull requests (expectations)
|
||||
- Atomic: one logical change; no drive-by refactors.
|
||||
- Describe: problem, approach, risk, test evidence.
|
||||
- List: touched paths if not obvious.
|
||||
|
||||
- C#: `src/.editorconfig`, StyleCop.Analyzers
|
||||
- C++: `src/.clang-format`
|
||||
- XAML: XamlStyler
|
||||
# When to ask for clarification
|
||||
- Ambiguous spec after scanning relevant docs (see below).
|
||||
- Cross-module impact (shared enum or struct) not clear.
|
||||
- Security, elevation, or installer changes.
|
||||
|
||||
## When to Ask for Clarification
|
||||
# Logging (use existing stacks)
|
||||
- C++ logging lives in `src/common/logger/**` (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`). Keep hot paths quiet (hooks, tight loops).
|
||||
- C# logging goes through `ManagedCommon.Logger` (`LogInfo`, `LogWarning`, `LogError`, `LogDebug`, `LogTrace`). Some UIs use injected `ILogger` via `LoggerInstance.Logger`.
|
||||
|
||||
- Ambiguous spec after scanning docs
|
||||
- Cross-module impact unclear
|
||||
- Security, elevation, or installer changes
|
||||
# Docs to consult
|
||||
- `tools/build/BUILD-GUIDELINES.md`
|
||||
- `doc/devdocs/core/architecture.md`
|
||||
- `doc/devdocs/core/runner.md`
|
||||
- `doc/devdocs/core/settings/readme.md`
|
||||
- `doc/devdocs/modules/readme.md`
|
||||
|
||||
## Component-Specific Instructions
|
||||
# Language style rules
|
||||
- Always enforce repo analyzers: root `.editorconfig` plus any `stylecop.json`.
|
||||
- C# code follows StyleCop.Analyzers and Microsoft.CodeAnalysis.NetAnalyzers.
|
||||
- C++ code honors `.clang-format` plus `.clang-tidy` (modernize/cppcoreguidelines/readability).
|
||||
- Markdown files wrap at 80 characters and use ATX headers with fenced code blocks that include language tags.
|
||||
- YAML files indent two spaces and add comments for complex settings while keeping keys clear.
|
||||
- PowerShell scripts use Verb-Noun names and prefer single-quoted literals while documenting parameters and satisfying PSScriptAnalyzer.
|
||||
|
||||
These are auto-applied based on file location:
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
|
||||
## Shortcut Guide V2 Manifests
|
||||
|
||||
When creating or editing Shortcut Guide keyboard shortcut manifest files, follow the schema and naming conventions in the spec:
|
||||
|
||||
- [WinGet Manifest Keyboard Shortcuts schema](<../doc/specs/WinGet Manifest Keyboard Shortcuts schema.md>) – manifest file format, field definitions, file naming, and the `+` prefix convention for apps without a WinGet package
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
- [Architecture](../doc/devdocs/core/architecture.md)
|
||||
- [Coding Style](../doc/devdocs/development/style.md)
|
||||
# Done checklist (self review before finishing)
|
||||
- Build clean? Tests updated or passed? No unintended formatting? Any new dependency? Documented skips?
|
||||
|
||||
261
.github/instructions/agent-skills.instructions.md
vendored
261
.github/instructions/agent-skills.instructions.md
vendored
@@ -1,261 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for creating high-quality Agent Skills for GitHub Copilot'
|
||||
applyTo: '**/.github/skills/**/SKILL.md, **/.claude/skills/**/SKILL.md'
|
||||
---
|
||||
|
||||
# Agent Skills File Guidelines
|
||||
|
||||
Instructions for creating effective and portable Agent Skills that enhance GitHub Copilot with specialized capabilities, workflows, and bundled resources.
|
||||
|
||||
## What Are Agent Skills?
|
||||
|
||||
Agent Skills are self-contained folders with instructions and bundled resources that teach AI agents specialized capabilities. Unlike custom instructions (which define coding standards), skills enable task-specific workflows that can include scripts, examples, templates, and reference data.
|
||||
|
||||
Key characteristics:
|
||||
- **Portable**: Works across VS Code, Copilot CLI, and Copilot coding agent
|
||||
- **Progressive loading**: Only loaded when relevant to the user's request
|
||||
- **Resource-bundled**: Can include scripts, templates, examples alongside instructions
|
||||
- **On-demand**: Activated automatically based on prompt relevance
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Skills are stored in specific locations:
|
||||
|
||||
| Location | Scope | Recommendation |
|
||||
|----------|-------|----------------|
|
||||
| `.github/skills/<skill-name>/` | Project/repository | Recommended for project skills |
|
||||
| `.claude/skills/<skill-name>/` | Project/repository | Legacy, for backward compatibility |
|
||||
| `~/.github/skills/<skill-name>/` | Personal (user-wide) | Recommended for personal skills |
|
||||
| `~/.claude/skills/<skill-name>/` | Personal (user-wide) | Legacy, for backward compatibility |
|
||||
|
||||
Each skill **must** have its own subdirectory containing at minimum a `SKILL.md` file.
|
||||
|
||||
## Required SKILL.md Format
|
||||
|
||||
### Frontmatter (Required)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: webapp-testing
|
||||
description: Toolkit for testing local web applications using Playwright. Use when asked to verify frontend functionality, debug UI behavior, capture browser screenshots, check for visual regressions, or view browser console logs. Supports Chrome, Firefox, and WebKit browsers.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
```
|
||||
|
||||
| Field | Required | Constraints |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | Lowercase, hyphens for spaces, max 64 characters (e.g., `webapp-testing`) |
|
||||
| `description` | Yes | Clear description of capabilities AND use cases, max 1024 characters |
|
||||
| `license` | No | Reference to LICENSE.txt (e.g., `Complete terms in LICENSE.txt`) or SPDX identifier |
|
||||
|
||||
### Description Best Practices
|
||||
|
||||
**CRITICAL**: The `description` field is the PRIMARY mechanism for automatic skill discovery. Copilot reads ONLY the `name` and `description` to decide whether to load a skill. If your description is vague, the skill will never be activated.
|
||||
|
||||
**What to include in description:**
|
||||
1. **WHAT** the skill does (capabilities)
|
||||
2. **WHEN** to use it (specific triggers, scenarios, file types, or user requests)
|
||||
3. **Keywords** that users might mention in their prompts
|
||||
|
||||
**Good description:**
|
||||
```yaml
|
||||
description: Toolkit for testing local web applications using Playwright. Use when asked to verify frontend functionality, debug UI behavior, capture browser screenshots, check for visual regressions, or view browser console logs. Supports Chrome, Firefox, and WebKit browsers.
|
||||
```
|
||||
|
||||
**Poor description:**
|
||||
```yaml
|
||||
description: Web testing helpers
|
||||
```
|
||||
|
||||
The poor description fails because:
|
||||
- No specific triggers (when should Copilot load this?)
|
||||
- No keywords (what user prompts would match?)
|
||||
- No capabilities (what can it actually do?)
|
||||
|
||||
### Body Content
|
||||
|
||||
The body contains detailed instructions that Copilot loads AFTER the skill is activated. Recommended sections:
|
||||
|
||||
| Section | Purpose |
|
||||
|---------|---------|
|
||||
| `# Title` | Brief overview of what this skill enables |
|
||||
| `## When to Use This Skill` | List of scenarios (reinforces description triggers) |
|
||||
| `## Prerequisites` | Required tools, dependencies, environment setup |
|
||||
| `## Step-by-Step Workflows` | Numbered steps for common tasks |
|
||||
| `## Troubleshooting` | Common issues and solutions table |
|
||||
| `## References` | Links to bundled docs or external resources |
|
||||
|
||||
## Bundling Resources
|
||||
|
||||
Skills can include additional files that Copilot accesses on-demand:
|
||||
|
||||
### Supported Resource Types
|
||||
|
||||
| Folder | Purpose | Loaded into Context? | Example Files |
|
||||
|--------|---------|---------------------|---------------|
|
||||
| `scripts/` | Executable automation that performs specific operations | When executed | `helper.py`, `validate.sh`, `build.ts` |
|
||||
| `references/` | Documentation the AI agent reads to inform decisions | Yes, when referenced | `api_reference.md`, `schema.md`, `workflow_guide.md` |
|
||||
| `assets/` | **Static files used AS-IS** in output (not modified by the AI agent) | No | `logo.png`, `brand-template.pptx`, `custom-font.ttf` |
|
||||
| `templates/` | **Starter code/scaffolds that the AI agent MODIFIES** and builds upon | Yes, when referenced | `viewer.html` (insert algorithm), `hello-world/` (extend) |
|
||||
|
||||
### Directory Structure Example
|
||||
|
||||
```
|
||||
.github/skills/my-skill/
|
||||
├── SKILL.md # Required: Main instructions
|
||||
├── LICENSE.txt # Recommended: License terms (Apache 2.0 typical)
|
||||
├── scripts/ # Optional: Executable automation
|
||||
│ ├── helper.py # Python script
|
||||
│ └── helper.ps1 # PowerShell script
|
||||
├── references/ # Optional: Documentation loaded into context
|
||||
│ ├── api_reference.md
|
||||
│ ├── step1-setup.md # Detailed workflow (>3 steps)
|
||||
│ └── step2-deployment.md
|
||||
├── assets/ # Optional: Static files used AS-IS in output
|
||||
│ ├── baseline.png # Reference image for comparison
|
||||
│ └── report-template.html
|
||||
└── templates/ # Optional: Starter code the AI agent modifies
|
||||
├── scaffold.py # Code scaffold the AI agent customizes
|
||||
└── config.template # Config template the AI agent fills in
|
||||
```
|
||||
|
||||
> **LICENSE.txt**: When creating a skill, download the Apache 2.0 license text from https://www.apache.org/licenses/LICENSE-2.0.txt and save as `LICENSE.txt`. Update the copyright year and owner in the appendix section.
|
||||
|
||||
### Assets vs Templates: Key Distinction
|
||||
|
||||
**Assets** are static resources **consumed unchanged** in the output:
|
||||
- A `logo.png` that gets embedded into a generated document
|
||||
- A `report-template.html` copied as output format
|
||||
- A `custom-font.ttf` applied to text rendering
|
||||
|
||||
**Templates** are starter code/scaffolds that **the AI agent actively modifies**:
|
||||
- A `scaffold.py` where the AI agent inserts logic
|
||||
- A `config.template` where the AI agent fills in values based on user requirements
|
||||
- A `hello-world/` project directory that the AI agent extends with new features
|
||||
|
||||
**Rule of thumb**: If the AI agent reads and builds upon the file content → `templates/`. If the file is used as-is in output → `assets/`.
|
||||
|
||||
### Referencing Resources in SKILL.md
|
||||
|
||||
Use relative paths to reference files within the skill directory:
|
||||
|
||||
```markdown
|
||||
## Available Scripts
|
||||
|
||||
Run the [helper script](./scripts/helper.py) to automate common tasks.
|
||||
|
||||
See [API reference](./references/api_reference.md) for detailed documentation.
|
||||
|
||||
Use the [scaffold](./templates/scaffold.py) as a starting point.
|
||||
```
|
||||
|
||||
## Progressive Loading Architecture
|
||||
|
||||
Skills use three-level loading for efficiency:
|
||||
|
||||
| Level | What Loads | When |
|
||||
|-------|------------|------|
|
||||
| 1. Discovery | `name` and `description` only | Always (lightweight metadata) |
|
||||
| 2. Instructions | Full `SKILL.md` body | When request matches description |
|
||||
| 3. Resources | Scripts, examples, docs | Only when Copilot references them |
|
||||
|
||||
This means:
|
||||
- Install many skills without consuming context
|
||||
- Only relevant content loads per task
|
||||
- Resources don't load until explicitly needed
|
||||
|
||||
## Content Guidelines
|
||||
|
||||
### Writing Style
|
||||
|
||||
- Use imperative mood: "Run", "Create", "Configure" (not "You should run")
|
||||
- Be specific and actionable
|
||||
- Include exact commands with parameters
|
||||
- Show expected outputs where helpful
|
||||
- Keep sections focused and scannable
|
||||
|
||||
### Script Requirements
|
||||
|
||||
When including scripts, prefer cross-platform languages:
|
||||
|
||||
| Language | Use Case |
|
||||
|----------|----------|
|
||||
| Python | Complex automation, data processing |
|
||||
| pwsh | PowerShell Core scripting |
|
||||
| Node.js | JavaScript-based tooling |
|
||||
| Bash/Shell | Simple automation tasks |
|
||||
|
||||
Best practices:
|
||||
- Include help/usage documentation (`--help` flag)
|
||||
- Handle errors gracefully with clear messages
|
||||
- Avoid storing credentials or secrets
|
||||
- Use relative paths where possible
|
||||
|
||||
### When to Bundle Scripts
|
||||
|
||||
Include scripts in your skill when:
|
||||
- The same code would be rewritten repeatedly by the agent
|
||||
- Deterministic reliability is critical (e.g., file manipulation, API calls)
|
||||
- Complex logic benefits from being pre-tested rather than generated each time
|
||||
- The operation has a self-contained purpose that can evolve independently
|
||||
- Testability matters — scripts can be unit tested and validated
|
||||
- Predictable behavior is preferred over dynamic generation
|
||||
|
||||
Scripts enable evolution: even simple operations benefit from being implemented as scripts when they may grow in complexity, need consistent behavior across invocations, or require future extensibility.
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Scripts rely on existing credential helpers (no credential storage)
|
||||
- Include `--force` flags only for destructive operations
|
||||
- Warn users before irreversible actions
|
||||
- Document any network operations or external calls
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Parameter Table Pattern
|
||||
|
||||
Document parameters clearly:
|
||||
|
||||
```markdown
|
||||
| Parameter | Required | Default | Description |
|
||||
|-----------|----------|---------|-------------|
|
||||
| `--input` | Yes | - | Input file or URL to process |
|
||||
| `--action` | Yes | - | Action to perform |
|
||||
| `--verbose` | No | `false` | Enable verbose output |
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before publishing a skill:
|
||||
|
||||
- [ ] `SKILL.md` has valid frontmatter with `name` and `description`
|
||||
- [ ] `name` is lowercase with hyphens, ≤64 characters
|
||||
- [ ] `description` clearly states **WHAT** it does, **WHEN** to use it, and relevant **KEYWORDS**
|
||||
- [ ] Body includes when to use, prerequisites, and step-by-step workflows
|
||||
- [ ] SKILL.md body kept under 500 lines (split large content into `references/` folder)
|
||||
- [ ] Large workflows (>5 steps) split into `references/` folder with clear links from SKILL.md
|
||||
- [ ] Scripts include help documentation and error handling
|
||||
- [ ] Relative paths used for all resource references
|
||||
- [ ] No hardcoded credentials or secrets
|
||||
|
||||
## Workflow Execution Pattern
|
||||
|
||||
When executing multi-step workflows, create a TODO list where each step references the relevant documentation:
|
||||
|
||||
```markdown
|
||||
## TODO
|
||||
- [ ] Step 1: Configure environment - see [workflow-setup.md](./references/workflow-setup.md#environment)
|
||||
- [ ] Step 2: Build project - see [workflow-setup.md](./references/workflow-setup.md#build)
|
||||
- [ ] Step 3: Deploy to staging - see [workflow-deployment.md](./references/workflow-deployment.md#staging)
|
||||
- [ ] Step 4: Run validation - see [workflow-deployment.md](./references/workflow-deployment.md#validation)
|
||||
- [ ] Step 5: Deploy to production - see [workflow-deployment.md](./references/workflow-deployment.md#production)
|
||||
```
|
||||
|
||||
This ensures traceability and allows resuming workflows if interrupted.
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Agent Skills Specification](https://agentskills.io/)
|
||||
- [VS Code Agent Skills Documentation](https://code.visualstudio.com/docs/copilot/customization/agent-skills)
|
||||
- [Reference Skills Repository](https://github.com/anthropics/skills)
|
||||
- [Awesome Copilot Skills](https://github.com/github/awesome-copilot/blob/main/docs/README.skills.md)
|
||||
791
.github/instructions/agents.instructions.md
vendored
791
.github/instructions/agents.instructions.md
vendored
@@ -1,791 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for creating custom agent files for GitHub Copilot'
|
||||
applyTo: '**/*.agent.md'
|
||||
---
|
||||
|
||||
# Custom Agent File Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable custom agent files that provide specialized expertise for specific development tasks in GitHub Copilot.
|
||||
|
||||
## Project Context
|
||||
|
||||
- Target audience: Developers creating custom agents for GitHub Copilot
|
||||
- File format: Markdown with YAML frontmatter
|
||||
- File naming convention: lowercase with hyphens (e.g., `test-specialist.agent.md`)
|
||||
- Location: `.github/agents/` directory (repository-level) or `agents/` directory (organization/enterprise-level)
|
||||
- Purpose: Define specialized agents with tailored expertise, tools, and instructions for specific tasks
|
||||
- Official documentation: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents
|
||||
|
||||
## Required Frontmatter
|
||||
|
||||
Every agent file must include YAML frontmatter with the following fields:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: 'Brief description of the agent purpose and capabilities'
|
||||
name: 'Agent Display Name'
|
||||
tools: ['read', 'edit', 'search']
|
||||
model: 'Claude Sonnet 4.5'
|
||||
target: 'vscode'
|
||||
infer: true
|
||||
---
|
||||
```
|
||||
|
||||
### Core Frontmatter Properties
|
||||
|
||||
#### **description** (REQUIRED)
|
||||
- Single-quoted string, clearly stating the agent's purpose and domain expertise
|
||||
- Should be concise (50-150 characters) and actionable
|
||||
- Example: `'Focuses on test coverage, quality, and testing best practices'`
|
||||
|
||||
#### **name** (OPTIONAL)
|
||||
- Display name for the agent in the UI
|
||||
- If omitted, defaults to filename (without `.md` or `.agent.md`)
|
||||
- Use title case and be descriptive
|
||||
- Example: `'Testing Specialist'`
|
||||
|
||||
#### **tools** (OPTIONAL)
|
||||
- List of tool names or aliases the agent can use
|
||||
- Supports comma-separated string or YAML array format
|
||||
- If omitted, agent has access to all available tools
|
||||
- See "Tool Configuration" section below for details
|
||||
|
||||
#### **model** (STRONGLY RECOMMENDED)
|
||||
- Specifies which AI model the agent should use
|
||||
- Supported in VS Code, JetBrains IDEs, Eclipse, and Xcode
|
||||
- Example: `'Claude Sonnet 4.5'`, `'gpt-4'`, `'gpt-4o'`
|
||||
- Choose based on agent complexity and required capabilities
|
||||
|
||||
#### **target** (OPTIONAL)
|
||||
- Specifies target environment: `'vscode'` or `'github-copilot'`
|
||||
- If omitted, agent is available in both environments
|
||||
- Use when agent has environment-specific features
|
||||
|
||||
#### **infer** (OPTIONAL)
|
||||
- Boolean controlling whether Copilot can automatically use this agent based on context
|
||||
- Default: `true` if omitted
|
||||
- Set to `false` to require manual agent selection
|
||||
|
||||
#### **metadata** (OPTIONAL, GitHub.com only)
|
||||
- Object with name-value pairs for agent annotation
|
||||
- Example: `metadata: { category: 'testing', version: '1.0' }`
|
||||
- Not supported in VS Code
|
||||
|
||||
#### **mcp-servers** (OPTIONAL, Organization/Enterprise only)
|
||||
- Configure MCP servers available only to this agent
|
||||
- Only supported for organization/enterprise level agents
|
||||
- See "MCP Server Configuration" section below
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
### Tool Specification Strategies
|
||||
|
||||
**Enable all tools** (default):
|
||||
```yaml
|
||||
# Omit tools property entirely, or use:
|
||||
tools: ['*']
|
||||
```
|
||||
|
||||
**Enable specific tools**:
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'search', 'execute']
|
||||
```
|
||||
|
||||
**Enable MCP server tools**:
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'github/*', 'playwright/navigate']
|
||||
```
|
||||
|
||||
**Disable all tools**:
|
||||
```yaml
|
||||
tools: []
|
||||
```
|
||||
|
||||
### Standard Tool Aliases
|
||||
|
||||
All aliases are case-insensitive:
|
||||
|
||||
| Alias | Alternative Names | Category | Description |
|
||||
|-------|------------------|----------|-------------|
|
||||
| `execute` | shell, Bash, powershell | Shell execution | Execute commands in appropriate shell |
|
||||
| `read` | Read, NotebookRead, view | File reading | Read file contents |
|
||||
| `edit` | Edit, MultiEdit, Write, NotebookEdit | File editing | Edit and modify files |
|
||||
| `search` | Grep, Glob, search | Code search | Search for files or text in files |
|
||||
| `agent` | custom-agent, Task | Agent invocation | Invoke other custom agents |
|
||||
| `web` | WebSearch, WebFetch | Web access | Fetch web content and search |
|
||||
| `todo` | TodoWrite | Task management | Create and manage task lists (VS Code only) |
|
||||
|
||||
### Built-in MCP Server Tools
|
||||
|
||||
**GitHub MCP Server**:
|
||||
```yaml
|
||||
tools: ['github/*'] # All GitHub tools
|
||||
tools: ['github/get_file_contents', 'github/search_repositories'] # Specific tools
|
||||
```
|
||||
- All read-only tools available by default
|
||||
- Token scoped to source repository
|
||||
|
||||
**Playwright MCP Server**:
|
||||
```yaml
|
||||
tools: ['playwright/*'] # All Playwright tools
|
||||
tools: ['playwright/navigate', 'playwright/screenshot'] # Specific tools
|
||||
```
|
||||
- Configured to access localhost only
|
||||
- Useful for browser automation and testing
|
||||
|
||||
### Tool Selection Best Practices
|
||||
|
||||
- **Principle of Least Privilege**: Only enable tools necessary for the agent's purpose
|
||||
- **Security**: Limit `execute` access unless explicitly required
|
||||
- **Focus**: Fewer tools = clearer agent purpose and better performance
|
||||
- **Documentation**: Comment why specific tools are required for complex configurations
|
||||
|
||||
## Sub-Agent Invocation (Agent Orchestration)
|
||||
|
||||
Agents can invoke other agents using `runSubagent` to orchestrate multi-step workflows.
|
||||
|
||||
### How It Works
|
||||
|
||||
Include `agent` in tools list to enable sub-agent invocation:
|
||||
|
||||
```yaml
|
||||
tools: ['read', 'edit', 'search', 'agent']
|
||||
```
|
||||
|
||||
Then invoke other agents with `runSubagent`:
|
||||
|
||||
```javascript
|
||||
const result = await runSubagent({
|
||||
description: 'What this step does',
|
||||
prompt: `You are the [Specialist] specialist.
|
||||
|
||||
Context:
|
||||
- Parameter: ${parameterValue}
|
||||
- Input: ${inputPath}
|
||||
- Output: ${outputPath}
|
||||
|
||||
Task:
|
||||
1. Do the specific work
|
||||
2. Write results to output location
|
||||
3. Return summary of completion`
|
||||
});
|
||||
```
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
Structure each sub-agent call with:
|
||||
|
||||
1. **description**: Clear one-line purpose of the sub-agent invocation
|
||||
2. **prompt**: Detailed instructions with substituted variables
|
||||
|
||||
The prompt should include:
|
||||
- Who the sub-agent is (specialist role)
|
||||
- What context it needs (parameters, paths)
|
||||
- What to do (concrete tasks)
|
||||
- Where to write output
|
||||
- What to return (summary)
|
||||
|
||||
### Example: Multi-Step Processing
|
||||
|
||||
```javascript
|
||||
// Step 1: Process data
|
||||
const processing = await runSubagent({
|
||||
description: 'Transform raw input data',
|
||||
prompt: `You are the Data Processor specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${basePath}/raw/
|
||||
Output: ${basePath}/processed/
|
||||
|
||||
Task:
|
||||
1. Read all files from input directory
|
||||
2. Apply transformations
|
||||
3. Write processed files to output
|
||||
4. Create summary: ${basePath}/processed/summary.md
|
||||
|
||||
Return: Number of files processed and any issues found`
|
||||
});
|
||||
|
||||
// Step 2: Analyze (depends on Step 1)
|
||||
const analysis = await runSubagent({
|
||||
description: 'Analyze processed data',
|
||||
prompt: `You are the Data Analyst specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${basePath}/processed/
|
||||
Output: ${basePath}/analysis/
|
||||
|
||||
Task:
|
||||
1. Read processed files from input
|
||||
2. Generate analysis report
|
||||
3. Write to: ${basePath}/analysis/report.md
|
||||
|
||||
Return: Key findings and identified patterns`
|
||||
});
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
- **Pass variables in prompts**: Use `${variableName}` for all dynamic values
|
||||
- **Keep prompts focused**: Clear, specific tasks for each sub-agent
|
||||
- **Return summaries**: Each sub-agent should report what it accomplished
|
||||
- **Sequential execution**: Use `await` to maintain order when steps depend on each other
|
||||
- **Error handling**: Check results before proceeding to dependent steps
|
||||
|
||||
### ⚠️ Tool Availability Requirement
|
||||
|
||||
**Critical**: If a sub-agent requires specific tools (e.g., `edit`, `execute`, `search`), the orchestrator must include those tools in its own `tools` list. Sub-agents cannot access tools that aren't available to their parent orchestrator.
|
||||
|
||||
**Example**:
|
||||
```yaml
|
||||
# If your sub-agents need to edit files, execute commands, or search code
|
||||
tools: ['read', 'edit', 'search', 'execute', 'agent']
|
||||
```
|
||||
|
||||
The orchestrator's tool permissions act as a ceiling for all invoked sub-agents. Plan your tool list carefully to ensure all sub-agents have the tools they need.
|
||||
|
||||
### ⚠️ Important Limitation
|
||||
|
||||
**Sub-agent orchestration is NOT suitable for large-scale data processing.** Avoid using `runSubagent` when:
|
||||
- Processing hundreds or thousands of files
|
||||
- Handling large datasets
|
||||
- Performing bulk transformations on big codebases
|
||||
- Orchestrating more than 5-10 sequential steps
|
||||
|
||||
Each sub-agent call adds latency and context overhead. For high-volume processing, implement logic directly in a single agent instead. Use orchestration only for coordinating specialized tasks on focused, manageable datasets.
|
||||
|
||||
## Agent Prompt Structure
|
||||
|
||||
The markdown content below the frontmatter defines the agent's behavior, expertise, and instructions. Well-structured prompts typically include:
|
||||
|
||||
1. **Agent Identity and Role**: Who the agent is and its primary role
|
||||
2. **Core Responsibilities**: What specific tasks the agent performs
|
||||
3. **Approach and Methodology**: How the agent works to accomplish tasks
|
||||
4. **Guidelines and Constraints**: What to do/avoid and quality standards
|
||||
5. **Output Expectations**: Expected output format and quality
|
||||
|
||||
### Prompt Writing Best Practices
|
||||
|
||||
- **Be Specific and Direct**: Use imperative mood ("Analyze", "Generate"); avoid vague terms
|
||||
- **Define Boundaries**: Clearly state scope limits and constraints
|
||||
- **Include Context**: Explain domain expertise and reference relevant frameworks
|
||||
- **Focus on Behavior**: Describe how the agent should think and work
|
||||
- **Use Structured Format**: Headers, bullets, and lists make prompts scannable
|
||||
|
||||
## Variable Definition and Extraction
|
||||
|
||||
Agents can define dynamic parameters to extract values from user input and use them throughout the agent's behavior and sub-agent communications. This enables flexible, context-aware agents that adapt to user-provided data.
|
||||
|
||||
### When to Use Variables
|
||||
|
||||
**Use variables when**:
|
||||
- Agent behavior depends on user input
|
||||
- Need to pass dynamic values to sub-agents
|
||||
- Want to make agents reusable across different contexts
|
||||
- Require parameterized workflows
|
||||
- Need to track or reference user-provided context
|
||||
|
||||
**Examples**:
|
||||
- Extract project name from user prompt
|
||||
- Capture certification name for pipeline processing
|
||||
- Identify file paths or directories
|
||||
- Extract configuration options
|
||||
- Parse feature names or module identifiers
|
||||
|
||||
### Variable Declaration Pattern
|
||||
|
||||
Define variables section early in the agent prompt to document expected parameters:
|
||||
|
||||
```markdown
|
||||
# Agent Name
|
||||
|
||||
## Dynamic Parameters
|
||||
|
||||
- **Parameter Name**: Description and usage
|
||||
- **Another Parameter**: How it's extracted and used
|
||||
|
||||
## Your Mission
|
||||
|
||||
Process [PARAMETER_NAME] to accomplish [task].
|
||||
```
|
||||
|
||||
### Variable Extraction Methods
|
||||
|
||||
#### 1. **Explicit User Input**
|
||||
Ask the user to provide the variable if not detected in the prompt:
|
||||
|
||||
```markdown
|
||||
## Your Mission
|
||||
|
||||
Process the project by analyzing your codebase.
|
||||
|
||||
### Step 1: Identify Project
|
||||
If no project name is provided, **ASK THE USER** for:
|
||||
- Project name or identifier
|
||||
- Base path or directory location
|
||||
- Configuration type (if applicable)
|
||||
|
||||
Use this information to contextualize all subsequent tasks.
|
||||
```
|
||||
|
||||
#### 2. **Implicit Extraction from Prompt**
|
||||
Automatically extract variables from the user's natural language input:
|
||||
|
||||
```javascript
|
||||
// Example: Extract certification name from user input
|
||||
const userInput = "Process My Certification";
|
||||
|
||||
// Extract key information
|
||||
const certificationName = extractCertificationName(userInput);
|
||||
// Result: "My Certification"
|
||||
|
||||
const basePath = `certifications/${certificationName}`;
|
||||
// Result: "certifications/My Certification"
|
||||
```
|
||||
|
||||
#### 3. **Contextual Variable Resolution**
|
||||
Use file context or workspace information to derive variables:
|
||||
|
||||
```markdown
|
||||
## Variable Resolution Strategy
|
||||
|
||||
1. **From User Prompt**: First, look for explicit mentions in user input
|
||||
2. **From File Context**: Check current file name or path
|
||||
3. **From Workspace**: Use workspace folder or active project
|
||||
4. **From Settings**: Reference configuration files
|
||||
5. **Ask User**: If all else fails, request missing information
|
||||
```
|
||||
|
||||
### Using Variables in Agent Prompts
|
||||
|
||||
#### Variable Substitution in Instructions
|
||||
|
||||
Use template variables in agent prompts to make them dynamic:
|
||||
|
||||
```markdown
|
||||
# Agent Name
|
||||
|
||||
## Dynamic Parameters
|
||||
- **Project Name**: ${projectName}
|
||||
- **Base Path**: ${basePath}
|
||||
- **Output Directory**: ${outputDir}
|
||||
|
||||
## Your Mission
|
||||
|
||||
Process the **${projectName}** project located at `${basePath}`.
|
||||
|
||||
## Process Steps
|
||||
|
||||
1. Read input from: `${basePath}/input/`
|
||||
2. Process files according to project configuration
|
||||
3. Write results to: `${outputDir}/`
|
||||
4. Generate summary report
|
||||
|
||||
## Quality Standards
|
||||
|
||||
- Maintain project-specific coding standards for **${projectName}**
|
||||
- Follow directory structure: `${basePath}/[structure]`
|
||||
```
|
||||
|
||||
#### Passing Variables to Sub-Agents
|
||||
|
||||
When invoking a sub-agent, pass all context through template variables in the prompt:
|
||||
|
||||
```javascript
|
||||
// Extract and prepare variables
|
||||
const basePath = `projects/${projectName}`;
|
||||
const inputPath = `${basePath}/src/`;
|
||||
const outputPath = `${basePath}/docs/`;
|
||||
|
||||
// Pass to sub-agent with all variables substituted
|
||||
const result = await runSubagent({
|
||||
description: 'Generate project documentation',
|
||||
prompt: `You are the Documentation specialist.
|
||||
|
||||
Project: ${projectName}
|
||||
Input: ${inputPath}
|
||||
Output: ${outputPath}
|
||||
|
||||
Task:
|
||||
1. Read source files from ${inputPath}
|
||||
2. Generate comprehensive documentation
|
||||
3. Write to ${outputPath}/index.md
|
||||
4. Include code examples and usage guides
|
||||
|
||||
Return: Summary of documentation generated (file count, word count)`
|
||||
});
|
||||
```
|
||||
|
||||
The sub-agent receives all necessary context embedded in the prompt. Variables are resolved before sending the prompt, so the sub-agent works with concrete paths and values, not variable placeholders.
|
||||
|
||||
### Real-World Example: Code Review Orchestrator
|
||||
|
||||
Example of a simple orchestrator that validates code through multiple specialized agents:
|
||||
|
||||
```javascript
|
||||
async function reviewCodePipeline(repositoryName, prNumber) {
|
||||
const basePath = `projects/${repositoryName}/pr-${prNumber}`;
|
||||
|
||||
// Step 1: Security Review
|
||||
const security = await runSubagent({
|
||||
description: 'Scan for security vulnerabilities',
|
||||
prompt: `You are the Security Reviewer specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
PR: ${prNumber}
|
||||
Code: ${basePath}/changes/
|
||||
|
||||
Task:
|
||||
1. Scan code for OWASP Top 10 vulnerabilities
|
||||
2. Check for injection attacks, auth flaws
|
||||
3. Write findings to ${basePath}/security-review.md
|
||||
|
||||
Return: List of critical, high, and medium issues found`
|
||||
});
|
||||
|
||||
// Step 2: Test Coverage Check
|
||||
const coverage = await runSubagent({
|
||||
description: 'Verify test coverage for changes',
|
||||
prompt: `You are the Test Coverage specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
PR: ${prNumber}
|
||||
Changes: ${basePath}/changes/
|
||||
|
||||
Task:
|
||||
1. Analyze code coverage for modified files
|
||||
2. Identify untested critical paths
|
||||
3. Write report to ${basePath}/coverage-report.md
|
||||
|
||||
Return: Current coverage percentage and gaps`
|
||||
});
|
||||
|
||||
// Step 3: Aggregate Results
|
||||
const finalReport = await runSubagent({
|
||||
description: 'Compile all review findings',
|
||||
prompt: `You are the Review Aggregator specialist.
|
||||
|
||||
Repository: ${repositoryName}
|
||||
Reports: ${basePath}/*.md
|
||||
|
||||
Task:
|
||||
1. Read all review reports from ${basePath}/
|
||||
2. Synthesize findings into single report
|
||||
3. Determine overall verdict (APPROVE/NEEDS_FIXES/BLOCK)
|
||||
4. Write to ${basePath}/final-review.md
|
||||
|
||||
Return: Final verdict and executive summary`
|
||||
});
|
||||
|
||||
return finalReport;
|
||||
}
|
||||
```
|
||||
|
||||
This pattern applies to any orchestration scenario: extract variables, call sub-agents with clear context, await results.
|
||||
|
||||
|
||||
### Variable Best Practices
|
||||
|
||||
#### 1. **Clear Documentation**
|
||||
Always document what variables are expected:
|
||||
|
||||
```markdown
|
||||
## Required Variables
|
||||
- **projectName**: The name of the project (string, required)
|
||||
- **basePath**: Root directory for project files (path, required)
|
||||
|
||||
## Optional Variables
|
||||
- **mode**: Processing mode - quick/standard/detailed (enum, default: standard)
|
||||
- **outputFormat**: Output format - markdown/json/html (enum, default: markdown)
|
||||
|
||||
## Derived Variables
|
||||
- **outputDir**: Automatically set to ${basePath}/output
|
||||
- **logFile**: Automatically set to ${basePath}/.log.md
|
||||
```
|
||||
|
||||
#### 2. **Consistent Naming**
|
||||
Use consistent variable naming conventions:
|
||||
|
||||
```javascript
|
||||
// Good: Clear, descriptive naming
|
||||
const variables = {
|
||||
projectName, // What project to work on
|
||||
basePath, // Where project files are located
|
||||
outputDirectory, // Where to save results
|
||||
processingMode, // How to process (detail level)
|
||||
configurationPath // Where config files are
|
||||
};
|
||||
|
||||
// Avoid: Ambiguous or inconsistent
|
||||
const bad_variables = {
|
||||
name, // Too generic
|
||||
path, // Unclear which path
|
||||
mode, // Too short
|
||||
config // Too vague
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. **Validation and Constraints**
|
||||
Document valid values and constraints:
|
||||
|
||||
```markdown
|
||||
## Variable Constraints
|
||||
|
||||
**projectName**:
|
||||
- Type: string (alphanumeric, hyphens, underscores allowed)
|
||||
- Length: 1-100 characters
|
||||
- Required: yes
|
||||
- Pattern: `/^[a-zA-Z0-9_-]+$/`
|
||||
|
||||
**processingMode**:
|
||||
- Type: enum
|
||||
- Valid values: "quick" (< 5min), "standard" (5-15min), "detailed" (15+ min)
|
||||
- Default: "standard"
|
||||
- Required: no
|
||||
```
|
||||
|
||||
## MCP Server Configuration (Organization/Enterprise Only)
|
||||
|
||||
MCP servers extend agent capabilities with additional tools. Only supported for organization and enterprise-level agents.
|
||||
|
||||
### Configuration Format
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-custom-agent
|
||||
description: 'Agent with MCP integration'
|
||||
tools: ['read', 'edit', 'custom-mcp/tool-1']
|
||||
mcp-servers:
|
||||
custom-mcp:
|
||||
type: 'local'
|
||||
command: 'some-command'
|
||||
args: ['--arg1', '--arg2']
|
||||
tools: ["*"]
|
||||
env:
|
||||
ENV_VAR_NAME: ${{ secrets.API_KEY }}
|
||||
---
|
||||
```
|
||||
|
||||
### MCP Server Properties
|
||||
|
||||
- **type**: Server type (`'local'` or `'stdio'`)
|
||||
- **command**: Command to start the MCP server
|
||||
- **args**: Array of command arguments
|
||||
- **tools**: Tools to enable from this server (`["*"]` for all)
|
||||
- **env**: Environment variables (supports secrets)
|
||||
|
||||
### Environment Variables and Secrets
|
||||
|
||||
Secrets must be configured in repository settings under "copilot" environment.
|
||||
|
||||
**Supported syntax**:
|
||||
```yaml
|
||||
env:
|
||||
# Environment variable only
|
||||
VAR_NAME: COPILOT_MCP_ENV_VAR_VALUE
|
||||
|
||||
# Variable with header
|
||||
VAR_NAME: $COPILOT_MCP_ENV_VAR_VALUE
|
||||
VAR_NAME: ${COPILOT_MCP_ENV_VAR_VALUE}
|
||||
|
||||
# GitHub Actions-style (YAML only)
|
||||
VAR_NAME: ${{ secrets.COPILOT_MCP_ENV_VAR_VALUE }}
|
||||
VAR_NAME: ${{ var.COPILOT_MCP_ENV_VAR_VALUE }}
|
||||
```
|
||||
|
||||
## File Organization and Naming
|
||||
|
||||
### Repository-Level Agents
|
||||
- Location: `.github/agents/`
|
||||
- Scope: Available only in the specific repository
|
||||
- Access: Uses repository-configured MCP servers
|
||||
|
||||
### Organization/Enterprise-Level Agents
|
||||
- Location: `.github-private/agents/` (then move to `agents/` root)
|
||||
- Scope: Available across all repositories in org/enterprise
|
||||
- Access: Can configure dedicated MCP servers
|
||||
|
||||
### Naming Conventions
|
||||
- Use lowercase with hyphens: `test-specialist.agent.md`
|
||||
- Name should reflect agent purpose
|
||||
- Filename becomes default agent name (if `name` not specified)
|
||||
- Allowed characters: `.`, `-`, `_`, `a-z`, `A-Z`, `0-9`
|
||||
|
||||
## Agent Processing and Behavior
|
||||
|
||||
### Versioning
|
||||
- Based on Git commit SHAs for the agent file
|
||||
- Create branches/tags for different agent versions
|
||||
- Instantiated using latest version for repository/branch
|
||||
- PR interactions use same agent version for consistency
|
||||
|
||||
### Name Conflicts
|
||||
Priority (highest to lowest):
|
||||
1. Repository-level agent
|
||||
2. Organization-level agent
|
||||
3. Enterprise-level agent
|
||||
|
||||
Lower-level configurations override higher-level ones with the same name.
|
||||
|
||||
### Tool Processing
|
||||
- `tools` list filters available tools (built-in and MCP)
|
||||
- No tools specified = all tools enabled
|
||||
- Empty list (`[]`) = all tools disabled
|
||||
- Specific list = only those tools enabled
|
||||
- Unrecognized tool names are ignored (allows environment-specific tools)
|
||||
|
||||
### MCP Server Processing Order
|
||||
1. Out-of-the-box MCP servers (e.g., GitHub MCP)
|
||||
2. Custom agent MCP configuration (org/enterprise only)
|
||||
3. Repository-level MCP configurations
|
||||
|
||||
Each level can override settings from previous levels.
|
||||
|
||||
## Agent Creation Checklist
|
||||
|
||||
### Frontmatter
|
||||
- [ ] `description` field present and descriptive (50-150 chars)
|
||||
- [ ] `description` wrapped in single quotes
|
||||
- [ ] `name` specified (optional but recommended)
|
||||
- [ ] `tools` configured appropriately (or intentionally omitted)
|
||||
- [ ] `model` specified for optimal performance
|
||||
- [ ] `target` set if environment-specific
|
||||
- [ ] `infer` set to `false` if manual selection required
|
||||
|
||||
### Prompt Content
|
||||
- [ ] Clear agent identity and role defined
|
||||
- [ ] Core responsibilities listed explicitly
|
||||
- [ ] Approach and methodology explained
|
||||
- [ ] Guidelines and constraints specified
|
||||
- [ ] Output expectations documented
|
||||
- [ ] Examples provided where helpful
|
||||
- [ ] Instructions are specific and actionable
|
||||
- [ ] Scope and boundaries clearly defined
|
||||
- [ ] Total content under 30,000 characters
|
||||
|
||||
### File Structure
|
||||
- [ ] Filename follows lowercase-with-hyphens convention
|
||||
- [ ] File placed in correct directory (`.github/agents/` or `agents/`)
|
||||
- [ ] Filename uses only allowed characters
|
||||
- [ ] File extension is `.agent.md`
|
||||
|
||||
### Quality Assurance
|
||||
- [ ] Agent purpose is unique and not duplicative
|
||||
- [ ] Tools are minimal and necessary
|
||||
- [ ] Instructions are clear and unambiguous
|
||||
- [ ] Agent has been tested with representative tasks
|
||||
- [ ] Documentation references are current
|
||||
- [ ] Security considerations addressed (if applicable)
|
||||
|
||||
## Common Agent Patterns
|
||||
|
||||
### Testing Specialist
|
||||
**Purpose**: Focus on test coverage and quality
|
||||
**Tools**: All tools (for comprehensive test creation)
|
||||
**Approach**: Analyze, identify gaps, write tests, avoid production code changes
|
||||
|
||||
### Implementation Planner
|
||||
**Purpose**: Create detailed technical plans and specifications
|
||||
**Tools**: Limited to `['read', 'search', 'edit']`
|
||||
**Approach**: Analyze requirements, create documentation, avoid implementation
|
||||
|
||||
### Code Reviewer
|
||||
**Purpose**: Review code quality and provide feedback
|
||||
**Tools**: `['read', 'search']` only
|
||||
**Approach**: Analyze, suggest improvements, no direct modifications
|
||||
|
||||
### Refactoring Specialist
|
||||
**Purpose**: Improve code structure and maintainability
|
||||
**Tools**: `['read', 'search', 'edit']`
|
||||
**Approach**: Analyze patterns, propose refactorings, implement safely
|
||||
|
||||
### Security Auditor
|
||||
**Purpose**: Identify security issues and vulnerabilities
|
||||
**Tools**: `['read', 'search', 'web']`
|
||||
**Approach**: Scan code, check against OWASP, report findings
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### Frontmatter Errors
|
||||
- ❌ Missing `description` field
|
||||
- ❌ Description not wrapped in quotes
|
||||
- ❌ Invalid tool names without checking documentation
|
||||
- ❌ Incorrect YAML syntax (indentation, quotes)
|
||||
|
||||
### Tool Configuration Issues
|
||||
- ❌ Granting excessive tool access unnecessarily
|
||||
- ❌ Missing required tools for agent's purpose
|
||||
- ❌ Not using tool aliases consistently
|
||||
- ❌ Forgetting MCP server namespace (`server-name/tool`)
|
||||
|
||||
### Prompt Content Problems
|
||||
- ❌ Vague, ambiguous instructions
|
||||
- ❌ Conflicting or contradictory guidelines
|
||||
- ❌ Lack of clear scope definition
|
||||
- ❌ Missing output expectations
|
||||
- ❌ Overly verbose instructions (exceeding character limits)
|
||||
- ❌ No examples or context for complex tasks
|
||||
|
||||
### Organizational Issues
|
||||
- ❌ Filename doesn't reflect agent purpose
|
||||
- ❌ Wrong directory (confusing repo vs org level)
|
||||
- ❌ Using spaces or special characters in filename
|
||||
- ❌ Duplicate agent names causing conflicts
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
### Manual Testing
|
||||
1. Create the agent file with proper frontmatter
|
||||
2. Reload VS Code or refresh GitHub.com
|
||||
3. Select the agent from the dropdown in Copilot Chat
|
||||
4. Test with representative user queries
|
||||
5. Verify tool access works as expected
|
||||
6. Confirm output meets expectations
|
||||
|
||||
### Integration Testing
|
||||
- Test agent with different file types in scope
|
||||
- Verify MCP server connectivity (if configured)
|
||||
- Check agent behavior with missing context
|
||||
- Test error handling and edge cases
|
||||
- Validate agent switching and handoffs
|
||||
|
||||
### Quality Checks
|
||||
- Run through agent creation checklist
|
||||
- Review against common mistakes list
|
||||
- Compare with example agents in repository
|
||||
- Get peer review for complex agents
|
||||
- Document any special configuration needs
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Official Documentation
|
||||
- [Creating Custom Agents](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/create-custom-agents)
|
||||
- [Custom Agents Configuration](https://docs.github.com/en/copilot/reference/custom-agents-configuration)
|
||||
- [Custom Agents in VS Code](https://code.visualstudio.com/docs/copilot/customization/custom-agents)
|
||||
- [MCP Integration](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/extend-coding-agent-with-mcp)
|
||||
|
||||
### Community Resources
|
||||
- [Awesome Copilot Agents Collection](https://github.com/github/awesome-copilot/tree/main/agents)
|
||||
- [Customization Library Examples](https://docs.github.com/en/copilot/tutorials/customization-library/custom-agents)
|
||||
- [Your First Custom Agent Tutorial](https://docs.github.com/en/copilot/tutorials/customization-library/custom-agents/your-first-custom-agent)
|
||||
|
||||
### Related Files
|
||||
- [Prompt Files Guidelines](./prompt.instructions.md) - For creating prompt files
|
||||
- [Instructions Guidelines](./instructions.instructions.md) - For creating instruction files
|
||||
|
||||
## Version Compatibility Notes
|
||||
|
||||
### GitHub.com (Coding Agent)
|
||||
- ✅ Fully supports all standard frontmatter properties
|
||||
- ✅ Repository and org/enterprise level agents
|
||||
- ✅ MCP server configuration (org/enterprise)
|
||||
- ❌ Does not support `model`, `argument-hint`, `handoffs` properties
|
||||
|
||||
### VS Code / JetBrains / Eclipse / Xcode
|
||||
- ✅ Supports `model` property for AI model selection
|
||||
- ✅ Supports `argument-hint` and `handoffs` properties
|
||||
- ✅ User profile and workspace-level agents
|
||||
- ❌ Cannot configure MCP servers at repository level
|
||||
- ⚠️ Some properties may behave differently
|
||||
|
||||
When creating agents for multiple environments, focus on common properties and test in all target environments. Use `target` property to create environment-specific agents when necessary.
|
||||
@@ -1,187 +0,0 @@
|
||||
---
|
||||
description: 'Best practices for Azure DevOps Pipeline YAML files'
|
||||
applyTo: '**/azure-pipelines.yml, **/azure-pipelines*.yml, **/*.pipeline.yml'
|
||||
---
|
||||
|
||||
# Azure DevOps Pipeline YAML Best Practices
|
||||
|
||||
Guidelines for creating maintainable, secure, and efficient Azure DevOps pipelines in PowerToys.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
- Use YAML syntax consistently with proper indentation (2 spaces)
|
||||
- Always include meaningful names and display names for pipelines, stages, jobs, and steps
|
||||
- Implement proper error handling and conditional execution
|
||||
- Use variables and parameters to make pipelines reusable and maintainable
|
||||
- Follow the principle of least privilege for service connections and permissions
|
||||
- Include comprehensive logging and diagnostics for troubleshooting
|
||||
|
||||
## Pipeline Structure
|
||||
|
||||
- Organize complex pipelines using stages for better visualization and control
|
||||
- Use jobs to group related steps and enable parallel execution when possible
|
||||
- Implement proper dependencies between stages and jobs
|
||||
- Use templates for reusable pipeline components
|
||||
- Keep pipeline files focused and modular - split large pipelines into multiple files
|
||||
|
||||
## Build Best Practices
|
||||
|
||||
- Use specific agent pool versions and VM images for consistency
|
||||
- Cache dependencies (npm, NuGet, Maven, etc.) to improve build performance
|
||||
- Implement proper artifact management with meaningful names and retention policies
|
||||
- Use build variables for version numbers and build metadata
|
||||
- Include code quality gates (lint checks, testing, security scans)
|
||||
- Ensure builds are reproducible and environment-independent
|
||||
|
||||
## Testing Integration
|
||||
|
||||
- Run unit tests as part of the build process
|
||||
- Publish test results in standard formats (JUnit, VSTest, etc.)
|
||||
- Include code coverage reporting and quality gates
|
||||
- Implement integration and end-to-end tests in appropriate stages
|
||||
- Use test impact analysis when available to optimize test execution
|
||||
- Fail fast on test failures to provide quick feedback
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Use Azure Key Vault for sensitive configuration and secrets
|
||||
- Implement proper secret management with variable groups
|
||||
- Use service connections with minimal required permissions
|
||||
- Enable security scans (dependency vulnerabilities, static analysis)
|
||||
- Implement approval gates for production deployments
|
||||
- Use managed identities when possible instead of service principals
|
||||
|
||||
## Deployment Strategies
|
||||
|
||||
- Implement proper environment promotion (dev → staging → production)
|
||||
- Use deployment jobs with proper environment targeting
|
||||
- Implement blue-green or canary deployment strategies when appropriate
|
||||
- Include rollback mechanisms and health checks
|
||||
- Use infrastructure as code (ARM, Bicep, Terraform) for consistent deployments
|
||||
- Implement proper configuration management per environment
|
||||
|
||||
## Variable and Parameter Management
|
||||
|
||||
- Use variable groups for shared configuration across pipelines
|
||||
- Implement runtime parameters for flexible pipeline execution
|
||||
- Use conditional variables based on branches or environments
|
||||
- Secure sensitive variables and mark them as secrets
|
||||
- Document variable purposes and expected values
|
||||
- Use variable templates for complex variable logic
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
- Use parallel jobs and matrix strategies when appropriate
|
||||
- Implement proper caching strategies for dependencies and build outputs
|
||||
- Use shallow clone for Git operations when full history isn't needed
|
||||
- Optimize Docker image builds with multi-stage builds and layer caching
|
||||
- Monitor pipeline performance and optimize bottlenecks
|
||||
- Use pipeline resource triggers efficiently
|
||||
|
||||
## Monitoring and Observability
|
||||
|
||||
- Include comprehensive logging throughout the pipeline
|
||||
- Use Azure Monitor and Application Insights for deployment tracking
|
||||
- Implement proper notification strategies for failures and successes
|
||||
- Include deployment health checks and automated rollback triggers
|
||||
- Use pipeline analytics to identify improvement opportunities
|
||||
- Document pipeline behavior and troubleshooting steps
|
||||
|
||||
## Template and Reusability
|
||||
|
||||
- Create pipeline templates for common patterns
|
||||
- Use extends templates for complete pipeline inheritance
|
||||
- Implement step templates for reusable task sequences
|
||||
- Use variable templates for complex variable logic
|
||||
- Version templates appropriately for stability
|
||||
- Document template parameters and usage examples
|
||||
|
||||
## Branch and Trigger Strategy
|
||||
|
||||
- Implement appropriate triggers for different branch types
|
||||
- Use path filters to trigger builds only when relevant files change
|
||||
- Configure proper CI/CD triggers for main/master branches
|
||||
- Use pull request triggers for code validation
|
||||
- Implement scheduled triggers for maintenance tasks
|
||||
- Consider resource triggers for multi-repository scenarios
|
||||
|
||||
## Example Structure
|
||||
|
||||
```yaml
|
||||
# azure-pipelines.yml
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
exclude:
|
||||
- docs/*
|
||||
- README.md
|
||||
|
||||
variables:
|
||||
- group: shared-variables
|
||||
- name: buildConfiguration
|
||||
value: 'Release'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: 'Build and Test'
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: 'Build Application'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET SDK'
|
||||
inputs:
|
||||
version: '8.x'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Restore dependencies'
|
||||
inputs:
|
||||
command: 'restore'
|
||||
projects: '**/*.csproj'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build application'
|
||||
inputs:
|
||||
command: 'build'
|
||||
projects: '**/*.csproj'
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore'
|
||||
|
||||
- stage: Deploy
|
||||
displayName: 'Deploy to Staging'
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
jobs:
|
||||
- deployment: DeployToStaging
|
||||
displayName: 'Deploy to Staging Environment'
|
||||
environment: 'staging'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- download: current
|
||||
displayName: 'Download drop artifact'
|
||||
artifact: drop
|
||||
- task: AzureWebApp@1
|
||||
displayName: 'Deploy to Azure Web App'
|
||||
inputs:
|
||||
azureSubscription: 'staging-service-connection'
|
||||
appType: 'webApp'
|
||||
appName: 'myapp-staging'
|
||||
package: '$(Pipeline.Workspace)/drop/**/*.zip'
|
||||
```
|
||||
|
||||
## Common Anti-Patterns to Avoid
|
||||
|
||||
- Hardcoding sensitive values directly in YAML files
|
||||
- Using overly broad triggers that cause unnecessary builds
|
||||
- Mixing build and deployment logic in a single stage
|
||||
- Not implementing proper error handling and cleanup
|
||||
- Using deprecated task versions without upgrade plans
|
||||
- Creating monolithic pipelines that are difficult to maintain
|
||||
- Not using proper naming conventions for clarity
|
||||
- Ignoring pipeline security best practices
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for shared libraries including logging, IPC, settings, DPI, telemetry, and utilities consumed by multiple modules'
|
||||
applyTo: 'src/common/**'
|
||||
---
|
||||
|
||||
# Common Libraries – Shared Code Guidance
|
||||
|
||||
Guidelines for modifying shared code in `src/common/`. Changes here can have wide-reaching impact across the entire PowerToys codebase.
|
||||
|
||||
## Scope
|
||||
|
||||
- Logging infrastructure (`src/common/logger/`)
|
||||
- IPC primitives and named pipe utilities
|
||||
- Settings serialization and management
|
||||
- DPI awareness and scaling utilities
|
||||
- Telemetry helpers
|
||||
- General utilities (JSON parsing, string helpers, etc.)
|
||||
|
||||
## Guidelines
|
||||
|
||||
### API Stability
|
||||
|
||||
- Avoid breaking public headers/APIs; if changed, search & update all callers
|
||||
- Coordinate ABI-impacting struct/class layout changes; keep binary compatibility
|
||||
- When modifying public interfaces, grep the entire codebase for usages
|
||||
|
||||
### Performance
|
||||
|
||||
- Watch perf in hot paths (hooks, timers, serialization)
|
||||
- Avoid avoidable allocations in frequently called code
|
||||
- Profile changes that touch performance-sensitive areas
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Ask before adding third-party deps or changing serialization formats
|
||||
- New dependencies must be MIT-licensed or approved by PM team
|
||||
- Add any new external packages to `NOTICE.md`
|
||||
|
||||
### Logging
|
||||
|
||||
- C++ logging uses spdlog (`Logger::info`, `Logger::warn`, `Logger::error`, `Logger::debug`)
|
||||
- Initialize with `init_logger()` early in startup
|
||||
- Keep hot paths quiet – no logging in tight loops or hooks
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- No unintended ABI breaks
|
||||
- No noisy logs in hot paths
|
||||
- New non-obvious symbols briefly commented
|
||||
- All callers updated when interfaces change
|
||||
|
||||
## Code Style
|
||||
|
||||
- **C++**: Follow `.clang-format` in `src/`; use Modern C++ patterns per C++ Core Guidelines
|
||||
- **C#**: Follow `src/.editorconfig`; enforce StyleCop.Analyzers
|
||||
|
||||
## Validation
|
||||
|
||||
- Build: `tools\build\build.cmd` from `src/common/` folder
|
||||
- Verify no ABI breaks: grep for changed function/struct names across codebase
|
||||
- Check logs: ensure no new logging in performance-critical paths
|
||||
256
.github/instructions/instructions.instructions.md
vendored
256
.github/instructions/instructions.instructions.md
vendored
@@ -1,256 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for creating high-quality custom instruction files for GitHub Copilot'
|
||||
applyTo: '**/*.instructions.md'
|
||||
---
|
||||
|
||||
# Custom Instructions File Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable custom instruction files that guide GitHub Copilot in generating domain-specific code and following project conventions.
|
||||
|
||||
## Project Context
|
||||
|
||||
- Target audience: Developers and GitHub Copilot working with domain-specific code
|
||||
- File format: Markdown with YAML frontmatter
|
||||
- File naming convention: lowercase with hyphens (e.g., `react-best-practices.instructions.md`)
|
||||
- Location: `.github/instructions/` directory
|
||||
- Purpose: Provide context-aware guidance for code generation, review, and documentation
|
||||
|
||||
## Required Frontmatter
|
||||
|
||||
Every instruction file must include YAML frontmatter with the following fields:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: 'Brief description of the instruction purpose and scope'
|
||||
applyTo: 'glob pattern for target files (e.g., **/*.ts, **/*.py)'
|
||||
---
|
||||
```
|
||||
|
||||
### Frontmatter Guidelines
|
||||
|
||||
- **description**: Single-quoted string, 1-500 characters, clearly stating the purpose
|
||||
- **applyTo**: Glob pattern(s) specifying which files these instructions apply to
|
||||
- Single pattern: `'**/*.ts'`
|
||||
- Multiple patterns: `'**/*.ts, **/*.tsx, **/*.js'`
|
||||
- Specific files: `'src/**/*.py'`
|
||||
- All files: `'**'`
|
||||
|
||||
## File Structure
|
||||
|
||||
A well-structured instruction file should include the following sections:
|
||||
|
||||
### 1. Title and Overview
|
||||
|
||||
- Clear, descriptive title using `#` heading
|
||||
- Brief introduction explaining the purpose and scope
|
||||
- Optional: Project context section with key technologies and versions
|
||||
|
||||
### 2. Core Sections
|
||||
|
||||
Organize content into logical sections based on the domain:
|
||||
|
||||
- **General Instructions**: High-level guidelines and principles
|
||||
- **Best Practices**: Recommended patterns and approaches
|
||||
- **Code Standards**: Naming conventions, formatting, style rules
|
||||
- **Architecture/Structure**: Project organization and design patterns
|
||||
- **Common Patterns**: Frequently used implementations
|
||||
- **Security**: Security considerations (if applicable)
|
||||
- **Performance**: Optimization guidelines (if applicable)
|
||||
- **Testing**: Testing standards and approaches (if applicable)
|
||||
|
||||
### 3. Examples and Code Snippets
|
||||
|
||||
Provide concrete examples with clear labels:
|
||||
|
||||
```markdown
|
||||
### Good Example
|
||||
\`\`\`language
|
||||
// Recommended approach
|
||||
code example here
|
||||
\`\`\`
|
||||
|
||||
### Bad Example
|
||||
\`\`\`language
|
||||
// Avoid this pattern
|
||||
code example here
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### 4. Validation and Verification (Optional but Recommended)
|
||||
|
||||
- Build commands to verify code
|
||||
- Lint checks and formatting tools
|
||||
- Testing requirements
|
||||
- Verification steps
|
||||
|
||||
## Content Guidelines
|
||||
|
||||
### Writing Style
|
||||
|
||||
- Use clear, concise language
|
||||
- Write in imperative mood ("Use", "Implement", "Avoid")
|
||||
- Be specific and actionable
|
||||
- Avoid ambiguous terms like "should", "might", "possibly"
|
||||
- Use bullet points and lists for readability
|
||||
- Keep sections focused and scannable
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **Be Specific**: Provide concrete examples rather than abstract concepts
|
||||
- **Show Why**: Explain the reasoning behind recommendations when it adds value
|
||||
- **Use Tables**: For comparing options, listing rules, or showing patterns
|
||||
- **Include Examples**: Real code snippets are more effective than descriptions
|
||||
- **Stay Current**: Reference current versions and best practices
|
||||
- **Link Resources**: Include official documentation and authoritative sources
|
||||
|
||||
### Common Patterns to Include
|
||||
|
||||
1. **Naming Conventions**: How to name variables, functions, classes, files
|
||||
2. **Code Organization**: File structure, module organization, import order
|
||||
3. **Error Handling**: Preferred error handling patterns
|
||||
4. **Dependencies**: How to manage and document dependencies
|
||||
5. **Comments and Documentation**: When and how to document code
|
||||
6. **Version Information**: Target language/framework versions
|
||||
|
||||
## Patterns to Follow
|
||||
|
||||
### Bullet Points and Lists
|
||||
|
||||
```markdown
|
||||
## Security Best Practices
|
||||
|
||||
- Always validate user input before processing
|
||||
- Use parameterized queries to prevent SQL injection
|
||||
- Store secrets in environment variables, never in code
|
||||
- Implement proper authentication and authorization
|
||||
- Enable HTTPS for all production endpoints
|
||||
```
|
||||
|
||||
### Tables for Structured Information
|
||||
|
||||
```markdown
|
||||
## Common Issues
|
||||
|
||||
| Issue | Solution | Example |
|
||||
| ---------------- | ------------------- | ----------------------------- |
|
||||
| Magic numbers | Use named constants | `const MAX_RETRIES = 3` |
|
||||
| Deep nesting | Extract functions | Refactor nested if statements |
|
||||
| Hardcoded values | Use configuration | Store API URLs in config |
|
||||
```
|
||||
|
||||
### Code Comparison
|
||||
|
||||
```markdown
|
||||
### Good Example - Using TypeScript interfaces
|
||||
\`\`\`typescript
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
function getUser(id: string): User {
|
||||
// Implementation
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Bad Example - Using any type
|
||||
\`\`\`typescript
|
||||
function getUser(id: any): any {
|
||||
// Loses type safety
|
||||
}
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### Conditional Guidance
|
||||
|
||||
```markdown
|
||||
## Framework Selection
|
||||
|
||||
- **For small projects**: Use Minimal API approach
|
||||
- **For large projects**: Use controller-based architecture with clear separation
|
||||
- **For microservices**: Consider domain-driven design patterns
|
||||
```
|
||||
|
||||
## Patterns to Avoid
|
||||
|
||||
- **Overly verbose explanations**: Keep it concise and scannable
|
||||
- **Outdated information**: Always reference current versions and practices
|
||||
- **Ambiguous guidelines**: Be specific about what to do or avoid
|
||||
- **Missing examples**: Abstract rules without concrete code examples
|
||||
- **Contradictory advice**: Ensure consistency throughout the file
|
||||
- **Copy-paste from documentation**: Add value by distilling and providing context
|
||||
|
||||
## Testing Your Instructions
|
||||
|
||||
Before finalizing instruction files:
|
||||
|
||||
1. **Test with Copilot**: Try the instructions with actual prompts in VS Code
|
||||
2. **Verify Examples**: Ensure code examples are correct and run without errors
|
||||
3. **Check Glob Patterns**: Confirm `applyTo` patterns match intended files
|
||||
|
||||
## Example Structure
|
||||
|
||||
Here's a minimal example structure for a new instruction file:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: 'Brief description of purpose'
|
||||
applyTo: '**/*.ext'
|
||||
---
|
||||
|
||||
# Technology Name Development
|
||||
|
||||
Brief introduction and context.
|
||||
|
||||
## General Instructions
|
||||
|
||||
- High-level guideline 1
|
||||
- High-level guideline 2
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Specific practice 1
|
||||
- Specific practice 2
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Naming Conventions
|
||||
- Rule 1
|
||||
- Rule 2
|
||||
|
||||
### File Organization
|
||||
- Structure 1
|
||||
- Structure 2
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1
|
||||
Description and example
|
||||
|
||||
\`\`\`language
|
||||
code example
|
||||
\`\`\`
|
||||
|
||||
### Pattern 2
|
||||
Description and example
|
||||
|
||||
## Validation
|
||||
|
||||
- Build command: `command to verify`
|
||||
- Lint checks: `command to lint`
|
||||
- Testing: `command to test`
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
- Review instructions when dependencies or frameworks are updated
|
||||
- Update examples to reflect current best practices
|
||||
- Remove outdated patterns or deprecated features
|
||||
- Add new patterns as they emerge in the community
|
||||
- Keep glob patterns accurate as project structure evolves
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Custom Instructions Documentation](https://code.visualstudio.com/docs/copilot/customization/custom-instructions)
|
||||
- [Awesome Copilot Instructions](https://github.com/github/awesome-copilot/tree/main/instructions)
|
||||
88
.github/instructions/prompt.instructions.md
vendored
88
.github/instructions/prompt.instructions.md
vendored
@@ -1,88 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for creating high-quality prompt files for GitHub Copilot'
|
||||
applyTo: '**/*.prompt.md'
|
||||
---
|
||||
|
||||
# Copilot Prompt Files Guidelines
|
||||
|
||||
Instructions for creating effective and maintainable prompt files that guide GitHub Copilot in delivering consistent, high-quality outcomes across any repository.
|
||||
|
||||
## Scope and Principles
|
||||
- Target audience: maintainers and contributors authoring reusable prompts for Copilot Chat.
|
||||
- Goals: predictable behaviour, clear expectations, minimal permissions, and portability across repositories.
|
||||
- Primary references: VS Code documentation on prompt files and organization-specific conventions.
|
||||
|
||||
## Frontmatter Requirements
|
||||
|
||||
Every prompt file should include YAML frontmatter with the following fields:
|
||||
|
||||
### Required/Recommended Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `description` | Recommended | A short description of the prompt (single sentence, actionable outcome) |
|
||||
| `name` | Optional | The name shown after typing `/` in chat. Defaults to filename if not specified |
|
||||
| `agent` | Recommended | The agent to use: `ask`, `edit`, `agent`, or a custom agent name. Defaults to current agent |
|
||||
| `model` | Optional | The language model to use. Defaults to currently selected model |
|
||||
| `tools` | Optional | List of tool/tool set names available for this prompt |
|
||||
| `argument-hint` | Optional | Hint text shown in chat input to guide user interaction |
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Use consistent quoting (single quotes recommended) and keep one field per line for readability and version control clarity
|
||||
- If `tools` are specified and current agent is `ask` or `edit`, the default agent becomes `agent`
|
||||
- Preserve any additional metadata (`language`, `tags`, `visibility`, etc.) required by your organization
|
||||
|
||||
## File Naming and Placement
|
||||
- Use kebab-case filenames ending with `.prompt.md` and store them under `.github/prompts/` unless your workspace standard specifies another directory.
|
||||
- Provide a short filename that communicates the action (for example, `generate-readme.prompt.md` rather than `prompt1.prompt.md`).
|
||||
|
||||
## Body Structure
|
||||
- Start with an `#` level heading that matches the prompt intent so it surfaces well in Quick Pick search.
|
||||
- Organize content with predictable sections. Recommended baseline: `Mission` or `Primary Directive`, `Scope & Preconditions`, `Inputs`, `Workflow` (step-by-step), `Output Expectations`, and `Quality Assurance`.
|
||||
- Adjust section names to fit the domain, but retain the logical flow: why → context → inputs → actions → outputs → validation.
|
||||
- Reference related prompts or instruction files using relative links to aid discoverability.
|
||||
|
||||
## Input and Context Handling
|
||||
- Use `${input:variableName[:placeholder]}` for required values and explain when the user must supply them. Provide defaults or alternatives where possible.
|
||||
- Call out contextual variables such as `${selection}`, `${file}`, `${workspaceFolder}` only when they are essential, and describe how Copilot should interpret them.
|
||||
- Document how to proceed when mandatory context is missing (for example, “Request the file path and stop if it remains undefined”).
|
||||
|
||||
## Tool and Permission Guidance
|
||||
- Limit `tools` to the smallest set that enables the task. List them in the preferred execution order when the sequence matters.
|
||||
- If the prompt inherits tools from a chat mode, mention that relationship and state any critical tool behaviours or side effects.
|
||||
- Warn about destructive operations (file creation, edits, terminal commands) and include guard rails or confirmation steps in the workflow.
|
||||
|
||||
## Instruction Tone and Style
|
||||
- Write in direct, imperative sentences targeted at Copilot (for example, “Analyze”, “Generate”, “Summarize”).
|
||||
- Keep sentences short and unambiguous, following Google Developer Documentation translation best practices to support localization.
|
||||
- Avoid idioms, humor, or culturally specific references; favor neutral, inclusive language.
|
||||
|
||||
## Output Definition
|
||||
- Specify the format, structure, and location of expected results (for example, “Create an architecture decision record file using the template below, such as `docs/architecture-decisions/record-XXXX.md`).
|
||||
- Include success criteria and failure triggers so Copilot knows when to halt or retry.
|
||||
- Provide validation steps—manual checks, automated commands, or acceptance criteria lists—that reviewers can execute after running the prompt.
|
||||
|
||||
## Examples and Reusable Assets
|
||||
- Embed Good/Bad examples or scaffolds (Markdown templates, JSON stubs) that the prompt should produce or follow.
|
||||
- Maintain reference tables (capabilities, status codes, role descriptions) inline to keep the prompt self-contained. Update these tables when upstream resources change.
|
||||
- Link to authoritative documentation instead of duplicating lengthy guidance.
|
||||
|
||||
## Quality Assurance Checklist
|
||||
- [ ] Frontmatter fields are complete, accurate, and least-privilege.
|
||||
- [ ] Inputs include placeholders, default behaviours, and fallbacks.
|
||||
- [ ] Workflow covers preparation, execution, and post-processing without gaps.
|
||||
- [ ] Output expectations include formatting and storage details.
|
||||
- [ ] Validation steps are actionable (commands, diff checks, review prompts).
|
||||
- [ ] Security, compliance, and privacy policies referenced by the prompt are current.
|
||||
- [ ] Prompt executes successfully in VS Code (`Chat: Run Prompt`) using representative scenarios.
|
||||
|
||||
## Maintenance Guidance
|
||||
- Version-control prompts alongside the code they affect; update them when dependencies, tooling, or review processes change.
|
||||
- Review prompts periodically to ensure tool lists, model requirements, and linked documents remain valid.
|
||||
- Coordinate with other repositories: when a prompt proves broadly useful, extract common guidance into instruction files or shared prompt packs.
|
||||
|
||||
## Additional Resources
|
||||
- [Prompt Files Documentation](https://code.visualstudio.com/docs/copilot/customization/prompt-files#_prompt-file-format)
|
||||
- [Awesome Copilot Prompt Files](https://github.com/github/awesome-copilot/tree/main/prompts)
|
||||
- [Tool Configuration](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode#_agent-mode-tools)
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
description: 'Guidelines for Runner and Settings UI components that communicate via named pipes and manage module lifecycle'
|
||||
applyTo: 'src/runner/**,src/settings-ui/**'
|
||||
---
|
||||
|
||||
# Runner & Settings UI – Core Components Guidance
|
||||
|
||||
Guidelines for modifying the Runner (tray/module loader) and Settings UI (configuration app). These components communicate via Windows Named Pipes using JSON messages.
|
||||
|
||||
## Runner (`src/runner/`)
|
||||
|
||||
### Scope
|
||||
|
||||
- Module bootstrap, hotkey management, settings bridge, update/elevation handling
|
||||
|
||||
### Guidelines
|
||||
|
||||
- If IPC/JSON contracts change, mirror updates in `src/settings-ui/**`
|
||||
- Keep module discovery in `src/runner/main.cpp` in sync when adding/removing modules
|
||||
- Keep startup lean: avoid blocking/network calls in early init path
|
||||
- Preserve GPO & elevation behaviors; confirm no regression in policy handling
|
||||
- Ask before modifying update workflow or elevation logic
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Stable startup, consistent contracts, no unnecessary logging noise
|
||||
|
||||
## Settings UI (`src/settings-ui/`)
|
||||
|
||||
### Scope
|
||||
|
||||
- WinUI/WPF UI, communicates with Runner over named pipes; manages persisted settings schema
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Don't break settings schema silently; add migration when shape changes
|
||||
- If IPC/JSON contracts change, align with `src/runner/**` implementation
|
||||
- Keep UI responsive: marshal to UI thread for UI-bound operations
|
||||
- Reuse existing styles/resources; avoid duplicate theme keys
|
||||
- Add/adjust migration or serialization tests when changing persisted settings
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- Schema integrity preserved, responsive UI, consistent contracts, no style duplication
|
||||
|
||||
## Shared Concerns
|
||||
|
||||
### IPC Contract Changes
|
||||
|
||||
When modifying the JSON message format between Runner and Settings UI:
|
||||
|
||||
1. Update both `src/runner/` and `src/settings-ui/` in the same PR
|
||||
2. Preserve backward compatibility where possible
|
||||
3. Add migration logic for settings schema changes
|
||||
4. Test both directions of communication
|
||||
|
||||
### Code Style
|
||||
|
||||
- **C++ (Runner)**: Follow `.clang-format` in `src/`
|
||||
- **C# (Settings UI)**: Follow `src/.editorconfig`, use StyleCop.Analyzers
|
||||
- **XAML**: Use XamlStyler or run `.\.pipelines\applyXamlStyling.ps1 -Main`
|
||||
|
||||
## Validation
|
||||
|
||||
- Build Runner: `tools\build\build.cmd` from `src/runner/`
|
||||
- Build Settings UI: `tools\build\build.cmd` from `src/settings-ui/`
|
||||
- Test IPC: Launch both Runner and Settings UI, verify communication works
|
||||
- Schema changes: Run serialization tests if settings shape changed
|
||||
@@ -1,228 +0,0 @@
|
||||
---
|
||||
description: 'Instructions for building Model Context Protocol (MCP) servers using the TypeScript SDK'
|
||||
applyTo: '**/*.ts, **/*.js, **/package.json'
|
||||
---
|
||||
|
||||
# TypeScript MCP Server Development
|
||||
|
||||
## Instructions
|
||||
|
||||
- Use the **@modelcontextprotocol/sdk** npm package: `npm install @modelcontextprotocol/sdk`
|
||||
- Import from specific paths: `@modelcontextprotocol/sdk/server/mcp.js`, `@modelcontextprotocol/sdk/server/stdio.js`, etc.
|
||||
- Use `McpServer` class for high-level server implementation with automatic protocol handling
|
||||
- Use `Server` class for low-level control with manual request handlers
|
||||
- Use **zod** for input/output schema validation: `npm install zod@3`
|
||||
- Always provide `title` field for tools, resources, and prompts for better UI display
|
||||
- Use `registerTool()`, `registerResource()`, and `registerPrompt()` methods (recommended over older APIs)
|
||||
- Define schemas using zod: `{ inputSchema: { param: z.string() }, outputSchema: { result: z.string() } }`
|
||||
- Return both `content` (for display) and `structuredContent` (for structured data) from tools
|
||||
- For HTTP servers, use `StreamableHTTPServerTransport` with Express or similar frameworks
|
||||
- For local integrations, use `StdioServerTransport` for stdio-based communication
|
||||
- Create new transport instances per request to prevent request ID collisions (stateless mode)
|
||||
- Use session management with `sessionIdGenerator` for stateful servers
|
||||
- Enable DNS rebinding protection for local servers: `enableDnsRebindingProtection: true`
|
||||
- Configure CORS headers and expose `Mcp-Session-Id` for browser-based clients
|
||||
- Use `ResourceTemplate` for dynamic resources with URI parameters: `new ResourceTemplate('resource://{param}', { list: undefined })`
|
||||
- Support completions for better UX using `completable()` wrapper from `@modelcontextprotocol/sdk/server/completable.js`
|
||||
- Implement sampling with `server.server.createMessage()` to request LLM completions from clients
|
||||
- Use `server.server.elicitInput()` to request additional user input during tool execution
|
||||
- Enable notification debouncing for bulk updates: `debouncedNotificationMethods: ['notifications/tools/list_changed']`
|
||||
- Dynamic updates: call `.enable()`, `.disable()`, `.update()`, or `.remove()` on registered items to emit `listChanged` notifications
|
||||
- Use `getDisplayName()` from `@modelcontextprotocol/sdk/shared/metadataUtils.js` for UI display names
|
||||
- Test servers with MCP Inspector: `npx @modelcontextprotocol/inspector`
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep tool implementations focused on single responsibilities
|
||||
- Provide clear, descriptive titles and descriptions for LLM understanding
|
||||
- Use proper TypeScript types for all parameters and return values
|
||||
- Implement comprehensive error handling with try-catch blocks
|
||||
- Return `isError: true` in tool results for error conditions
|
||||
- Use async/await for all asynchronous operations
|
||||
- Close database connections and clean up resources properly
|
||||
- Validate input parameters before processing
|
||||
- Use structured logging for debugging without polluting stdout/stderr
|
||||
- Consider security implications when exposing file system or network access
|
||||
- Implement proper resource cleanup on transport close events
|
||||
- Use environment variables for configuration (ports, API keys, etc.)
|
||||
- Document tool capabilities and limitations clearly
|
||||
- Test with multiple clients to ensure compatibility
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Basic Server Setup (HTTP)
|
||||
```typescript
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import express from 'express';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'my-server',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post('/mcp', async (req, res) => {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
});
|
||||
|
||||
res.on('close', () => transport.close());
|
||||
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
### Basic Server Setup (stdio)
|
||||
```typescript
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
const server = new McpServer({
|
||||
name: 'my-server',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
// ... register tools, resources, prompts ...
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
### Simple Tool
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
server.registerTool(
|
||||
'calculate',
|
||||
{
|
||||
title: 'Calculator',
|
||||
description: 'Perform basic calculations',
|
||||
inputSchema: { a: z.number(), b: z.number(), op: z.enum(['+', '-', '*', '/']) },
|
||||
outputSchema: { result: z.number() }
|
||||
},
|
||||
async ({ a, b, op }) => {
|
||||
const result = op === '+' ? a + b : op === '-' ? a - b :
|
||||
op === '*' ? a * b : a / b;
|
||||
const output = { result };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Dynamic Resource
|
||||
```typescript
|
||||
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
|
||||
server.registerResource(
|
||||
'user',
|
||||
new ResourceTemplate('users://{userId}', { list: undefined }),
|
||||
{
|
||||
title: 'User Profile',
|
||||
description: 'Fetch user profile data'
|
||||
},
|
||||
async (uri, { userId }) => ({
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
text: `User ${userId} data here`
|
||||
}]
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Tool with Sampling
|
||||
```typescript
|
||||
server.registerTool(
|
||||
'summarize',
|
||||
{
|
||||
title: 'Text Summarizer',
|
||||
description: 'Summarize text using LLM',
|
||||
inputSchema: { text: z.string() },
|
||||
outputSchema: { summary: z.string() }
|
||||
},
|
||||
async ({ text }) => {
|
||||
const response = await server.server.createMessage({
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: { type: 'text', text: `Summarize: ${text}` }
|
||||
}],
|
||||
maxTokens: 500
|
||||
});
|
||||
|
||||
const summary = response.content.type === 'text' ?
|
||||
response.content.text : 'Unable to summarize';
|
||||
const output = { summary };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Prompt with Completion
|
||||
```typescript
|
||||
import { completable } from '@modelcontextprotocol/sdk/server/completable.js';
|
||||
|
||||
server.registerPrompt(
|
||||
'review',
|
||||
{
|
||||
title: 'Code Review',
|
||||
description: 'Review code with specific focus',
|
||||
argsSchema: {
|
||||
language: completable(z.string(), value =>
|
||||
['typescript', 'python', 'javascript', 'java']
|
||||
.filter(l => l.startsWith(value))
|
||||
),
|
||||
code: z.string()
|
||||
}
|
||||
},
|
||||
({ language, code }) => ({
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Review this ${language} code:\n\n${code}`
|
||||
}
|
||||
}]
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```typescript
|
||||
server.registerTool(
|
||||
'risky-operation',
|
||||
{
|
||||
title: 'Risky Operation',
|
||||
description: 'An operation that might fail',
|
||||
inputSchema: { input: z.string() },
|
||||
outputSchema: { result: z.string() }
|
||||
},
|
||||
async ({ input }) => {
|
||||
try {
|
||||
const result = await performRiskyOperation(input);
|
||||
const output = { result };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
const error = err as Error;
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
37
.github/policies/resourceManagement.yml
vendored
37
.github/policies/resourceManagement.yml
vendored
@@ -163,7 +163,7 @@ configuration:
|
||||
association: Collaborator
|
||||
then:
|
||||
- addReply:
|
||||
reply: We've identified this issue as a duplicate of an existing one and are closing this thread so discussion stays in one place.<br/><br/>Please see the comment above for the link to the original tracking issue, and feel free to subscribe there for updates.
|
||||
reply: Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report!
|
||||
- closeIssue
|
||||
- removeLabel:
|
||||
label: Needs-Triage
|
||||
@@ -233,30 +233,6 @@ configuration:
|
||||
- addReply:
|
||||
reply: Hi! Thanks for making us aware of the problem. We raised the issue with our internal localization team. This issue should be fixed hopefully in the next version of PowerToys.
|
||||
description:
|
||||
- if:
|
||||
- payloadType: Issue_Comment
|
||||
- commentContains:
|
||||
pattern: '\/need-monitor-info'
|
||||
isRegex: True
|
||||
- hasLabel:
|
||||
label: Product-Cursor Wrap
|
||||
- or:
|
||||
- activitySenderHasAssociation:
|
||||
association: Owner
|
||||
- activitySenderHasAssociation:
|
||||
association: Member
|
||||
- activitySenderHasAssociation:
|
||||
association: Collaborator
|
||||
then:
|
||||
- removeLabel:
|
||||
label: Needs-Triage
|
||||
- removeLabel:
|
||||
label: Needs-Team-Response
|
||||
- addLabel:
|
||||
label: Needs-Author-Feedback
|
||||
- addReply:
|
||||
reply: "To help debug your layout, please run [this script](https://github.com/microsoft/PowerToys/blob/main/src/modules/MouseUtils/CursorWrap/CursorWrapTests/Capture-MonitorLayout.ps1) and attach the generated JSON output to this thread.\n\nThis allows us to better understand the issue and investigate potential fixes."
|
||||
description:
|
||||
- if:
|
||||
- payloadType: Issue_Comment
|
||||
- commentContains:
|
||||
@@ -266,5 +242,16 @@ configuration:
|
||||
- addReply:
|
||||
reply: Hi! Your last comment indicates to our system, that you might want to contribute to this feature/fix this bug. Thank you! Please make us aware on our ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769), as we don't see all the comments. <br /><br />_I'm a bot (beep!) so please excuse any mistakes I may make_
|
||||
description:
|
||||
- if:
|
||||
- payloadType: Issues
|
||||
- isAction:
|
||||
action: Opened
|
||||
- bodyContains:
|
||||
pattern: 'Area\(s\) with issue\?\s*\nWorkspaces'
|
||||
isRegex: True
|
||||
then:
|
||||
- addLabel:
|
||||
label: Product-Workspaces
|
||||
description:
|
||||
onFailure:
|
||||
onSuccess:
|
||||
|
||||
57
.github/prompts/create-commit-title.prompt.md
vendored
57
.github/prompts/create-commit-title.prompt.md
vendored
@@ -1,49 +1,16 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Generate an 80-character git commit title for the local diff'
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: 'Generate an 80-character git commit title for the local diff.'
|
||||
---
|
||||
|
||||
# Generate Commit Title
|
||||
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
|
||||
|
||||
## Purpose
|
||||
Provide a single-line, ready-to-paste git commit title (<= 80 characters) that reflects the most important local changes since `HEAD`.
|
||||
|
||||
## Input to collect
|
||||
- Run exactly one command to view the local diff:
|
||||
```@terminal
|
||||
git diff HEAD
|
||||
```
|
||||
|
||||
## How to decide the title
|
||||
1. From the diff, find the dominant area (e.g., `src/modules/*`, `doc/devdocs/**`) and the change type (bug fix, docs update, config tweak).
|
||||
2. Draft an imperative, plain-ASCII title that:
|
||||
- Mentions the primary component when obvious (e.g., `FancyZones:` or `Docs:`)
|
||||
- Stays within 80 characters and has no trailing punctuation
|
||||
|
||||
## Final output
|
||||
- Reply with only the commit title on a single line—no extra text.
|
||||
|
||||
## PR title convention (when asked)
|
||||
Use Conventional Commits style:
|
||||
|
||||
`<type>(<scope>): <summary>`
|
||||
|
||||
**Allowed types**
|
||||
- feat, fix, docs, refactor, perf, test, build, ci, chore
|
||||
|
||||
**Scope rules**
|
||||
- Use a short, PowerToys-focused scope (one word preferred). Common scopes:
|
||||
- Core: `runner`, `settings-ui`, `common`, `docs`, `build`, `ci`, `installer`, `gpo`, `dsc`
|
||||
- Modules: `fancyzones`, `powerrename`, `awake`, `colorpicker`, `imageresizer`, `keyboardmanager`, `mouseutils`, `peek`, `hosts`, `file-locksmith`, `screen-ruler`, `text-extractor`, `cropandlock`, `paste`, `powerlauncher`
|
||||
- If unclear, pick the closest module or subsystem; omit only if unavoidable
|
||||
|
||||
**Summary rules**
|
||||
- Imperative, present tense (“add”, “update”, “remove”, “fix”)
|
||||
- Keep it <= 72 characters when possible; be specific, avoid “misc changes”
|
||||
|
||||
**Examples**
|
||||
- `feat(fancyzones): add canvas template duplication`
|
||||
- `fix(mouseutils): guard crosshair toggle when dpi info missing`
|
||||
- `docs(runner): document tray icon states`
|
||||
- `build(installer): align wix v5 suffix flag`
|
||||
- `ci(ci): cache pipeline artifacts for x64`
|
||||
**Workflow:**
|
||||
1. Run a single command to view the local diff since the last commit:
|
||||
```@terminal
|
||||
git diff HEAD
|
||||
```
|
||||
2. From that diff, identify the dominant area (reference key paths like `src/modules/*`, `doc/devdocs/**`, etc.), the type of change (bug fix, docs update, config tweak), and any notable impact.
|
||||
3. Draft a concise, imperative commit title summarizing the dominant change. Keep it plain ASCII, <= 80 characters, and avoid trailing punctuation. Mention the primary component when obvious (for example `FancyZones:` or `Docs:`).
|
||||
4. Respond with only the final commit title on a single line so it can be pasted directly into `git commit`.
|
||||
|
||||
8
.github/prompts/create-pr-summary.prompt.md
vendored
8
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -1,10 +1,9 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff'
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff.'
|
||||
---
|
||||
|
||||
# Generate PR Summary
|
||||
|
||||
**Goal:** Produce a ready-to-paste PR title and description that follows PowerToys conventions by comparing the current branch against a user-selected target branch.
|
||||
|
||||
**Repo guardrails:**
|
||||
@@ -21,4 +20,3 @@ description: 'Generate a PowerToys-ready pull request description from the local
|
||||
5. Confirm validation: list tests executed with results or state why tests were skipped in line with repo guidance.
|
||||
6. Load `.github/pull_request_template.md`, mirror its section order, and populate it with the gathered facts. Include only relevant checklist entries, marking them `[x]/[ ]` and noting any intentional omissions as "N/A".
|
||||
7. Present the filled template inside a fenced ```markdown code block with no extra commentary so it is ready to paste into a PR, clearly flagging any placeholders that still need user input.
|
||||
8. Prepend the PR title above the filled template, applying the Conventional Commit type/scope rules from `.github/prompts/create-commit-title.prompt.md`; pick the dominant component from the diff and keep the title concise and imperative.
|
||||
|
||||
9
.github/prompts/fix-issue.prompt.md
vendored
9
.github/prompts/fix-issue.prompt.md
vendored
@@ -1,11 +1,10 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Execute the fix for a GitHub issue using the previously generated implementation plan'
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: " Execute the fix for a GitHub issue using the previously generated implementation plan. Apply code & tests directly in the repo. Output only a PR description (and optional manual steps)."
|
||||
---
|
||||
|
||||
# Fix GitHub Issue
|
||||
|
||||
## Dependencies
|
||||
# DEPENDENCY
|
||||
Source review prompt (for generating the implementation plan if missing):
|
||||
- .github/prompts/review-issue.prompt.md
|
||||
|
||||
|
||||
70
.github/prompts/fix-pr-active-comments.prompt.md
vendored
70
.github/prompts/fix-pr-active-comments.prompt.md
vendored
@@ -1,70 +0,0 @@
|
||||
---
|
||||
description: 'Fix active pull request comments with scoped changes'
|
||||
name: 'fix-pr-active-comments'
|
||||
agent: 'agent'
|
||||
argument-hint: 'PR number or active PR URL'
|
||||
---
|
||||
|
||||
# Fix Active PR Comments
|
||||
|
||||
## Mission
|
||||
Resolve active pull request comments by applying only simple fixes. For complex refactors, write a plan instead of changing code.
|
||||
|
||||
## Scope & Preconditions
|
||||
- You must have an active pull request context or a provided PR number.
|
||||
- Only implement simple changes. Do not implement large refactors.
|
||||
- If required context is missing, request it and stop.
|
||||
|
||||
## Inputs
|
||||
- Required: ${input:pr_number:PR number or URL}
|
||||
- Optional: ${input:comment_scope:files or areas to focus on}
|
||||
- Optional: ${input:fixing_guidelines:additional fixing guidelines from the user}
|
||||
|
||||
## Workflow
|
||||
1. Locate all active (unresolved) PR review comments for the given PR.
|
||||
2. For each comment, classify the change scope:
|
||||
- Simple change: limited edits, localized fix, low risk, no broad redesign.
|
||||
- Large refactor: multi-file redesign, architecture change, or risky behavior change.
|
||||
3. For each large refactor request:
|
||||
- Do not modify code.
|
||||
- Write a planning document to Generated Files/prReview/${input:pr_number}/fixPlan/.
|
||||
4. For each simple change request:
|
||||
- Implement the fix with minimal edits.
|
||||
- Run quick checks if needed.
|
||||
- Commit and push the change.
|
||||
5. For comments that seem invalid, unclear, or not applicable (even if simple):
|
||||
- Do not change code.
|
||||
- Add the item to a summary table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
|
||||
- Consult back to the end user in a friendly, polite tone.
|
||||
6. Respond to each comment that you fixed:
|
||||
- Reply in the active conversation.
|
||||
- Use a polite or friendly tone.
|
||||
- Keep the response under 200 words.
|
||||
- Resolve the comment after replying.
|
||||
|
||||
## Output Expectations
|
||||
- Simple fixes: code changes committed and pushed.
|
||||
- Large refactors: a plan file saved to Generated Files/prReview/${input:pr_number}/fixPlan/.
|
||||
- Invalid or unclear comments: captured in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
|
||||
- Each fixed comment has a reply under 200 words and is resolved.
|
||||
|
||||
## Plan File Template
|
||||
Use this template for each large refactor item:
|
||||
|
||||
# Fix Plan: <short title>
|
||||
|
||||
## Context
|
||||
- Comment link:
|
||||
- Impacted areas:
|
||||
|
||||
## Overview Table Template
|
||||
Use this table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md:
|
||||
|
||||
| Comment link | Summary | Reason not applied | Suggested follow-up |
|
||||
| --- | --- | --- | --- |
|
||||
| | | | |
|
||||
|
||||
## Quality Assurance
|
||||
- Verify plan file path exists.
|
||||
- Ensure no code changes were made for large refactor items.
|
||||
- Confirm replies are under 200 words and comments are resolved.
|
||||
16
.github/prompts/fix-spelling.prompt.md
vendored
16
.github/prompts/fix-spelling.prompt.md
vendored
@@ -1,16 +1,15 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
mode: 'agent'
|
||||
model: GPT-5-Codex (Preview)
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR.'
|
||||
---
|
||||
|
||||
# Fix Spelling Comments
|
||||
|
||||
**Goal:** Clear every outstanding GitHub pull request comment created by the `Code scanning / check-spelling` workflow by explicitly allowing intentional terms.
|
||||
|
||||
**Guardrails:**
|
||||
- Update only discussion threads authored by `github-actions` or `github-actions[bot]` that mention `Code scanning results / check-spelling`.
|
||||
- Prefer improving the wording in the originally flagged file when it clarifies intent without changing meaning; if the wording is already clear/standard for the context, handle it via `.github/actions/spell-check/expect.txt` and reuse existing entries.
|
||||
- Limit edits to the flagged text and `.github/actions/spell-check/expect.txt`; leave all other files and topics untouched.
|
||||
- Resolve findings solely by editing `.github/actions/spell-check/expect.txt`; reuse existing entries.
|
||||
- Leave all other files and topics untouched.
|
||||
|
||||
**Prerequisites:**
|
||||
- Install GitHub CLI if it is not present: `winget install GitHub.cli`.
|
||||
@@ -19,6 +18,5 @@ description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
**Workflow:**
|
||||
1. Determine the active pull request with a single `gh pr view --json number` call (default to the current branch).
|
||||
2. Fetch all PR discussion data once via `gh pr view --json comments,reviews` and filter to check-spelling comments authored by `github-actions` or `github-actions[bot]` that are not minimized; when several remain, process only the most recent comment body.
|
||||
3. For each flagged token, first consider tightening or rephrasing the original text to avoid the false positive while keeping the meaning intact; if the existing wording is already normal and professional for the context, proceed to allowlisting instead of changing it.
|
||||
4. When allowlisting, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
|
||||
5. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.
|
||||
3. For each flagged token, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
|
||||
4. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.
|
||||
21
.github/prompts/review-issue.prompt.md
vendored
21
.github/prompts/review-issue.prompt.md
vendored
@@ -1,27 +1,20 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Review a GitHub issue, score it (0-100), and generate an implementation plan'
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: "You are github issue review and planning expertise, Score (0–100) and write one Implementation Plan. Outputs: overview.md, implementation-plan.md."
|
||||
---
|
||||
|
||||
# Review GitHub Issue
|
||||
|
||||
## Goal
|
||||
# GOAL
|
||||
For **#{{issue_number}}** produce:
|
||||
1) `Generated Files/issueReview/{{issue_number}}/overview.md`
|
||||
2) `Generated Files/issueReview/{{issue_number}}/implementation-plan.md`
|
||||
|
||||
## Inputs
|
||||
Figure out required inputs {{issue_number}} from the invocation context; if anything is missing, ask for the value or note it as a gap.
|
||||
figure out from the prompt on the
|
||||
|
||||
# CONTEXT (brief)
|
||||
Ground evidence using `gh issue view {{issue_number}} --json number,title,body,author,createdAt,updatedAt,state,labels,milestone,reactions,comments,linkedPullRequests`, download images via MCP `github_issue_images` to better understand the issue context. Finally, use MCP `github_issue_attachments` to download logs with parameter `extractFolder` as `Generated Files/issueReview/{{issue_number}}/logs`, and analyze the downloaded logs if available to identify relevant issues. Locate the source code in the current workspace (use `rg`/`git grep` as needed). Link related issues and PRs.
|
||||
|
||||
## When to call MCP tools
|
||||
If the following MCP "github-artifacts" tools are available in the environment, use them:
|
||||
- `github_issue_images`: use when the issue/PR likely contains screenshots or other visual evidence (UI bugs, glitches, design problems).
|
||||
- `github_issue_attachments`: use when the issue/PR mentions attached ZIPs (PowerToysReport_*.zip, logs.zip, debug.zip) or asks to analyze logs/diagnostics. Always provide `extractFolder` as `Generated Files/issueReview/{{issue_number}}/logs`
|
||||
|
||||
If these tools are not available (not listed by the runtime), start the MCP server "github-artifacts" first.
|
||||
Ground evidence using `gh issue view {{issue_number}} --json number,title,body,author,createdAt,updatedAt,state,labels,milestone,reactions,comments,linkedPullRequests`, and download the image for understand the context of the issue more.
|
||||
Locate source code in current workspace, but also free feel to use via `rg`/`git grep`. Link related issues/PRs.
|
||||
|
||||
# OVERVIEW.MD
|
||||
## Summary
|
||||
|
||||
7
.github/prompts/review-pr.prompt.md
vendored
7
.github/prompts/review-pr.prompt.md
vendored
@@ -1,9 +1,10 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
description: 'Perform a comprehensive PR review with per-step Markdown and machine-readable outputs'
|
||||
mode: 'agent'
|
||||
model: Claude Sonnet 4.5
|
||||
description: "gh-driven PR review; per-step Markdown + machine-readable outputs"
|
||||
---
|
||||
|
||||
# Review Pull Request
|
||||
# PR Review — gh + stepwise
|
||||
|
||||
**Goal**: Given `{{pr_number}}`, run a *one-topic-per-step* review. Write files to `Generated Files/prReview/{{pr_number}}/` (replace `{{pr_number}}` with the integer). Emit machine‑readable blocks for a GitHub MCP to post review comments.
|
||||
|
||||
|
||||
377
.github/scripts/telemetry-pr-check.js
vendored
377
.github/scripts/telemetry-pr-check.js
vendored
@@ -1,377 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Detects telemetry-event additions/modifications in a pull request and
|
||||
* posts (or updates) a PR comment when telemetry-related changes are found.
|
||||
*
|
||||
* This script is executed by .github/workflows/telemetry-pr-check.yml.
|
||||
* Keep both files aligned when changing trigger behavior, env usage, or messaging.
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const REVIEWER_LOGIN = 'chatasweetie';
|
||||
const REVIEWER_MENTION = `@${REVIEWER_LOGIN}`;
|
||||
|
||||
const COMMENT_MARKER = '<!-- telemetry-event-check -->';
|
||||
const COMMENT_BODY_WITH_PRIVACY_UPDATE = `${COMMENT_MARKER}
|
||||
Thank you for contributing to PowerToys. We've detected that this PR might include a new or modified telemetry event. After this PR is merged, please follow these next steps:
|
||||
|
||||
- [ ] Reach out to Jessica (${REVIEWER_MENTION}) to follow up on the next steps: https://aka.ms/pt-telemetry-process
|
||||
`;
|
||||
|
||||
const COMMENT_BODY_WITHOUT_PRIVACY_UPDATE = `${COMMENT_MARKER}
|
||||
Thank you for contributing to PowerToys. We've detected that this PR might include a new or modified telemetry event. Please ensure the following before merging:
|
||||
|
||||
- [ ] Add your telemetry events to [DATA_AND_PRIVACY](https://github.com/microsoft/PowerToys/blob/main/DATA_AND_PRIVACY.md).md within this PR.
|
||||
|
||||
- [ ] Reach out to Jessica (${REVIEWER_MENTION}) to follow up on the next steps: https://aka.ms/pt-telemetry-process`;
|
||||
|
||||
const TELEMETRY_PATH_PATTERNS = [
|
||||
/(^|\/)trace\.(h|hpp|cpp|cs)$/i,
|
||||
/(^|\/)telemetry\//i,
|
||||
/(^|\/)events\/.+event\.cs$/i,
|
||||
/^src\/common\/Telemetry\//i,
|
||||
/^src\/common\/ManagedTelemetry\//i,
|
||||
/^src\/runner\/trace\.(h|cpp)$/i,
|
||||
/^src\/settings-ui\/.+\/Telemetry\//i,
|
||||
];
|
||||
|
||||
const TELEMETRY_LINE_PATTERNS = [
|
||||
/TraceLoggingWriteWrapper\s*\(/,
|
||||
/\bTraceLoggingWrite\s*\(/,
|
||||
/\bTRACELOGGING_DEFINE_PROVIDER\b/,
|
||||
/\bTraceLoggingOptionProjectTelemetry\b/,
|
||||
/\bProjectTelemetryPrivacyDataTag\b/,
|
||||
/\bPROJECT_KEYWORD_MEASURE\b/,
|
||||
/\bRegisterProvider\s*\(/,
|
||||
/\bUnregisterProvider\s*\(/,
|
||||
/\bPowerToysTelemetry\.Log\.WriteEvent\s*\(/,
|
||||
/\bclass\s+\w+\s*:\s*EventBase\s*,\s*IEvent\b/,
|
||||
/\bclass\s+\w+\s*:\s*TelemetryBase\b/,
|
||||
/\bPartA_PrivTags\b/,
|
||||
/\[EventData\]/,
|
||||
/\bEventName\b/,
|
||||
];
|
||||
|
||||
function requireEnv(name) {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function validateRepository(repository) {
|
||||
if (!/^[^/]+\/[^/]+$/.test(repository)) {
|
||||
throw new Error(
|
||||
`GITHUB_REPOSITORY must be in owner/repo format, received: ${JSON.stringify(repository)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function readEventPayload(eventPath) {
|
||||
let raw;
|
||||
try {
|
||||
raw = fs.readFileSync(eventPath, 'utf8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read event payload at ${eventPath}: ${error.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse JSON from ${eventPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePullNumber(event) {
|
||||
const fromPullRequest = event?.pull_request?.number;
|
||||
const fromWorkflowDispatch = event?.inputs?.pr_number;
|
||||
const rawPullNumber = fromPullRequest ?? fromWorkflowDispatch;
|
||||
|
||||
if (rawPullNumber === undefined || rawPullNumber === null || rawPullNumber === '') {
|
||||
throw new Error(
|
||||
'Unable to determine pull request number from event payload. Expected pull_request.number or inputs.pr_number.'
|
||||
);
|
||||
}
|
||||
|
||||
const pullNumber = Number.parseInt(String(rawPullNumber), 10);
|
||||
if (!Number.isInteger(pullNumber) || pullNumber <= 0) {
|
||||
throw new Error(`Invalid pull request number: ${JSON.stringify(rawPullNumber)}`);
|
||||
}
|
||||
|
||||
return pullNumber;
|
||||
}
|
||||
|
||||
function isTelemetryPath(filePath) {
|
||||
return TELEMETRY_PATH_PATTERNS.some((pattern) => pattern.test(filePath));
|
||||
}
|
||||
|
||||
function changedLinesFromPatch(patch) {
|
||||
if (!patch) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return patch
|
||||
.split('\n')
|
||||
.filter((line) => {
|
||||
if (line.startsWith('+++') || line.startsWith('---')) {
|
||||
return false;
|
||||
}
|
||||
return line.startsWith('+') || line.startsWith('-');
|
||||
})
|
||||
.map((line) => line.slice(1));
|
||||
}
|
||||
|
||||
function hasTelemetryLineSignal(lines) {
|
||||
return lines.some((line) => TELEMETRY_LINE_PATTERNS.some((pattern) => pattern.test(line)));
|
||||
}
|
||||
|
||||
async function apiRequest(url, method = 'GET', body) {
|
||||
const token = requireEnv('GITHUB_TOKEN');
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Network error during ${method} ${url}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
||||
const rateLimitHint =
|
||||
response.status === 403 && rateLimitReset
|
||||
? ` (rate limit reset at epoch ${rateLimitReset})`
|
||||
: '';
|
||||
throw new Error(`${method} ${url} failed (${response.status})${rateLimitHint}: ${text}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse JSON response for ${method} ${url}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAllPullFiles(apiBaseUrl, repository, pullNumber) {
|
||||
const files = [];
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
const url = `${apiBaseUrl}/repos/${repository}/pulls/${pullNumber}/files?per_page=100&page=${page}`;
|
||||
const batch = await apiRequest(url);
|
||||
if (!Array.isArray(batch)) {
|
||||
throw new Error(`Unexpected response while listing PR files on page ${page}.`);
|
||||
}
|
||||
|
||||
if (batch.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
files.push(...batch);
|
||||
|
||||
if (batch.length < 100) {
|
||||
break;
|
||||
}
|
||||
|
||||
page += 1;
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function getPullRequest(apiBaseUrl, repository, pullNumber) {
|
||||
const url = `${apiBaseUrl}/repos/${repository}/pulls/${pullNumber}`;
|
||||
const pullRequest = await apiRequest(url);
|
||||
if (!pullRequest || typeof pullRequest !== 'object') {
|
||||
throw new Error('Unexpected response while fetching pull request details.');
|
||||
}
|
||||
return pullRequest;
|
||||
}
|
||||
|
||||
async function ensureReviewerRequested(apiBaseUrl, repository, pullNumber, pullRequest) {
|
||||
const authorLogin = String(pullRequest?.user?.login || '').toLowerCase();
|
||||
const targetReviewer = REVIEWER_LOGIN.toLowerCase();
|
||||
|
||||
if (authorLogin === targetReviewer) {
|
||||
console.log(`Skipping reviewer request: ${REVIEWER_LOGIN} is the PR author.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestedReviewers = Array.isArray(pullRequest?.requested_reviewers)
|
||||
? pullRequest.requested_reviewers
|
||||
: [];
|
||||
const alreadyRequested = requestedReviewers.some(
|
||||
(reviewer) => String(reviewer?.login || '').toLowerCase() === targetReviewer
|
||||
);
|
||||
|
||||
if (alreadyRequested) {
|
||||
console.log(`Reviewer ${REVIEWER_LOGIN} is already requested.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${apiBaseUrl}/repos/${repository}/pulls/${pullNumber}/requested_reviewers`;
|
||||
try {
|
||||
await apiRequest(url, 'POST', { reviewers: [REVIEWER_LOGIN] });
|
||||
console.log(`Requested reviewer ${REVIEWER_LOGIN}.`);
|
||||
} catch (error) {
|
||||
// Reviewer request should not fail the telemetry guidance workflow.
|
||||
console.warn(
|
||||
`Unable to request reviewer ${REVIEWER_LOGIN}: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function findExistingTelemetryComment(apiBaseUrl, repository, pullNumber) {
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
const commentsUrl = `${apiBaseUrl}/repos/${repository}/issues/${pullNumber}/comments?per_page=100&page=${page}`;
|
||||
const comments = await apiRequest(commentsUrl);
|
||||
|
||||
if (!Array.isArray(comments)) {
|
||||
throw new Error(`Unexpected response while listing issue comments on page ${page}.`);
|
||||
}
|
||||
|
||||
const existing = comments.find(
|
||||
(comment) => typeof comment.body === 'string' && comment.body.includes(COMMENT_MARKER)
|
||||
);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (comments.length < 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
page += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function detectTelemetryChanges(files) {
|
||||
const matches = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filename = file.filename || '';
|
||||
const telemetryPath = isTelemetryPath(filename);
|
||||
const changedLines = changedLinesFromPatch(file.patch);
|
||||
const telemetryLineSignal = hasTelemetryLineSignal(changedLines);
|
||||
|
||||
// Some large diffs omit patch content. If the file path is telemetry-centric,
|
||||
// treat it as a telemetry modification to avoid false negatives.
|
||||
const patchUnavailable = !file.patch && telemetryPath;
|
||||
|
||||
if (telemetryPath || telemetryLineSignal || patchUnavailable) {
|
||||
matches.push({
|
||||
filename,
|
||||
telemetryPath,
|
||||
telemetryLineSignal,
|
||||
patchUnavailable,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function hasDataAndPrivacyChange(files) {
|
||||
return files.some((file) => {
|
||||
const filename = (file.filename || '').toLowerCase();
|
||||
return filename === 'data_and_privacy.md';
|
||||
});
|
||||
}
|
||||
|
||||
async function upsertPrComment(apiBaseUrl, repository, pullNumber, body) {
|
||||
const existing = await findExistingTelemetryComment(apiBaseUrl, repository, pullNumber);
|
||||
|
||||
if (existing) {
|
||||
const updateUrl = `${apiBaseUrl}/repos/${repository}/issues/comments/${existing.id}`;
|
||||
await apiRequest(updateUrl, 'PATCH', { body });
|
||||
console.log(`Updated existing telemetry comment (id: ${existing.id}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const createUrl = `${apiBaseUrl}/repos/${repository}/issues/${pullNumber}/comments`;
|
||||
await apiRequest(createUrl, 'POST', { body });
|
||||
console.log('Created telemetry comment on PR.');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const eventPath = requireEnv('GITHUB_EVENT_PATH');
|
||||
const repository = requireEnv('GITHUB_REPOSITORY');
|
||||
const apiBaseUrl = process.env.GITHUB_API_URL || 'https://api.github.com';
|
||||
validateRepository(repository);
|
||||
|
||||
let parsedApiBaseUrl;
|
||||
try {
|
||||
parsedApiBaseUrl = new URL(apiBaseUrl);
|
||||
} catch {
|
||||
throw new Error(`Invalid GITHUB_API_URL: ${JSON.stringify(apiBaseUrl)}`);
|
||||
}
|
||||
|
||||
const event = readEventPayload(eventPath);
|
||||
const pullNumber = resolvePullNumber(event);
|
||||
|
||||
console.log(`Event name: ${process.env.GITHUB_EVENT_NAME || 'unknown'}`);
|
||||
console.log(`Repository: ${repository}`);
|
||||
console.log(`PR number: ${pullNumber}`);
|
||||
|
||||
const files = await getAllPullFiles(parsedApiBaseUrl.origin, repository, pullNumber);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No changed files found for PR; skipping telemetry comment update.');
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = detectTelemetryChanges(files);
|
||||
const dataAndPrivacyChanged = hasDataAndPrivacyChange(files);
|
||||
|
||||
console.log(`Scanned ${files.length} changed files.`);
|
||||
console.log(`Telemetry matches found: ${matches.length}.`);
|
||||
console.log(`DATA_AND_PRIVACY.md changed: ${dataAndPrivacyChanged}.`);
|
||||
|
||||
if (matches.length === 0) {
|
||||
console.log('No telemetry-related additions/modifications detected.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
console.log(
|
||||
`- ${match.filename} (telemetryPath=${match.telemetryPath}, telemetryLineSignal=${match.telemetryLineSignal}, patchUnavailable=${match.patchUnavailable})`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const pullRequest = await getPullRequest(parsedApiBaseUrl.origin, repository, pullNumber);
|
||||
await ensureReviewerRequested(parsedApiBaseUrl.origin, repository, pullNumber, pullRequest);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Failed to fetch PR details or request reviewer; continuing to post telemetry guidance comment.'
|
||||
);
|
||||
console.warn(error instanceof Error ? error.stack || error.message : error);
|
||||
}
|
||||
|
||||
const commentBody = dataAndPrivacyChanged
|
||||
? COMMENT_BODY_WITH_PRIVACY_UPDATE
|
||||
: COMMENT_BODY_WITHOUT_PRIVACY_UPDATE;
|
||||
|
||||
await upsertPrComment(apiBaseUrl, repository, pullNumber, commentBody);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Telemetry PR check failed.');
|
||||
console.error(error instanceof Error ? error.stack || error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
152
.github/skills/changelog-generator/SKILL.md
vendored
Normal file
152
.github/skills/changelog-generator/SKILL.md
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
name: changelog-generator
|
||||
description: Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
|
||||
---
|
||||
|
||||
# Changelog Generator
|
||||
|
||||
This skill transforms technical git commits into polished, user-friendly changelogs that your customers and users will actually understand and appreciate.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Preparing release notes for a new version
|
||||
- Generating changelog between two tags/commits
|
||||
- Creating draft release notes from recent commits
|
||||
|
||||
## Quick Start
|
||||
|
||||
```
|
||||
Create a changelog from commits since v0.96.1
|
||||
```
|
||||
|
||||
```
|
||||
Create release notes for version 0.97.0 starting from tag v0.96.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 1. Fetch │───▶│ 2. Filter │───▶│ 3. Categorize│───▶│ 4. Generate │
|
||||
│ Commits │ │ & Dedupe │ │ by Module │ │ Descriptions │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
📄 github-api 📄 commit- 📄 module- 📄 user-facing-
|
||||
reference.md filtering.md mapping.md description.md
|
||||
```
|
||||
|
||||
## Sub-Skills Reference
|
||||
|
||||
This skill is composed of the following sub-skills.
|
||||
|
||||
**⚠️ IMPORTANT: Do NOT read all sub-skills at once!**
|
||||
- Only read a sub-skill when you reach that step in the workflow
|
||||
- This saves context window and improves accuracy
|
||||
- Use `read_file` to load a sub-skill only when needed
|
||||
|
||||
| Step | Sub-Skill | When to Read |
|
||||
|------|-----------|--------------|
|
||||
| Fetch data | [github-api-reference.md](sub-skills/github-api-reference.md) | When fetching commits/PRs |
|
||||
| Filter commits | [commit-filtering.md](sub-skills/commit-filtering.md) | When checking if commit should be skipped |
|
||||
| Categorize | [module-mapping.md](sub-skills/module-mapping.md) | When determining which module a PR belongs to |
|
||||
| Generate text | [user-facing-description.md](sub-skills/user-facing-description.md) | When writing the changelog entry text |
|
||||
| Attribution | [contributor-attribution.md](sub-skills/contributor-attribution.md) | When checking if author needs thanks |
|
||||
| Large releases | [progress-tracking.md](sub-skills/progress-tracking.md) | Only if processing 50+ commits |
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Summary
|
||||
|
||||
### Step 1: Get Commit Range
|
||||
```powershell
|
||||
# Count commits between tags
|
||||
gh api repos/microsoft/PowerToys/compare/v0.96.0...v0.96.1 --jq '.commits | length'
|
||||
```
|
||||
👉 Details: [github-api-reference.md](sub-skills/github-api-reference.md)
|
||||
|
||||
### Step 2: Filter Commits
|
||||
- Skip commits already in start tag
|
||||
- Skip cherry-picks and backports
|
||||
- Deduplicate by PR number
|
||||
|
||||
👉 Details: [commit-filtering.md](sub-skills/commit-filtering.md)
|
||||
|
||||
### Step 3: For Each PR, Generate Entry
|
||||
1. Get PR title, body, files, labels
|
||||
2. Determine module from file paths or labels
|
||||
3. Check if user-facing (skip internal changes)
|
||||
4. Transform to user-friendly description
|
||||
5. Add contributor attribution if needed
|
||||
|
||||
👉 Details: [user-facing-description.md](sub-skills/user-facing-description.md), [module-mapping.md](sub-skills/module-mapping.md), [contributor-attribution.md](sub-skills/contributor-attribution.md)
|
||||
|
||||
### Step 4: Checkpoint (if 50+ commits)
|
||||
- Save progress after every 15-20 commits
|
||||
- Track processed PRs for deduplication
|
||||
- Enable resume from interruption
|
||||
|
||||
👉 Details: [progress-tracking.md](sub-skills/progress-tracking.md)
|
||||
|
||||
### Step 5: Format Output
|
||||
```markdown
|
||||
## ✨ What's new
|
||||
**Version X.XX (Month Year)**
|
||||
|
||||
**✨ Highlights**
|
||||
- [Most impactful change 1]
|
||||
- [Most impactful change 2]
|
||||
|
||||
### [Module Name - Alphabetical]
|
||||
- [Description]. Thanks [@contributor](https://github.com/contributor)!
|
||||
|
||||
### Development
|
||||
- [Internal changes]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Output
|
||||
|
||||
```markdown
|
||||
## ✨ What's new
|
||||
**Version 0.96.1 (December 2025)**
|
||||
|
||||
**✨ Highlights**
|
||||
- Advanced Paste now supports multiple AI providers.
|
||||
- PowerRename can extract photo metadata for renaming.
|
||||
|
||||
### Advanced Paste
|
||||
- Added support for Azure OpenAI, Google Gemini, Mistral, and more.
|
||||
|
||||
### Awake
|
||||
- The countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Development
|
||||
- Resolved build warnings in Command Palette projects.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Propose highlights** after all entries are generated
|
||||
2. **Check PR body** when title is unclear
|
||||
3. **Thank external contributors** - see [contributor-attribution.md](sub-skills/contributor-attribution.md)
|
||||
4. **Use progress tracking** for large releases - see [progress-tracking.md](sub-skills/progress-tracking.md)
|
||||
5. **Save output** to `release-change-note-draft.md`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Lost progress mid-generation | Read `release-notes-progress.md` to resume |
|
||||
| Duplicate entries | Check processed PR list, dedupe by PR number |
|
||||
| Commit already released | Use `git merge-base --is-ancestor` to verify |
|
||||
| API rate limited | Check `gh api rate_limit`, wait or use token |
|
||||
|
||||
👉 See sub-skills for detailed troubleshooting.
|
||||
107
.github/skills/changelog-generator/sub-skills/commit-filtering.md
vendored
Normal file
107
.github/skills/changelog-generator/sub-skills/commit-filtering.md
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
# Commit Filtering Rules
|
||||
|
||||
This sub-skill defines rules for filtering commits to include in the changelog.
|
||||
|
||||
## Understanding the Branch Model
|
||||
|
||||
PowerToys uses a release branch model where fixes are cherry-picked from main:
|
||||
|
||||
```
|
||||
main: A---B---C---D---E---F---G---H (HEAD)
|
||||
\
|
||||
release: X---Y---Z (v0.96.1 tag)
|
||||
↑
|
||||
(X, Y are cherry-picks of C, E from main)
|
||||
```
|
||||
|
||||
**Key insight:** When comparing `v0.96.1...main`:
|
||||
- The release tag (v0.96.1) is on a **different branch** than main
|
||||
- GitHub compare finds the merge-base and returns commits on main after that point
|
||||
- Commits C and E appear in the results even though they were cherry-picked to release as X and Y
|
||||
- **The SHAs are different**, so SHA-based filtering won't work!
|
||||
|
||||
## ⚠️ CRITICAL: Filter by PR Number, Not SHA
|
||||
|
||||
Since cherry-picks have different SHAs, you **MUST** check by PR number:
|
||||
|
||||
```powershell
|
||||
# Extract PR number from commit message and check if it exists in the release tag
|
||||
$prNumber = "43785"
|
||||
$startTag = "v0.96.1"
|
||||
|
||||
# Search the release branch for this PR number in commit messages
|
||||
$cherryPicked = git log $startTag --oneline --grep="#$prNumber"
|
||||
if ($cherryPicked) {
|
||||
Write-Host "SKIP: PR #$prNumber was cherry-picked to $startTag"
|
||||
} else {
|
||||
Write-Host "INCLUDE: PR #$prNumber is new since $startTag"
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Filtering Workflow
|
||||
|
||||
```powershell
|
||||
$startTag = "v0.96.1" # Release tag (on release branch)
|
||||
$endRef = "main" # Target (main branch)
|
||||
|
||||
# Step 1: Get all commits from main since the merge-base with release tag
|
||||
$commits = gh api "repos/microsoft/PowerToys/compare/$startTag...$endRef" `
|
||||
--jq '.commits[] | {sha: .sha, message: .commit.message}' | ConvertFrom-Json
|
||||
|
||||
# Step 2: Build list of PR numbers already in the release tag
|
||||
$releasePRs = git log $startTag --oneline | Select-String -Pattern '#(\d+)' -AllMatches |
|
||||
ForEach-Object { $_.Matches.Groups[1].Value } | Sort-Object -Unique
|
||||
|
||||
Write-Host "PRs already in $startTag : $($releasePRs.Count)"
|
||||
|
||||
# Step 3: Filter commits - skip if PR was cherry-picked to release
|
||||
$newCommits = @()
|
||||
foreach ($commit in $commits) {
|
||||
if ($commit.message -match '#(\d+)') {
|
||||
$prNumber = $matches[1]
|
||||
if ($releasePRs -contains $prNumber) {
|
||||
Write-Host "SKIP: PR #$prNumber already in $startTag (cherry-picked)"
|
||||
continue
|
||||
}
|
||||
}
|
||||
$newCommits += $commit
|
||||
}
|
||||
|
||||
Write-Host "New commits to process: $($newCommits.Count)"
|
||||
```
|
||||
|
||||
## Why SHA-Based Methods Don't Work Here
|
||||
|
||||
| Method | Works for same branch? | Works for cross-branch (cherry-picks)? |
|
||||
|--------|------------------------|----------------------------------------|
|
||||
| `git merge-base --is-ancestor` | ✅ Yes | ❌ No - different SHAs |
|
||||
| `git tag --contains` | ✅ Yes | ❌ No - tag is on different branch |
|
||||
| GitHub Compare API | ✅ Yes | ❌ No - returns commits by SHA |
|
||||
| **PR number matching** | ✅ Yes | ✅ **Yes** |
|
||||
|
||||
## Skip Rules Summary
|
||||
|
||||
| Priority | Condition | Action |
|
||||
|----------|-----------|--------|
|
||||
| 1 | PR number found in `git log $startTag --grep="#$prNumber"` | **SKIP** - cherry-picked |
|
||||
| 2 | Same PR number already processed in this run | **SKIP** - duplicate |
|
||||
| 3 | Bot author (dependabot, etc.) | **SKIP** - unless user-visible |
|
||||
| 4 | Internal-only change (CI, tests, refactor) | Move to **Development** section |
|
||||
|
||||
## User-Facing vs Non-User-Facing
|
||||
|
||||
**Include in changelog:**
|
||||
- New features and capabilities
|
||||
- Bug fixes that affect users
|
||||
- UI/UX improvements
|
||||
- Performance improvements users would notice
|
||||
- Breaking changes or behavior modifications
|
||||
- Security fixes
|
||||
|
||||
**Exclude from changelog (put in Development section):**
|
||||
- Internal refactoring
|
||||
- CI/CD changes
|
||||
- Code style fixes
|
||||
- Test additions/modifications
|
||||
- Documentation-only changes
|
||||
- Dependency updates (unless user-visible impact)
|
||||
110
.github/skills/changelog-generator/sub-skills/contributor-attribution.md
vendored
Normal file
110
.github/skills/changelog-generator/sub-skills/contributor-attribution.md
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
# Contributor Attribution Rules
|
||||
|
||||
This sub-skill defines when and how to credit contributors in changelog entries.
|
||||
|
||||
## Attribution Rules
|
||||
|
||||
- Check [COMMUNITY.md](../../../COMMUNITY.md) for the "PowerToys core team" section
|
||||
- If author is **NOT** in core team → Add `Thanks [@username](https://github.com/username)!`
|
||||
- If author **IS** in core team → No attribution needed
|
||||
- Microsoft employees working on PowerToys as their job → No attribution needed
|
||||
|
||||
## Core Team Members
|
||||
|
||||
Current core team members (from COMMUNITY.md) - do NOT thank these:
|
||||
|
||||
```
|
||||
@craigloewen-msft, @niels9001, @dhowett, @yeelam-gordon, @jamrobot,
|
||||
@lei9444, @shuaiyuanxx, @moooyo, @haoliuu, @chenmy77, @chemwolf6922,
|
||||
@yaqingmi, @zhaoqpcn, @urnotdfs, @zhaopy536, @wang563681252, @vanzue,
|
||||
@zadjii-msft, @khmyznikov, @chatasweetie, @MichaelJolley, @Jaylyn-Barbee,
|
||||
@zateutsch, @crutkas
|
||||
```
|
||||
|
||||
## Check Author Script
|
||||
|
||||
```powershell
|
||||
$coreTeam = @(
|
||||
'craigloewen-msft', 'niels9001', 'dhowett', 'yeelam-gordon', 'jamrobot',
|
||||
'lei9444', 'shuaiyuanxx', 'moooyo', 'haoliuu', 'chenmy77', 'chemwolf6922',
|
||||
'yaqingmi', 'zhaoqpcn', 'urnotdfs', 'zhaopy536', 'wang563681252', 'vanzue',
|
||||
'zadjii-msft', 'khmyznikov', 'chatasweetie', 'MichaelJolley', 'Jaylyn-Barbee',
|
||||
'zateutsch', 'crutkas'
|
||||
)
|
||||
|
||||
function Get-Attribution {
|
||||
param([string]$author)
|
||||
|
||||
if ($coreTeam -contains $author) {
|
||||
return $null # No attribution needed
|
||||
}
|
||||
return "Thanks [@$author](https://github.com/$author)!"
|
||||
}
|
||||
|
||||
# Usage
|
||||
$author = gh pr view 12345 --repo microsoft/PowerToys --json author --jq '.author.login'
|
||||
$attribution = Get-Attribution $author
|
||||
if ($attribution) {
|
||||
Write-Host "Add: $attribution"
|
||||
} else {
|
||||
Write-Host "No attribution needed (core team member)"
|
||||
}
|
||||
```
|
||||
|
||||
## Attribution Format
|
||||
|
||||
**With attribution:**
|
||||
```markdown
|
||||
- The Awake countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
```
|
||||
|
||||
**Without attribution (core team):**
|
||||
```markdown
|
||||
- Added new feature to Command Palette for opening settings.
|
||||
```
|
||||
|
||||
## Special Cases
|
||||
|
||||
1. **Co-authored commits**: Credit the primary author (first in list)
|
||||
2. **Bot accounts** (dependabot, etc.): No attribution
|
||||
3. **Former core team members**: Check if they were core team at time of PR
|
||||
4. **Multiple PRs by same external contributor**: Thank them on each entry
|
||||
|
||||
## High-Impact Community Members
|
||||
|
||||
These contributors have made significant ongoing contributions and are recognized in COMMUNITY.md.
|
||||
**ALWAYS thank these contributors** - they are NOT core team and deserve recognition:
|
||||
|
||||
```
|
||||
@davidegiacometti, @htcfreek, @daverayment, @jiripolasek
|
||||
```
|
||||
|
||||
Check COMMUNITY.md for the full up-to-date list under "High impact community members" section.
|
||||
|
||||
## Updated Check Author Script
|
||||
|
||||
```powershell
|
||||
$coreTeam = @(
|
||||
'craigloewen-msft', 'niels9001', 'dhowett', 'yeelam-gordon', 'jamrobot',
|
||||
'lei9444', 'shuaiyuanxx', 'moooyo', 'haoliuu', 'chenmy77', 'chemwolf6922',
|
||||
'yaqingmi', 'zhaoqpcn', 'urnotdfs', 'zhaopy536', 'wang563681252', 'vanzue',
|
||||
'zadjii-msft', 'khmyznikov', 'chatasweetie', 'MichaelJolley', 'Jaylyn-Barbee',
|
||||
'zateutsch', 'crutkas'
|
||||
)
|
||||
|
||||
# High-impact community members - ALWAYS thank these!
|
||||
$highImpactCommunity = @(
|
||||
'davidegiacometti', 'htcfreek', 'daverayment', 'jiripolasek'
|
||||
)
|
||||
|
||||
function Get-Attribution {
|
||||
param([string]$author)
|
||||
|
||||
# Core team and bots don't need thanks
|
||||
if ($coreTeam -contains $author -or $author -match '\[bot\]$') {
|
||||
return $null
|
||||
}
|
||||
# Everyone else (including high-impact community) gets thanked
|
||||
return "Thanks [@$author](https://github.com/$author)!"
|
||||
}
|
||||
```
|
||||
101
.github/skills/changelog-generator/sub-skills/github-api-reference.md
vendored
Normal file
101
.github/skills/changelog-generator/sub-skills/github-api-reference.md
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# GitHub API Reference for Changelog Generation
|
||||
|
||||
This sub-skill provides GitHub API commands and scripts for fetching commit and PR data.
|
||||
|
||||
## Authentication
|
||||
|
||||
```powershell
|
||||
# Ensure gh CLI is authenticated
|
||||
gh auth status
|
||||
|
||||
# Or use personal access token
|
||||
$headers = @{ Authorization = "token $env:GITHUB_TOKEN" }
|
||||
```
|
||||
|
||||
## Useful API Endpoints
|
||||
|
||||
```powershell
|
||||
# Compare two refs (tags, branches, commits)
|
||||
gh api repos/microsoft/PowerToys/compare/v0.96.0...v0.96.1
|
||||
|
||||
# List commits with pagination
|
||||
gh api "repos/microsoft/PowerToys/commits?per_page=100&page=1"
|
||||
|
||||
# Get PR associated with a commit
|
||||
gh api repos/microsoft/PowerToys/commits/{sha}/pulls
|
||||
|
||||
# Get PR details
|
||||
gh api repos/microsoft/PowerToys/pulls/{number}
|
||||
|
||||
# Get files changed in a PR
|
||||
gh api repos/microsoft/PowerToys/pulls/{number}/files
|
||||
|
||||
# Search PRs merged in date range
|
||||
gh api "search/issues?q=repo:microsoft/PowerToys+is:pr+is:merged+merged:2025-01-01..2025-01-31"
|
||||
```
|
||||
|
||||
## Fetch Commits in Range
|
||||
|
||||
```powershell
|
||||
$owner = "microsoft"
|
||||
$repo = "PowerToys"
|
||||
$startTag = "v0.96.0"
|
||||
$endTag = "v0.96.1"
|
||||
|
||||
# Get all commits between two tags
|
||||
gh api repos/$owner/$repo/compare/$startTag...$endTag --jq '.commits[] | {sha: .sha, message: .commit.message, author: .author.login, date: .commit.author.date}'
|
||||
```
|
||||
|
||||
## Get PR Details for a Commit
|
||||
|
||||
```powershell
|
||||
# Get PR associated with a commit SHA
|
||||
gh api "repos/$owner/$repo/commits/{sha}/pulls" --jq '.[0] | {number: .number, title: .title, body: .body, user: .user.login, labels: [.labels[].name]}'
|
||||
|
||||
# Get full PR details
|
||||
gh pr view 1234 --repo microsoft/PowerToys --json title,body,author,files,labels,mergedAt
|
||||
```
|
||||
|
||||
## Batch Processing Script
|
||||
|
||||
```powershell
|
||||
$startTag = "v0.96.0"
|
||||
$endTag = "v0.96.1"
|
||||
|
||||
$commits = gh api repos/microsoft/PowerToys/compare/$startTag...$endTag --jq '.commits[].sha' | ForEach-Object {
|
||||
$sha = $_
|
||||
$prInfo = gh api "repos/microsoft/PowerToys/commits/$sha/pulls" 2>$null | ConvertFrom-Json
|
||||
if ($prInfo) {
|
||||
[PSCustomObject]@{
|
||||
SHA = $sha
|
||||
PRNumber = $prInfo[0].number
|
||||
Title = $prInfo[0].title
|
||||
Author = $prInfo[0].user.login
|
||||
Labels = $prInfo[0].labels.name -join ", "
|
||||
}
|
||||
}
|
||||
}
|
||||
$commits | Format-Table
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
```powershell
|
||||
# Check remaining rate limit
|
||||
gh api rate_limit --jq '.rate'
|
||||
# Authenticated: 5000 requests/hour
|
||||
# Unauthenticated: 60 requests/hour
|
||||
```
|
||||
|
||||
## Pagination for Large Results
|
||||
|
||||
```powershell
|
||||
# GitHub API returns max 100 items per page
|
||||
$page = 1
|
||||
$allCommits = @()
|
||||
do {
|
||||
$commits = gh api "repos/$owner/$repo/commits?per_page=100&page=$page" | ConvertFrom-Json
|
||||
$allCommits += $commits
|
||||
$page++
|
||||
} while ($commits.Count -eq 100)
|
||||
```
|
||||
137
.github/skills/changelog-generator/sub-skills/module-mapping.md
vendored
Normal file
137
.github/skills/changelog-generator/sub-skills/module-mapping.md
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
# PowerToys Module Path Mapping
|
||||
|
||||
This sub-skill maps file paths to PowerToys module names for categorization.
|
||||
|
||||
## Module Path Mapping Table
|
||||
|
||||
| Module | Path Pattern |
|
||||
|--------|--------------|
|
||||
| Advanced Paste | `src/modules/AdvancedPaste/**` |
|
||||
| Always On Top | `src/modules/alwaysontop/**` |
|
||||
| Awake | `src/modules/Awake/**` |
|
||||
| Color Picker | `src/modules/colorPicker/**` |
|
||||
| Command Palette | `src/modules/cmdpal/**` |
|
||||
| Crop And Lock | `src/modules/CropAndLock/**` |
|
||||
| Environment Variables | `src/modules/EnvironmentVariables/**` |
|
||||
| FancyZones | `src/modules/fancyzones/**` |
|
||||
| File Explorer Add-ons | `src/modules/previewpane/**`, `src/modules/FileExplorerPreview/**` |
|
||||
| File Locksmith | `src/modules/FileLocksmith/**` |
|
||||
| Find My Mouse | `src/modules/MouseUtils/FindMyMouse/**` |
|
||||
| Hosts File Editor | `src/modules/Hosts/**` |
|
||||
| Image Resizer | `src/modules/imageresizer/**` |
|
||||
| Keyboard Manager | `src/modules/keyboardmanager/**` |
|
||||
| Light Switch | `src/modules/LightSwitch/**` |
|
||||
| Mouse Highlighter | `src/modules/MouseUtils/MouseHighlighter/**` |
|
||||
| Mouse Jump | `src/modules/MouseUtils/MouseJump/**` |
|
||||
| Mouse Pointer Crosshairs | `src/modules/MouseUtils/MousePointerCrosshairs/**` |
|
||||
| Mouse Without Borders | `src/modules/MouseWithoutBorders/**` |
|
||||
| New+ | `src/modules/NewPlus/**` |
|
||||
| Paste As Plain Text | `src/modules/PastePlain/**` |
|
||||
| Peek | `src/modules/Peek/**` |
|
||||
| PowerRename | `src/modules/powerrename/**` |
|
||||
| PowerToys Run | `src/modules/launcher/**` |
|
||||
| Quick Accent | `src/modules/QuickAccent/**` |
|
||||
| Registry Preview | `src/modules/RegistryPreview/**` |
|
||||
| Screen Ruler | `src/modules/MeasureTool/**` |
|
||||
| Shortcut Guide | `src/modules/ShortcutGuide/**` |
|
||||
| Text Extractor | `src/modules/TextExtractor/**` |
|
||||
| Video Conference Mute | `src/modules/videoconference/**` |
|
||||
| Workspaces | `src/modules/Workspaces/**` |
|
||||
| ZoomIt | `src/modules/ZoomIt/**` |
|
||||
| Settings | `src/settings-ui/**` |
|
||||
| Runner | `src/runner/**` |
|
||||
| Installer | `installer/**` |
|
||||
| General / Infrastructure | `src/common/**`, `.github/**`, `tools/**` |
|
||||
|
||||
## Categorization by PR Labels
|
||||
|
||||
Common PowerToys PR labels for modules:
|
||||
- `Product-FancyZones`
|
||||
- `Product-PowerToys Run`
|
||||
- `Product-Awake`
|
||||
- `Product-ColorPicker`
|
||||
- `Product-Keyboard Manager`
|
||||
- etc.
|
||||
|
||||
## Auto-Categorization Script
|
||||
|
||||
```powershell
|
||||
function Get-ModuleFromPath {
|
||||
param([string]$filePath)
|
||||
|
||||
$moduleMap = @{
|
||||
'src/modules/AdvancedPaste/' = 'Advanced Paste'
|
||||
'src/modules/alwaysontop/' = 'Always On Top'
|
||||
'src/modules/Awake/' = 'Awake'
|
||||
'src/modules/colorPicker/' = 'Color Picker'
|
||||
'src/modules/cmdpal/' = 'Command Palette'
|
||||
'src/modules/CropAndLock/' = 'Crop And Lock'
|
||||
'src/modules/EnvironmentVariables/' = 'Environment Variables'
|
||||
'src/modules/fancyzones/' = 'FancyZones'
|
||||
'src/modules/previewpane/' = 'File Explorer Add-ons'
|
||||
'src/modules/FileExplorerPreview/' = 'File Explorer Add-ons'
|
||||
'src/modules/FileLocksmith/' = 'File Locksmith'
|
||||
'src/modules/MouseUtils/FindMyMouse/' = 'Find My Mouse'
|
||||
'src/modules/Hosts/' = 'Hosts File Editor'
|
||||
'src/modules/imageresizer/' = 'Image Resizer'
|
||||
'src/modules/keyboardmanager/' = 'Keyboard Manager'
|
||||
'src/modules/LightSwitch/' = 'Light Switch'
|
||||
'src/modules/MouseUtils/MouseHighlighter/' = 'Mouse Highlighter'
|
||||
'src/modules/MouseUtils/MouseJump/' = 'Mouse Jump'
|
||||
'src/modules/MouseUtils/MousePointerCrosshairs/' = 'Mouse Pointer Crosshairs'
|
||||
'src/modules/MouseWithoutBorders/' = 'Mouse Without Borders'
|
||||
'src/modules/NewPlus/' = 'New+'
|
||||
'src/modules/PastePlain/' = 'Paste As Plain Text'
|
||||
'src/modules/Peek/' = 'Peek'
|
||||
'src/modules/powerrename/' = 'PowerRename'
|
||||
'src/modules/launcher/' = 'PowerToys Run'
|
||||
'src/modules/QuickAccent/' = 'Quick Accent'
|
||||
'src/modules/RegistryPreview/' = 'Registry Preview'
|
||||
'src/modules/MeasureTool/' = 'Screen Ruler'
|
||||
'src/modules/ShortcutGuide/' = 'Shortcut Guide'
|
||||
'src/modules/TextExtractor/' = 'Text Extractor'
|
||||
'src/modules/videoconference/' = 'Video Conference Mute'
|
||||
'src/modules/Workspaces/' = 'Workspaces'
|
||||
'src/modules/ZoomIt/' = 'ZoomIt'
|
||||
'src/settings-ui/' = 'Settings'
|
||||
'src/runner/' = 'Runner'
|
||||
'installer/' = 'Installer'
|
||||
'src/common/' = 'General'
|
||||
'.github/' = 'Development'
|
||||
'tools/' = 'Development'
|
||||
}
|
||||
|
||||
foreach ($pattern in $moduleMap.Keys) {
|
||||
if ($filePath -like "*$pattern*") {
|
||||
return $moduleMap[$pattern]
|
||||
}
|
||||
}
|
||||
return 'General'
|
||||
}
|
||||
|
||||
# Usage: categorize a PR by its changed files
|
||||
$files = gh pr view 12345 --repo microsoft/PowerToys --json files --jq '.files[].path'
|
||||
$modules = $files | ForEach-Object { Get-ModuleFromPath $_ } | Sort-Object -Unique
|
||||
Write-Host "PR affects modules: $($modules -join ', ')"
|
||||
```
|
||||
|
||||
## Output Organization
|
||||
|
||||
Modules should be listed in **alphabetical order** in the changelog:
|
||||
|
||||
```markdown
|
||||
### Advanced Paste
|
||||
- ...
|
||||
|
||||
### Awake
|
||||
- ...
|
||||
|
||||
### Command Palette
|
||||
- ...
|
||||
|
||||
### General
|
||||
- ...
|
||||
|
||||
### Development
|
||||
- ...
|
||||
```
|
||||
121
.github/skills/changelog-generator/sub-skills/progress-tracking.md
vendored
Normal file
121
.github/skills/changelog-generator/sub-skills/progress-tracking.md
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
# Progress Tracking for Large Changelogs
|
||||
|
||||
This sub-skill provides the checkpoint mechanism for processing many commits without losing progress.
|
||||
|
||||
## When to Use Progress Tracking
|
||||
|
||||
- Processing 50+ commits
|
||||
- Long-running changelog generation
|
||||
- Risk of context overflow in AI conversations
|
||||
|
||||
## Checkpoint Mechanism
|
||||
|
||||
1. **Before starting**: Create a progress tracking file `release-notes-progress.md`
|
||||
2. **After each batch**: Append processed results to `release-change-note-draft.md`
|
||||
3. **Track position**: Record the last processed commit SHA in `release-notes-progress.md`
|
||||
|
||||
## Progress File Template
|
||||
|
||||
Create `release-notes-progress.md`:
|
||||
|
||||
```markdown
|
||||
# Release Notes Generation Progress
|
||||
|
||||
## Configuration
|
||||
- Start Tag: v0.96.0
|
||||
- End Tag: v0.96.1
|
||||
- Total Commits: 127
|
||||
- Batch Size: 20
|
||||
|
||||
## Progress Tracker
|
||||
| Batch | Status | Last SHA | PRs Processed |
|
||||
|-------|--------|----------|---------------|
|
||||
| 1 (1-20) | ✅ Done | abc1234 | #1001, #1002, #1003... |
|
||||
| 2 (21-40) | ✅ Done | def5678 | #1004, #1005... |
|
||||
| 3 (41-60) | 🔄 In Progress | ghi9012 | #1006... |
|
||||
| 4 (61-80) | ⏳ Pending | - | - |
|
||||
| 5 (81-100) | ⏳ Pending | - | - |
|
||||
| 6 (101-120) | ⏳ Pending | - | - |
|
||||
| 7 (121-127) | ⏳ Pending | - | - |
|
||||
|
||||
## Processed PRs (deduplication list)
|
||||
#1001, #1002, #1003, #1004, #1005, #1006
|
||||
|
||||
## Last Checkpoint
|
||||
- Timestamp: 2025-01-07 10:30:00
|
||||
- Last processed commit: ghi9012
|
||||
- Next commit to process: jkl3456
|
||||
```
|
||||
|
||||
## Batch Processing Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. Get total commit count and create progress file │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2. Filter: Skip commits already in start tag │
|
||||
│ - Check if commit is ancestor of start tag │
|
||||
│ - Skip cherry-picks or backports already released │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3. Process batch of 15-20 commits │
|
||||
│ - Fetch commit details │
|
||||
│ - Get associated PRs │
|
||||
│ - Generate changelog entries │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 4. CHECKPOINT: Save progress │
|
||||
│ - Append entries to release-change-note-draft.md │
|
||||
│ - Update release-notes-progress.md with last SHA │
|
||||
│ - Record processed PR numbers │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 5. Check: More commits remaining? │
|
||||
│ YES → Go to step 3 with next batch │
|
||||
│ NO → Go to step 6 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 6. Final merge and formatting │
|
||||
│ - Combine all batches │
|
||||
│ - Deduplicate by PR number │
|
||||
│ - Sort by module alphabetically │
|
||||
│ - Add highlights section │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Resuming from Checkpoint
|
||||
|
||||
If interrupted, read `release-notes-progress.md` to find:
|
||||
1. Which batch was last completed
|
||||
2. The SHA of the last processed commit
|
||||
3. Which PRs have already been processed (for deduplication)
|
||||
|
||||
Then continue from the next unprocessed commit:
|
||||
|
||||
```powershell
|
||||
# Find where you left off
|
||||
$lastSha = "abc1234" # from progress file
|
||||
$remainingShas = gh api repos/microsoft/PowerToys/compare/$lastSha...main --jq '.commits[].sha'
|
||||
```
|
||||
|
||||
## Batch Size Recommendations
|
||||
|
||||
| Total Commits | Recommended Batch Size |
|
||||
|---------------|------------------------|
|
||||
| < 30 | Process all at once |
|
||||
| 30-100 | 15-20 per batch |
|
||||
| 100-300 | 20 per batch |
|
||||
| 300+ | 25 per batch + parallel processing |
|
||||
|
||||
## Deduplication
|
||||
|
||||
Track processed PR numbers to avoid duplicates:
|
||||
- Same PR can appear multiple times (multiple commits)
|
||||
- Cherry-picks may reference same PR
|
||||
- Always check `Processed PRs` list before generating entry
|
||||
119
.github/skills/changelog-generator/sub-skills/user-facing-description.md
vendored
Normal file
119
.github/skills/changelog-generator/sub-skills/user-facing-description.md
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
# Generating User-Facing Descriptions
|
||||
|
||||
This sub-skill explains how to transform technical PR/commit info into user-friendly changelog entries.
|
||||
|
||||
## Information Sources (Priority Order)
|
||||
|
||||
1. **PR Title** - Often the best summary, already user-facing
|
||||
2. **PR Description/Body** - Look for "What does this PR do?" or summary sections
|
||||
3. **Commit Message** - First line is usually descriptive
|
||||
4. **Changed Files** - Infer the impact from what was modified
|
||||
5. **PR Labels** - Indicate type (bug, feature, enhancement)
|
||||
|
||||
## Data Collection Command
|
||||
|
||||
```powershell
|
||||
# Get all relevant info for a PR
|
||||
$prNumber = 12345
|
||||
$prData = gh pr view $prNumber --repo microsoft/PowerToys --json title,body,author,files,labels,mergedAt
|
||||
|
||||
# Parse the data
|
||||
$pr = $prData | ConvertFrom-Json
|
||||
Write-Host "Title: $($pr.title)"
|
||||
Write-Host "Author: $($pr.author.login)"
|
||||
Write-Host "Labels: $($pr.labels.name -join ', ')"
|
||||
Write-Host "Files changed: $($pr.files.Count)"
|
||||
Write-Host "Body preview: $($pr.body.Substring(0, [Math]::Min(500, $pr.body.Length)))"
|
||||
```
|
||||
|
||||
## Transformation Rules
|
||||
|
||||
| Source Info | Transformation | Example Output |
|
||||
|-------------|----------------|----------------|
|
||||
| PR Title: "Fix null reference in FancyZones editor" | Describe the fix from user perspective | "Fixed a crash that could occur when editing zone layouts." |
|
||||
| PR Title: "Add support for XYZ format" | State the new capability | "Added support for XYZ format in File Explorer preview." |
|
||||
| PR Title: "[FancyZones] Refactor grid logic" | Check if user-visible; if not → Development section | (Development) "Refactored FancyZones grid logic for improved maintainability." |
|
||||
| PR with label "bug" | Frame as a fix | "Fixed an issue where..." |
|
||||
| PR with label "enhancement" | Frame as improvement | "Improved..." or "Enhanced..." |
|
||||
|
||||
## Description Generation Process
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ INPUT: PR #12345 │
|
||||
│ Title: "Fix Awake timer drift after system sleep" │
|
||||
│ Body: "The timer was resetting incorrectly when the system │
|
||||
│ resumed from sleep, causing the countdown to be wrong." │
|
||||
│ Author: @daverayment │
|
||||
│ Labels: [bug, Product-Awake] │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ ANALYSIS: │
|
||||
│ 1. Is it user-facing? YES (timer behavior affects users) │
|
||||
│ 2. Category: Bug fix │
|
||||
│ 3. Module: Awake (from label + file paths) │
|
||||
│ 4. Author in core team? NO → needs attribution │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ OUTPUT: │
|
||||
│ "The Awake countdown timer now stays accurate over long │
|
||||
│ periods. Thanks [@daverayment](https://github.com/daverayment)!" │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Writing Style Guidelines
|
||||
|
||||
**DO:**
|
||||
- Start with what changed or what users can now do
|
||||
- Use active voice: "Added...", "Fixed...", "Improved..."
|
||||
- Be specific about the benefit to users
|
||||
- Keep it concise (1-2 sentences max)
|
||||
- Include link to documentation if it's a new feature
|
||||
|
||||
**DON'T:**
|
||||
- Use technical jargon users won't understand
|
||||
- Reference internal code names, file names, or class names
|
||||
- Say "Fixed bug" without explaining what was wrong
|
||||
- Include PR numbers in the description (they're tracked separately)
|
||||
|
||||
## Example Transformations
|
||||
|
||||
| Technical PR Title | User-Facing Description |
|
||||
|--------------------|------------------------|
|
||||
| "Fix NRE in FZEditor when zones is null" | "Fixed a crash that could occur when opening the FancyZones editor with no saved layouts." |
|
||||
| "Add ExifTool integration for PowerRename" | "PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`." |
|
||||
| "Perf: Reduce memory allocation in PT Run" | "Improved PowerToys Run startup performance and reduced memory usage." |
|
||||
| "Update Newtonsoft.Json to 13.0.3" | (Development) "Updated Newtonsoft.Json dependency." OR skip if no user impact |
|
||||
| "Add unit tests for color picker" | SKIP - not user-facing |
|
||||
| "Fix typo in settings UI" | "Fixed a typo in the Settings interface." (only if visible to users) |
|
||||
|
||||
## Context-Aware Description
|
||||
|
||||
Sometimes you need to read the PR body or changed files to understand the impact:
|
||||
|
||||
```powershell
|
||||
# If PR title is unclear, check the body for context
|
||||
$prBody = gh pr view 12345 --repo microsoft/PowerToys --json body --jq '.body'
|
||||
|
||||
# Look for common patterns in PR descriptions:
|
||||
# - "## Summary" or "## Description"
|
||||
# - "This PR fixes/adds/improves..."
|
||||
# - "Before/After" comparisons
|
||||
# - Screenshots (indicate UI changes)
|
||||
|
||||
# Check what files changed to understand scope
|
||||
$files = gh pr view 12345 --repo microsoft/PowerToys --json files --jq '.files[].path'
|
||||
# If mostly .xaml files → UI change
|
||||
# If mostly .cs/.cpp in one module → module-specific change
|
||||
# If in src/common/ → potentially affects multiple modules
|
||||
```
|
||||
|
||||
## Handling Ambiguous PRs
|
||||
|
||||
If a PR's impact is unclear:
|
||||
1. **Check the linked issue** - often has user-reported symptoms
|
||||
2. **Look at file changes** - understand what was modified
|
||||
3. **Check PR comments** - may have discussion about user impact
|
||||
4. **When in doubt** - put in Development section or ask for clarification
|
||||
201
.github/skills/powertoys-verification/LICENSE.txt
vendored
201
.github/skills/powertoys-verification/LICENSE.txt
vendored
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
331
.github/skills/powertoys-verification/SKILL.md
vendored
331
.github/skills/powertoys-verification/SKILL.md
vendored
@@ -1,331 +0,0 @@
|
||||
---
|
||||
name: powertoys-verification
|
||||
description: "Verify PowerToys behavior end-to-end with the winapp CLI across two scenarios: (A) a module's release checklist against the installed build; (B) PR validation — derive each PR's checklist from its description + diff, then drive it against the installed build (a merged/shipped PR, or a whole release/hotfix set) or by building + sideloading the module when the PR isn't in the build yet (unmerged or not-yet-released). Drive each item via UIA invoke / Named Events / settings.json edits / clipboard / GPO / SendInput, and emit a structured PASS / FAIL / BLOCKED verdict per item with evidence (FAIL distinguishes product defects from stale/ambiguous checklist items). Use when asked to verify a module checklist, validate a PR, sign off a release/hotfix's PRs, or QA installed/sideloaded PowerToys bits. Combines generic winapp ui mechanics (references/winapp-ui-testing.md) with PT-specific recipes, per-scenario playbooks (references/scenarios/), and the helper .ps1 files shipped with this skill."
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill whenever you need to **verify PowerToys behavior with the winapp CLI** and emit a
|
||||
structured PASS / FAIL / BLOCKED verdict with evidence (UIA enumeration, log line, settings.json
|
||||
diff, screenshot, etc.). It runs in **two scenarios** that share the same drive techniques,
|
||||
helpers, taxonomy and report format — and differ only on what the checklist is and what bits you test:
|
||||
|
||||
| Scenario | Trigger | Checklist source | Bits under test |
|
||||
|---|---|---|---|
|
||||
| **A — Module checklist** | "verify all `<Module>` items", "sign off Color Picker" | Supplied file (`references/release-checklist/<module>.md`) | Installed shipped artifact (read-only) |
|
||||
| **B — PR validation** | "validate PR #N" (open or merged), "build it and test the fix", "verify the PRs in this release/hotfix", "sign off 0.X.Y" | **Derived** from each PR's description + diff | **Installed** shipped artifact if the code is already in the build; **build + sideload** if it isn't (unmerged / not-yet-released) |
|
||||
|
||||
> **Step 0 for every run — pick the scenario.** Read **`references/scenarios/index.md`** (the
|
||||
> router + the "bits under test" contract + the verdict-vocabulary mapping), then read the one
|
||||
> matching scenario doc (`references/scenarios/{module-checklist,pr-validation}.md`). For **B**, also
|
||||
> resolve the bits sub-decision ("is the PR's code in the build under test?") before driving.
|
||||
> This `SKILL.md` is the *shared engine* (drive techniques, helpers, taxonomy, pitfalls) common to both.
|
||||
|
||||
Each item produces a PASS / FAIL / BLOCKED verdict with evidence. For **A** the checklist is
|
||||
supplied; for **B** you derive it from the PR(s). The skill is the *how* — independent of any
|
||||
specific checklist.
|
||||
|
||||
## Required reads (in order)
|
||||
|
||||
1. **`references/scenarios/index.md`** — **read FIRST**: the scenario router (A/B), the
|
||||
**"bits under test" contract** (installed-and-immutable, vs build-and-sideload for a PR whose code
|
||||
isn't in the build under test), and the verdict-vocabulary mapping. Then read the **one** matching
|
||||
scenario doc: `references/scenarios/module-checklist.md` (A) · `pr-validation.md` (B).
|
||||
2. **`references/winapp-ui-testing.md`** — the **prerequisite** UIA mechanics doc (winapp ui verbs, scripted batch testing, file pickers, accessibility audits, screenshots, click-vs-invoke, PostMessage, SendInput cb=40, stunted-UIA recovery, settings-mutation safety contract). **Read this first** — this skill assumes you know its content and only adds PT-specific extensions.
|
||||
3. **This `SKILL.md`** — the shared engine: the 3-bucket drive-technique selector (Step 2), classification taxonomy, critical pitfalls, helper-script catalog.
|
||||
4. **`references/modules/<module>.md` IF IT EXISTS** — per-module entry-paths, item-by-item recipes, common BLOCKED traps, fixture lists, source citations. **Always check `references/modules/` first.** If no profile exists, fall back to this SKILL.md and create one after you finish (template in `references/modules/README.md`).
|
||||
5. **`references/explorer-context-menu-flow.md` IF your module registers an Explorer right-click entry** (PowerRename, File Locksmith, Image Resizer, New+, Preview Pane, RegistryPreview) — shared synthetic-right-click + UIA-invoke + multi-file-selection flow + module-caption table. Helper: `scripts/pt-explorer-contextmenu.ps1`.
|
||||
6. **`references/pre-flight.md`** — pre-flight checks, bootstrap snippet, state-hygiene cleanup, final wrap-up, hard rules.
|
||||
7. **`references/reporting-format.md`** — per-item table template, top-of-report summary, step-table rules, anti-patterns, worked example.
|
||||
8. **`references/environment-setup.md`** — RDP/sleep/screensaver/session-attachment gotchas. Cite in BLK-ENV verdicts.
|
||||
9. **`references/release-checklist/<module>.md` — SCENARIO A ONLY** — the supplied checklist for the module under test (one file per module; see `references/release-checklist/index.md`). Each item carries `[ADMIN: …]` + `[CLARITY: …]` metadata. **This file IS the set of items to verify.** For B the checklist is derived from the PR(s) instead (see the scenario doc).
|
||||
|
||||
## Helper scripts shipped with this skill
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `scripts/pt-shared-events.ps1` | `Invoke-PtSharedEvent`, `Test-PtSharedEvent`, `Get-PtSharedEventCatalog` — 56-entry friendly-name map for PT Named Events (CmdPal.Show, AOT.Pin, PowerLauncher.Invoke, LightSwitch.Toggle, ZoomIt.Draw, ...). The deterministic, foreground-free, UIPI-immune way to trigger a module. |
|
||||
| `scripts/pt-sendinput-chord.ps1` | `Send-PtChord`, `Wait-PtHotkeyAccepted` — last-resort SendInput hotkey injection with the cb=40 fix. Use only when the module has no Named Event and the hotkey itself is the test subject. |
|
||||
| `scripts/pt-foreground-guard.ps1` | `Test-PtForeground`, `Force-PtForeground`, `Assert-PtForegroundOrAbort` — guard helpers to ensure target window IS foreground before SendInput, so keys don't leak to caller's terminal. |
|
||||
| `scripts/pt-cmdpal-recycle.ps1` | `Reset-CmdPalAppX`, `Reset-CmdPalToHome`, `Test-CmdPalDegraded`, `Invoke-CmdPalQuery` — CmdPal-specific lifecycle (handles TextChanged-broken state, BackButton navigation, AppX recycle). |
|
||||
| `scripts/pt-admin-probe.ps1` | `Test-PtAdmin`, `Test-ProcessElevated`, `Test-PtRunnerAdmin` — TokenElevation probes to verify your session and the PT runner have the right elevation for the test. |
|
||||
| `scripts/pt-clipboard-diff.ps1` | `Get-PtClipboardFormats`, `Compare-PtClipboardFormatDiff`, `Set-PtClipboardRich` — multi-format clipboard inspection for Advanced Paste tests. |
|
||||
| `scripts/pt-explorer-com.ps1` | `Get-PtExplorerWindows`, `Open-PtExplorerAtPath`, `Select-PtExplorerFiles`, `Invoke-PtPeekWithExplorerSelection`, `Test-PtInteractiveDesktop` — drive Explorer via Shell COM to set up multi-file selections, then trigger Peek/FZ/PowerRename/Image Resizer/Workspaces via their hotkeys. **Use this for Peek L706-L709, L719-L720 and any test that needs an Explorer file selection.** |
|
||||
| `scripts/pt-explorer-contextmenu.ps1` | `Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Open-PtBackgroundContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems` — open Win11's real context menu **coordinate-free** (COM-select the file → focus it → Shift+F10; coordinate right-click only as fallback), then UIA-invoke a menu item by name. **Canonical user-flow path for File Locksmith / Image Resizer / PowerRename / New+ menu-presence + launch tests** — prefer selecting a file (`Open-PtExplorerContextMenu -FileName`) over the background menu; open one menu per fresh window; assert menu TYPE + focus, not module presence. Needs an unlocked interactive desktop. See `references/explorer-context-menu-flow.md` for the full write-up, stability notes, and per-module captions. |
|
||||
| `scripts/pt-shell-verbs.ps1` | `Get-PtShellVerbs`, `Invoke-PtShellVerb`, `Reset-PtShellComCache` — enumerate + invoke CLASSIC HKCR shell verbs via Shell.Application COM. **NOT for PT context-menu modules on Win11** (PT registers via IExplorerCommand, not classic — use `pt-explorer-contextmenu.ps1` for those). Useful for non-PT verbs (Open/Edit/Send-to/third-party) and as a negative check that PT verbs are NOT classic-shadowed. |
|
||||
| `scripts/pt-state.ps1` | `Get-PtSettings`, `Get-PtModuleSettings`, `Get-CmdPalSettings`, `Get-PtRunnerLogTail`, `Test-PtModuleEnabled`, `Test-PtModuleProcess`, `Restart-PtRunner`, `Backup-PtModuleSettings`, `Restore-PtModuleSettings` — common state checks. |
|
||||
| `scripts/pt-nonelevated.ps1` | `Start-PtNonElevated`, `Invoke-PtNonElevatedCapture` — launch an exe at **Medium IL (non-elevated)** from an elevated agent shell via a one-shot `RunLevel Limited` scheduled task. Required for elevation-visibility tests (a non-elevated module must NOT see higher-integrity processes; e.g. File Locksmith L649/L650). Verify the result with `Test-ProcessElevated`. |
|
||||
|
||||
Dot-source them **all** at once in your bootstrap (the `Get-ChildItem` loop loads every helper — see **Step 1 — Bootstrap**):
|
||||
```powershell
|
||||
$skill = '<this skill folder>' # the folder containing SKILL.md, e.g. <PT-repo>\.github\skills\powertoys-verification
|
||||
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
|
||||
```
|
||||
|
||||
## Step 1 — Bootstrap
|
||||
|
||||
```powershell
|
||||
$module = 'AdvancedPaste' # or 'CmdPal', 'FZ', 'Peek', ...
|
||||
# Work out of %TEMP% during the run (keeps screenshots/scratch off OneDrive); move to the
|
||||
# sign-off archive at the very end (see Step 7).
|
||||
$workspace = "$env:TEMP\verify-$module-$(Get-Date -Format yyyyMMdd-HHmmss)"
|
||||
New-Item -ItemType Directory -Path $workspace, "$workspace\artifacts" -Force | Out-Null
|
||||
$report = "$workspace\verify-$module.md"
|
||||
|
||||
# Dot-source helpers
|
||||
$skill = '<this skill folder>' # set once at top of your script (the folder containing SKILL.md)
|
||||
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
|
||||
|
||||
# Verify environment
|
||||
"=== Environment ===" | Tee-Object $report -Append
|
||||
"IsAdmin: $(Test-PtAdmin)" | Tee-Object $report -Append
|
||||
$rn = Test-PtRunnerAdmin
|
||||
"PT runner: PID=$($rn.Pid) Elevated=$($rn.Elevated)" | Tee-Object $report -Append
|
||||
|
||||
# The checklist source depends on the scenario (see references/scenarios/):
|
||||
# A - read the supplied references/release-checklist/<module>.md
|
||||
# B - derive 1-3 items from each PR's `gh pr view/diff`; then drive the installed bits, OR
|
||||
# build+sideload if the code isn't in the build under test (pr-validation.md)
|
||||
# Then iterate the items (see Step 6 - Verifier loop).
|
||||
```
|
||||
|
||||
## Step 2 — Drive techniques
|
||||
|
||||
Every checklist item boils down to ONE of three intents. **Pick the bucket from the verb in the item, then use the best technique inside it.** Stop at the first technique that works.
|
||||
|
||||
| Intent | Verb-cues in the checklist item | Bucket |
|
||||
|---|---|---|
|
||||
| Change a setting | "default is X", "setting persists", "is enabled/disabled by default", "value Y is accepted" | §2.A |
|
||||
| Interact with a UI element | "click X", "toggle X", "type into Y", "X button is visible", "selecting Z does W" | §2.B |
|
||||
| Trigger a module action | "pressing hotkey X opens Y", "module launches", "Z happens when invoked" | §2.C |
|
||||
|
||||
### §2.A — Change a setting (single technique)
|
||||
|
||||
Edit the JSON file the module reads, wait for the file-watcher debounce, assert, then restore from backup. Zero external tools.
|
||||
|
||||
```powershell
|
||||
$bk = Backup-PtModuleSettings -ModuleDir AdvancedPaste
|
||||
try {
|
||||
$j = Get-PtModuleSettings -ModuleDir AdvancedPaste
|
||||
$j.properties.IsAdvancedAIEnabled.value = $false
|
||||
$j | ConvertTo-Json -Depth 12 | Set-Content "$env:LOCALAPPDATA\Microsoft\PowerToys\AdvancedPaste\settings.json"
|
||||
Start-Sleep -Seconds 4 # debounce — runner re-reads via file-watcher
|
||||
# ... assertion ...
|
||||
} finally {
|
||||
Restore-PtModuleSettings -ModuleDir AdvancedPaste -BackupPath $bk
|
||||
}
|
||||
```
|
||||
|
||||
> For shell-extension modules (PowerRename, File Locksmith, Image Resizer, New+) edit the **module-owned** file under `%LOCALAPPDATA%\Microsoft\PowerToys\<Module>\`, then `Restart-PtRunner` (and on stubborn handlers, restart Explorer). See pitfall #12 below.
|
||||
>
|
||||
> If you need to flip the *enabled* bit for a whole module, debounce isn't enough — call `Restart-PtRunner` after the write.
|
||||
|
||||
### §2.B — Interact with a UI element (2 techniques, most-reliable first)
|
||||
|
||||
#### B1. UIA invoke / set-value — **always try first**
|
||||
```powershell
|
||||
winapp ui invoke 'SubmitButton' -a PowerToys.Settings
|
||||
winapp ui set-value 'QueryTextBox' '=2+3*4' -a PowerToys.PowerLauncher
|
||||
Start-Sleep -Milliseconds 600
|
||||
winapp ui inspect -a PowerToys.PowerLauncher --depth 7 -i 2>$null
|
||||
```
|
||||
Invoke goes through UIA InvokePattern COM IPC — no foreground steal, no UIPI. See references/winapp-ui-testing.md §CRITICAL — invoke vs click.
|
||||
|
||||
#### B2. PostMessage WM_KEYDOWN/CHAR — when UIA can't reach the target
|
||||
For elevated targets, AppX windows with stunted UIA trees, or keystrokes that UIA `set-value` can't dispatch (arrow-key ListView nav, Enter to commit). See references/winapp-ui-testing.md §CRITICAL — Keystroke input that bypasses UIPI (PostMessage). Esc is often filtered by WinUI 3 raw-input hook — use BackButton invoke instead.
|
||||
|
||||
### §2.C — Trigger a module action (2 techniques, most-reliable first)
|
||||
|
||||
| | C1 Named Event | C2 SendInput chord |
|
||||
|---|---|---|
|
||||
| **Proves** | The action fires (the path *downstream* of the hotkey). **Not** that the chord is bound. | The full path: real keys → runner hook → action. The **only** method that proves the chord binding itself. |
|
||||
| **Robustness** | Highest — no foreground, no input desktop, UIPI-immune; works headless / RDP-minimized. | Lowest — needs an attached input desktop (else `BLK-ENV`), steals foreground, can't inject OS-reserved chords (Win+L / Win+Tab). |
|
||||
| **Precondition** | Owning module process is running (the event only exists while it is). | Attached input desktop + foreground. |
|
||||
|
||||
**Pick by what the item asserts:** for "does action Y happen" use C1; for "pressing chord X triggers Y" or "the rebind takes effect", C1 is insufficient (it bypasses the chord) — use C2, or C1 *plus* a runner-log line proving the chord was accepted.
|
||||
|
||||
#### C1. Named Event signal — preferred
|
||||
```powershell
|
||||
Invoke-PtSharedEvent -Name 'CmdPal.Show' # opens CmdPal without keyboard
|
||||
Invoke-PtSharedEvent -Name 'AOT.Pin' # pins foreground window via AOT
|
||||
Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke' # opens PT Run
|
||||
Invoke-PtSharedEvent -Name 'LightSwitch.Toggle' # toggles theme
|
||||
Get-PtSharedEventCatalog | Format-Table # full list
|
||||
```
|
||||
No synthetic input — it's a `SetEvent` on the kernel event the module waits on, the same downstream path the runner's hotkey handler signals. Verify the side effect via UIA (`winapp ui list-windows -a <module>`), a log line (`Get-PtRunnerLogTail`), or settings.json diff (`Get-PtModuleSettings`). The event only exists while the owning process runs, so `Test-PtSharedEvent` doubles as an "is the module alive" check.
|
||||
|
||||
#### C2. SendInput chord — last resort / chord-binding verification
|
||||
Real synthetic keys. Loud (steals foreground) and fragile, but the only way to prove the activation chord is actually bound. The runner's global keyboard hook catches the chord regardless of focus, so the precondition is just an **attached input desktop** (pitfall #7; on a detached desktop `SendInput` returns `ACCESS_DENIED` and the keys vanish → mark `BLK-ENV`).
|
||||
```powershell
|
||||
# Precondition: input desktop attached? 0 = detached → don't bother sending, mark BLK-ENV (pitfall #7)
|
||||
if ([PtFg]::GetForegroundWindow() -eq [IntPtr]::Zero) { throw 'No input desktop — BLK-ENV (pitfall #7)' }
|
||||
|
||||
Send-PtChord -Mods 0x5B,0x10 -Key 0x43 # Win+Shift+C → Color Picker (cb=40 fix is inside the helper)
|
||||
$line = Wait-PtHotkeyAccepted -ModuleHint 'Color' -TimeoutSec 3
|
||||
if (-not $line) { throw 'Runner did not log hotkey invocation' }
|
||||
```
|
||||
|
||||
> **Rare fallback — a module that uses its own `RegisterHotKey` and exposes no Named Event.** Post `WM_HOTKEY` (`0x312`) straight to its message window (find the HWND via `EnumWindows`+`GetClassName` through `Add-Type` — same P/Invoke pattern as `pt-foreground-guard.ps1`). **No current PT module needs this:** ZoomIt — the obvious candidate — also waits on Named Events (`ZoomIt.Zoom`, `ZoomIt.Draw`, …; source: `Zoomit.cpp` `CreateEventW(ZOOMIT_ZOOM_EVENT)`), so drive it with C1.
|
||||
|
||||
> **Different case — sending keys *into* a specific focused window** (e.g. a CmdPal alias like `=` / `<` / `>` that `winapp ui set-value` can't trigger because it bypasses TextChanged; see `references/modules/command-palette.md`). Here the keystrokes go to whatever currently has focus, so you must bring the target window foreground first:
|
||||
> ```powershell
|
||||
> Assert-PtForegroundOrAbort -AppId Microsoft.CmdPal.UI # -AppId = the window you're typing INTO
|
||||
> Send-PtChord -Key 0xBB # '=' (no modifiers) to trigger the calculator alias
|
||||
> ```
|
||||
> The `-AppId` is whatever window you're targeting — it's **not** CmdPal-specific. CmdPal is just the worst offender: its AppX foreground-lock drops focus after the first `SetForegroundWindow`, so without the guard the keys silently leak to your terminal.
|
||||
|
||||
> Verdict decisions (PASS if behavior matches spec; **FAIL** if the product is wrong *or* the checklist item is stale/ambiguous; BLOCKED if you couldn't run the check after ≥2 entry-paths) live in **Step 3 — Classification taxonomy** below. Don't put verdict logic in Step 2.
|
||||
|
||||
## Step 3 — Classification taxonomy
|
||||
|
||||
### Verdicts (assign exactly ONE per item)
|
||||
|
||||
| Verdict | Meaning |
|
||||
|---|---|
|
||||
| **PASS** | You drove/observed the behavior and it matched the spec. **A pass is a pass — there is no PASS sub-type.** Record *how* you verified in the item's **Category** field as free text, e.g. "full UIA flow + asserted popup", "settings.json round-trip", "runner-log line", "Shell COM / IExplorerCommand", "screenshot pixel-diff", "output matches fixture", "process spawn/exit", "module CLI", "admin GPO write". |
|
||||
| **FAIL** | The item is **red** — something is wrong and action is required. Treat the checklist as test code: a test fails because **the product is wrong** *or* **the test/checklist is wrong**. Record the **cause** in the **Category** field: <br>• **product** — behavior contradicts a valid spec → file a product bug (repro + expected-vs-actual + screenshot/log + build version). <br>• **checklist** — the item itself is broken: *stale* (feature was removed/deprecated — cite the source grep proving it's gone) or *ambiguous* (`[CLARITY: VAGUE-*]`, no definable pass/fail criterion — quote the original wording). Fix the checklist, not the product. |
|
||||
| **BLOCKED** | Couldn't run the check in this environment / with this toolset *after ≥2 entry-paths* — inconclusive, like a skipped test. **Not red against the product.** Tag exactly one concrete reason below. |
|
||||
|
||||
### BLOCKED reasons
|
||||
Different failure reasons stay distinct because each drives a different remediation.
|
||||
|
||||
| Reason | When |
|
||||
|---|---|
|
||||
| `BLK-ENV` | This specific shell can't drive it (non-interactive / Session 0, RDP-minimized, missing Explorer windows) but a normal interactive desktop CAN. Triggers a "re-run on an interactive desktop" recommendation. Cite `references/environment-setup.md`. |
|
||||
| `BLK-HARDWARE` | Needs hardware this session lacks — multi-monitor, 2 physical PCs (MWB), real camera / battery / game-mode, or live screen/device capture. State the specific shortfall in **Category**. |
|
||||
| `BLK-DRAG-REQUIRED` | Needs a real mouse-drag gesture; synthetic drag is insufficient (e.g. FancyZones snap). |
|
||||
| `BLK-DESTRUCTIVE` | Reboot, hibernate, install/uninstall, or mid-session AppX uninstall — would damage the run environment. |
|
||||
| `BLK-VISUAL-RENDER` | The thing to verify is a rendered surface UIA can't see — WinUI3 islands, WebView2, or Explorer-side context-menu rendering/localization. Needs pixel/OCR or a manual eyeball. |
|
||||
| `BLK-OVERLAY-INPUT-BLOCK` | Overlay both blocks input and excludes itself from capture (`BlockInput` + `WDA_EXCLUDEFROMCAPTURE`, e.g. ZoomIt draw mode) — can neither drive nor screenshot it. |
|
||||
| `BLK-EXTERNAL-APP` | Needs a 3rd-party tool, a real API key, or a system locale change. |
|
||||
|
||||
**Rule of thumb**: in your report, separate the two FAIL causes — *product* FAILs are bugs to file; *checklist* FAILs are items to rewrite or prune. `BLOCKED` is only for a concrete, named obstacle (cite it), never a substitute for effort. If a large share of a module's items are checklist-FAILs, the checklist needs an overhaul before re-verifying.
|
||||
|
||||
## Step 4 — Report format
|
||||
|
||||
**See `references/reporting-format.md` for the full template** (per-item table, summary, step-table rules, anti-patterns, worked example). Don't paraphrase; copy the templates literally. This includes a mandatory **§G Retrospective** — a self-reflection on the *run itself*: list every friction encountered (classified by source — `SKILL-UNCLEAR` / `WINAPP-TOOL-BUG` / `WINAPP-DOC-UNCLEAR` / `HELPER-FLAW` / `PT-PRODUCT` / `ENVIRONMENT` — with severity + minutes/attempts cost + a suggested fix), or write `Everything was smooth — no friction encountered.` if there was none. This is how the skill improves run over run, so don't skip it.
|
||||
|
||||
## Step 5 — State hygiene (CRITICAL)
|
||||
|
||||
**See `references/pre-flight.md` §State hygiene** for the backup/restore pattern and cleanup commands. Always wrap mutations in `try { ... } finally { Restore-* }`.
|
||||
|
||||
## Module-specific quick reference
|
||||
|
||||
**Look for `references/modules/<module>.md` FIRST.** Each per-module profile contains paths, entry-paths, item-by-item recipes, common BLOCKED traps, fixture lists, and source citations specific to that module.
|
||||
|
||||
Catalog: see `references/modules/README.md`. Currently authored: `peek.md`, `power-rename.md`, `file-locksmith.md`, `image-resizer.md`.
|
||||
|
||||
If your module has NO profile yet:
|
||||
1. Fall back to the generic drive-stack in §2 above.
|
||||
2. **For Explorer-context-menu modules** (PowerRename / File Locksmith / Image Resizer / New+ / Preview Pane / RegistryPreview): read **`references/explorer-context-menu-flow.md`** first — it has the synthetic-right-click + UIA-invoke pattern with stability rules and module-caption table. Per-module profiles cite it and only document module-specific quirks. The canonical helper is `scripts/pt-explorer-contextmenu.ps1`.
|
||||
3. After finishing the verification, **create the profile** using the template in `references/modules/README.md` so the next agent benefits from what you learned.
|
||||
|
||||
Quick one-liners for modules without dedicated profiles (will be moved to per-module files as they're authored):
|
||||
|
||||
- **Advanced Paste**: `Invoke-PtSharedEvent -Name 'AdvancedPaste.ShowUI'` + `Set-PtClipboardRich` + `Compare-PtClipboardFormatDiff` (see `scripts/pt-clipboard-diff.ps1`).
|
||||
- **Command Palette**: `Invoke-PtSharedEvent -Name 'CmdPal.Show'` + `Invoke-CmdPalQuery` (auto-handles degraded state via `scripts/pt-cmdpal-recycle.ps1`). Settings file via `Get-CmdPalSettings`. **Full profile + AppX/alias/foreground quirks: `references/modules/command-palette.md`.**
|
||||
- **PowerToys Run**: `Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke'` + `winapp ui set-value QueryTextBox`. Window has 2 HWNDs — filter by width ≥ 800.
|
||||
- **FancyZones**: `Invoke-PtSharedEvent -Name 'FancyZones.ToggleEditor'`. Snap-drag tests are usually `BLK-DRAG-REQUIRED`; settings verify via settings.json round-trip.
|
||||
- **Light Switch**: `Invoke-PtSharedEvent -Name 'LightSwitch.Toggle' | LightSwitch.Light | LightSwitch.Dark`. Verify via `HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme`.
|
||||
- **Always on Top**: `Invoke-PtSharedEvent -Name 'AOT.Pin'`. Verify `WS_EX_TOPMOST` on pinned HWND.
|
||||
- **Hosts File Editor** (admin): `Invoke-PtSharedEvent -Name 'Hosts.Show' | Hosts.ShowAdmin`.
|
||||
- **GPO** (admin): write `HKLM:\Software\Policies\PowerToys` + `Restart-PtRunner` + `Get-PtRunnerLogTail -Pattern 'GPO sets'`. Cleanup: `Remove-Item HKLM:\Software\Policies\PowerToys -Recurse -Force`.
|
||||
- **Mouse Without Borders**: most items `BLK-HARDWARE` (need 2 physical PCs).
|
||||
- **ZoomIt**: most modes inside `BlockInput + WDA_EXCLUDEFROMCAPTURE` overlay → `BLK-OVERLAY-INPUT-BLOCK`. Mode triggers: `ZoomIt.Zoom`, `ZoomIt.Draw`, `ZoomIt.Break`, etc.
|
||||
- **Peek**: see `references/modules/peek.md` for the full recipe (CLI back-door + Shell.Application + Ctrl+Space).
|
||||
|
||||
## Step 6 — Verifier loop per checkbox
|
||||
|
||||
```
|
||||
For each item in module:
|
||||
1. Pick a bucket from the verb in the item (§2.A change a setting / §2.B interact with UI / §2.C trigger an action)
|
||||
2. Walk that bucket's techniques top-to-bottom; stop at the first one that drives the item
|
||||
3. Compare observed behavior to the spec:
|
||||
• matches the spec → PASS (note the method in Category)
|
||||
• product behaves wrong → FAIL, cause=product (repro + expected/actual + screenshot/log + build)
|
||||
4. Checklist item itself is broken — feature removed from source, or spec too ambiguous to judge → FAIL, cause=checklist (cite the source proof / quote the wording)
|
||||
5. Couldn't drive it after ≥2 entry-paths → BLOCKED with a concrete reason (§3)
|
||||
6. Record verdict + evidence + cleanup
|
||||
7. Next item
|
||||
```
|
||||
|
||||
When done, run state hygiene cleanup, write the report **including the §G retrospective**, archive the workspace (Step 7), and exit.
|
||||
|
||||
## Step 7 — Archive the workspace to the sign-off folder (do this LAST)
|
||||
|
||||
The live run works out of `%TEMP%`, but the **final deliverable must live in the module sign-off archive** so reports persist and sync via OneDrive:
|
||||
|
||||
```powershell
|
||||
# After the report is written AND the artifact-existence check passes:
|
||||
$signoff = "$env:OneDrive\PowerToys\Module-Signoff" # e.g. C:\Users\<you>\OneDrive - Microsoft\PowerToys\Module-Signoff
|
||||
New-Item -ItemType Directory -Path $signoff -Force | Out-Null
|
||||
$final = Join-Path $signoff (Split-Path $workspace -Leaf)
|
||||
Move-Item -Path $workspace -Destination $final -Force
|
||||
# Report uses RELATIVE artifacts/… paths, so all links stay valid after the move.
|
||||
Write-Host "Final report: $(Join-Path $final (Split-Path $report -Leaf))"
|
||||
```
|
||||
|
||||
Print the **moved** report path (under `…\PowerToys\Module-Signoff\`) as the last line — never the `%TEMP%` path.
|
||||
|
||||
## Invocation & placeholders
|
||||
|
||||
This skill auto-activates for either of the two scenarios above (verify a module checklist, or
|
||||
validate a PR — open, merged, or a whole release/hotfix set). **Step 0: resolve the scenario via
|
||||
`references/scenarios/index.md` and set the `BITS:` contract** (for B, also the bits sub-decision:
|
||||
"is the PR's code in the build under test?") before driving anything. Scope rule: **one module per
|
||||
run for Scenario A**, **one PR per report folder for B**. Resolve these placeholders:
|
||||
|
||||
| Placeholder | Substitute with |
|
||||
|---|---|
|
||||
| `<Module>` | Exact display name, e.g. `Color Picker`, `Command Palette`, `PowerToys Run`, `FancyZones` (see `references/release-checklist/index.md`). |
|
||||
| `<module>` | Lowercase-kebab-case for file lookup, e.g. `color-picker`, `command-palette`, `power-rename` — used for BOTH `references/release-checklist/<module>.md` (A's checklist) and `references/modules/<module>.md` (profile, if any). |
|
||||
| `<ModuleDir>` | settings.json sub-dir under `%LOCALAPPDATA%\Microsoft\PowerToys\` (e.g. `AdvancedPaste`, `FancyZones`, `PowerToys Run` (with space)). |
|
||||
| `<N>` | Scenario A: total item count for the module. Scenario B: the GitHub PR number. |
|
||||
|
||||
**Execution order:** `references/scenarios/index.md` (pick scenario + set `BITS`) → the matching
|
||||
`references/scenarios/<scenario>.md` (inputs, discipline, and build+sideload deploy steps for B when the code isn't in the build) → `references/pre-flight.md`
|
||||
→ per item, the §2 drive-stack (this file) → `references/reporting-format.md` per-item table →
|
||||
Step 6 verifier loop → `references/pre-flight.md` §Final wrap-up → Step 7 archive → print the final report path.
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Do NOT skip Step 0 — drive nothing until you've set the scenario and the `BITS:` contract. A run that mutates the wrong bits (sideloads when the code is already installed, or drives the stale installed binary when validating unreleased code) is invalid regardless of the verdict.
|
||||
- Do NOT chain multiple modules in one report (Scenario A) — one module per run. For B, one PR per report folder.
|
||||
- Do NOT mark an item BLOCKED without a concrete, named obstacle (see §3 and `references/pre-flight.md` §Hard rules).
|
||||
- Do NOT invent steps for a VAGUE checklist item — if the spec is too ambiguous to judge, that is FAIL (cause=checklist), not a guess.
|
||||
- All other rules (foreground guard, always restore mutated state, etc.) live in `references/pre-flight.md` §Hard rules — follow them.
|
||||
|
||||
## Critical pitfalls (PT-specific)
|
||||
|
||||
*Reference, not a sequential step — skim before you start and consult while driving. Numbered for cross-reference only.*
|
||||
|
||||
1. **Each module's own `<Module>\settings.json` IS hot-reloaded** via per-module file watcher (~3s debounce). **EXCEPTION — shell-extension/context-menu modules do NOT read this file; see pitfall #12.**
|
||||
2. **GPO HKLM vs HKCU**: HKLM wins when both are set with conflicting values.
|
||||
3. **HKLM `Software\Policies\PowerToys` writes require admin** — verify with `Test-PtAdmin`.
|
||||
4. **`Stop-Process` is policy-blocked in this session unless you pass `-Id <int>` literally**. Always inline the PID.
|
||||
5. **WinUI 3 islands are largely invisible to UIA** (QuickAccess flyout, RegistryPreview Monaco editor, Peek WebView2). For these, fall back to screenshot + OCR or settings.json diff.
|
||||
6. **OS-reserved chords (Win+L, Win+Tab)** are consumed by Windows before any hook and cannot be injected via SendInput at all.
|
||||
7. **RDP minimized = `SendInput` denied.** Even though `quser` shows the remote session State=Active, minimizing the mstsc client detaches the session's input desktop. `GetForegroundWindow()` returns 0; `SendInput` returns `ACCESS_DENIED (5)`; tests that need synthetic input fail. **Same applies to: closed mstsc with X (Disconnected), local PC sleep (RDP TCP drops), remote screensaver/workstation lock, remote machine sleep.** Run `scripts/pt-session-diagnose.ps1` in pre-flight to detect, and see `references/environment-setup.md` for the full per-scenario table + `powercfg` setup commands the user should run before starting the agent. The agent should call `Test-PtForeground` mid-run before each input-injection-dependent item; if it returns False, mark `BLK-ENV` with mitigation citation (an environment block — not a product FAIL).
|
||||
8. **`winapp ui` arg-order quirk**: `winapp ui inspect --depth N -w $hwnd` may intermittently fail to parse `--depth` as Int64 if `-w` precedes it. **Put `-w $hwnd` AFTER `--depth N`** or as the first arg before any flag. If you see "Cannot bind argument" or numeric parse errors, swap the order and retry.
|
||||
9. **`winapp ui list-windows` line wrapping**: when window titles or process names are long, output may wrap a single window's `HWND <id>: "<title>" ... (proc, PID N)` across multiple lines, breaking single-line regexes. Either pipe through `Out-String` and use a multi-line regex, or use `--json` (when supported) and parse structured output.
|
||||
10. **De-elevation: launching a NON-elevated (Medium IL) child from an elevated agent shell.** The drive-stack only covers gaining *more* privilege; some items need the opposite. From a High-IL shell you cannot `Start-Process` a Medium-IL child directly. Use `scripts/pt-nonelevated.ps1` (`Start-PtNonElevated` / `Invoke-PtNonElevatedCapture`) — a one-shot `RunLevel Limited` + `LogonType Interactive` scheduled task that lands on the user's desktop at their filtered token. Confirm with `Test-ProcessElevated`. Needed for elevation-visibility pairs (File Locksmith L649/L650: non-elevated FL must not see the elevated runner; elevated FL must).
|
||||
11. **Win11 packaged context menus are not observable without real Explorer.** Modern PT context-menu entries are packaged `IExplorerCommand`s (sparse MSIX, e.g. File Locksmith CLSID `{AAF1E27D-…}`). They are **NOT** enumerable via classic `Shell.Application … FolderItem.Verbs()` and **NOT** `CoCreate`-able from a non-Explorer host (`REGDB_E_CLASSNOTREGISTERED`). So "verify the entry appears / no longer appears" cannot be pixel-verified by API. Verify instead via the gate flag the entry's `GetState` reads (e.g. general `enabled.<Module>`) + a source citation that maps it to `ECS_HIDDEN`; treat the literal render as `BLK-VISUAL-RENDER` and recommend a 5-second manual right-click. (Disabling does NOT unregister the package — it stays `Status Ok`; the entry is hidden dynamically.)
|
||||
12. **Shell-extension modules read a module-OWNED settings file, NOT the PT-store `<Module>\settings.json`.** PowerRename, File Locksmith, Image Resizer, and New+ context-menu handlers and exes run *outside* the runner (hosted by Explorer / launched on demand) and cannot use the PT-Settings IPC. Each reads its **own** json in `%LOCALAPPDATA%\Microsoft\PowerToys\<Module>\` *at process/handler launch* (registry-migrated `CSettings`/`Settings` classes — `lib/Settings.cpp` `Load→ParseJson`). The PT-Settings UI writes the *PT-store* `settings.json` (the `bool_*`/`int_*` file `Get-PtModuleSettings` reads); the runner's module DLL syncs PT-store→module-store **only on a Settings-UI change event** — so the PT-store file can be **stale for days** and editing it has **no effect** on the running shell handler. **To drive a settings item on these modules, edit the module-owned file directly (drive-stack §2.A) and relaunch the module (or restart runner+Explorer for the menu handlers), then restore.**
|
||||
|
||||
**Pitfall #12 — module-owned files + their key style** (verified 2026-06-10 against `<PT-repo>\src`):
|
||||
|
||||
| Module | Module-owned file (under `…\PowerToys\<Module>\`) | Key style | PT-store `settings.json` keys (UI/`Get-PtModuleSettings`) |
|
||||
|---|---|---|---|
|
||||
| PowerRename | `power-rename-settings.json` (+ `power-rename-last-run-data.json`, `search-mru.json`, `replace-mru.json`) | `ShowIcon`, `ExtendedContextMenuOnly`, `PersistState`, `MRUEnabled`, `MaxMRUSize`, `UseBoostLib` | `bool_show_icon_on_menu`, `bool_show_extended_menu`, `bool_persist_input`, `bool_mru_enabled`, `int_max_mru_size`, `bool_use_boost_lib` |
|
||||
| File Locksmith | `file-locksmith-settings.json` | `showInExtendedContextMenu` | `bool_show_extended_context_menu` |
|
||||
| Image Resizer | `image-resizer-settings.json` | (resize sizes/encoder/etc.) | mirrored `imageresizer*` keys |
|
||||
| New+ | `NewPlus\settings.json` (sub-folder **`NewPlus`**, verified on disk + `constants.h` `powertoy_name=L"NewPlus"`) | `HideFileExtension`, `HideStartingDigits`, `TemplateLocation`, `ReplaceVariables`, `BuiltInNewHidePreference` | mirrored `newplus*` keys |
|
||||
|
||||
Confirm which file actually drives behavior with a quick A/B: edit the module-owned file → relaunch → observe; if behavior follows, that's the source of truth (PowerRename L394/L395/L396/L397/L409 were all driven this way).
|
||||
|
||||
13. **Foreground the window under test, and close per-test Explorer windows — for clean recordings AND reliable screenshots.** When a module UI opens (e.g. `PowerToys.PowerRename.exe` launched with file args, or the Settings window), it can open **behind** a previously-opened Explorer/temp folder that still holds the foreground z-order, so a human watching / a screen recording sees the wrong window. Two complementary rules (**do both**):
|
||||
- **(a) Foreground the target before you observe/screenshot/drive it.** Call `Force-PtForeground -AppId <appId>` (`scripts/pt-foreground-guard.ps1`) right after the window appears — e.g. `Force-PtForeground -AppId PowerToys.PowerRename` after `Start-Process …PowerToys.PowerRename.exe`. `winapp ui screenshot -w <hwnd>` captures by HWND so it *technically* works when the window is buried, but a recording only shows the top window, and SendInput / coordinate-click fallbacks need real foreground anyway. This is the **primary** fix. (CmdPal's AppX foreground-lock is the exception — see `references/modules/command-palette.md`; use `Assert-PtForegroundOrAbort`.)
|
||||
- **(b) Close each Explorer window when its item's menu tests end.** The "open one menu per fresh Explorer window" rule (context-menu flow) piles up windows that fight for z-order and clutter the capture. Track the HWNDs you open and close them **per item** (not only in global cleanup): `(New-Object -ComObject Shell.Application).Windows() | Where-Object { $_.LocationURL -match '<your-fixture-tag>' } | ForEach-Object { $_.Quit() }`. Match your disposable-fixture folder name so you never close the user's own Explorer windows.
|
||||
|
||||
If you find another gap during verification, update this skill (add a recipe) AND consider proposing the addition to references/winapp-ui-testing.md if it's generic enough.
|
||||
@@ -1,143 +0,0 @@
|
||||
# Environment setup for PowerToys verification
|
||||
|
||||
**Audience**: human user preparing a test machine before running a verification agent.
|
||||
**One-time** (per test session) — restore afterward.
|
||||
|
||||
## Why this matters
|
||||
|
||||
PowerToys release checklists test real user interactions: pressing hotkeys, dragging files, switching windows. Many tests use `SendInput` to inject keystrokes. Windows refuses `SendInput` when the calling session has **no attached input desktop** — and several common Windows states cause exactly that to happen:
|
||||
|
||||
- RDP client minimized
|
||||
- Workstation locked (screensaver kicked in, idle timeout)
|
||||
- Remote machine asleep
|
||||
- Local machine asleep (RDP TCP drops)
|
||||
|
||||
If any of these happens mid-verification, items that need synthetic input fail with `BLK-ENV` even though the feature itself works fine. This guide eliminates the env causes so the only BLOCKED verdicts you see are real test/framework limitations.
|
||||
|
||||
## Per-scenario reference table
|
||||
|
||||
| Scenario | Remote session State | `GetForegroundWindow()` | `SendInput` | Verdict for input-injection tests |
|
||||
|---|---|---|---|---|
|
||||
| mstsc window focused | Active | Real HWND | Works | ✅ Drivable |
|
||||
| mstsc visible but not focused (covered or alt-tabbed) | Active | Real HWND | Works | ✅ Drivable |
|
||||
| **mstsc MINIMIZED** | Active | **0** | **ACCESS_DENIED (5)** | ❌ BLK-ENV |
|
||||
| Local machine sleeps / RDP TCP drops | **Disconnected** | 0 | ACCESS_DENIED | ❌ BLK-ENV |
|
||||
| User closes mstsc with X (no signout) | **Disconnected** | 0 | ACCESS_DENIED | ❌ BLK-ENV |
|
||||
| Sign out from the remote | Session destroyed | — | — | ❌ Agent killed |
|
||||
| Remote machine sleeps | Suspended | — | — | ❌ Catastrophic — timing corruption |
|
||||
| Remote screensaver / auto-lock kicks in | Active but desktop locked | 0 | ACCESS_DENIED | ❌ BLK-ENV |
|
||||
| **2nd RDP login as the SAME user** (you reconnect from another client) | the OLD session flips to **Disconnected** | 0 (in the old session) | ACCESS_DENIED | ❌ BLK-ENV — your running test's session got taken over |
|
||||
|
||||
**Key insight**: "Active" in `quser` ≠ "can inject input". Always check `GetForegroundWindow()` first (the diagnostic script `scripts/pt-session-diagnose.ps1` does this).
|
||||
|
||||
## Can I verify two modules at once in two RDP sessions?
|
||||
|
||||
Short answer on a **client edition of Windows (Windows 10/11, ProductType=1)**: **no — not as the same user, and effectively not at all.** This was investigated live on this machine (Windows 11 Enterprise, build 26200, `fSingleSessionPerUser=1` default):
|
||||
|
||||
- **Two monitors ≠ two sessions.** A multi-monitor setup is **one** session spanning both screens — it shares a single input desktop, foreground window, and `SendInput` queue across the monitors. Monitor count has nothing to do with session count, so "I have two monitors" does not give you two sessions to run two modules in.
|
||||
- **Sessions are isolated** — each Windows session has its own input desktop, its own foreground window, and its own `SendInput` queue. So *typing in session B genuinely does NOT disturb session A's foreground or input.* Cross-session interference is **not** the problem (so if you somehow DID have two live sessions — Server/RDS — they could run in parallel without colliding).
|
||||
- **The real blocker is session takeover.** Client Windows allows only **one interactive (console/owning) session at a time**, and `fSingleSessionPerUser=1` (the default) means one user gets **one** session. When you open the *second* RDP connection (as the same user), Windows **disconnects the first session** — it flips to `Disconnected`, its input desktop detaches, `GetForegroundWindow()` → 0, and any in-flight UI test there fails with `ACCESS_DENIED` → BLK-ENV. It's not your *typing* that breaks the test; it's the act of logging in the second session that evicts the first.
|
||||
- A different *user* account doesn't rescue it either: client Windows still permits only one connected interactive session, so the second login still disconnects the first.
|
||||
- Therefore, on client Windows, **run modules serially in one session.** True concurrent multi-session needs Windows Server + the RDS (Remote Desktop Session Host) role; unofficial multi-session patches exist but are out of scope here.
|
||||
|
||||
> **Verdict on the common assumption "I can run two modules in two RDP sessions because I have two monitors":** the *conclusion* (can't run two at once on client Windows) is correct, but the *reasoning* is wrong on two counts — two monitors is still one session, and you can't get two simultaneously-Active sessions on client Windows at all (the 2nd login disconnects the 1st). The limit is "can't open a 2nd Active session", not "the two sessions fight each other".
|
||||
|
||||
**Practical guidance:** keep a single RDP session for the whole run; don't reconnect/relogin mid-run; if you must check something elsewhere, alt-tab inside the *same* session rather than opening a new RDP connection. To detect a takeover after the fact, `qwinsta` will show your former session as `Disconnected`.
|
||||
|
||||
## Pre-run setup checklist
|
||||
|
||||
Run these BEFORE starting the verification agent.
|
||||
|
||||
### On the test machine (the one being verified)
|
||||
|
||||
```powershell
|
||||
# Snapshot current power settings so you can restore after
|
||||
$bk = "$env:TEMP\powercfg-backup-$(Get-Date -f yyyyMMdd-HHmmss).txt"
|
||||
powercfg /query SCHEME_CURRENT SUB_SLEEP > $bk
|
||||
powercfg /query SCHEME_CURRENT SUB_VIDEO >> $bk
|
||||
"# Restore later with the values from $bk" | Set-Content "$bk.note"
|
||||
|
||||
# Disable sleep + display-off + hibernate (AC and battery)
|
||||
powercfg /change standby-timeout-ac 0
|
||||
powercfg /change standby-timeout-dc 0
|
||||
powercfg /change monitor-timeout-ac 0
|
||||
powercfg /change monitor-timeout-dc 0
|
||||
powercfg /change hibernate-timeout-ac 0
|
||||
powercfg /change hibernate-timeout-dc 0
|
||||
|
||||
# Disable screensaver
|
||||
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '0'
|
||||
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '0'
|
||||
|
||||
# Disable workstation lock-on-idle (requires admin)
|
||||
# 0 = never lock. Restore your original value (commonly 600 = 10 min) afterward.
|
||||
$origLock = (Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name InactivityTimeoutSecs -EA SilentlyContinue).InactivityTimeoutSecs
|
||||
"$origLock" | Out-File "$bk.lock"
|
||||
Set-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name InactivityTimeoutSecs -Value 0 -EA SilentlyContinue
|
||||
|
||||
# Confirm
|
||||
powercfg /query SCHEME_CURRENT SUB_SLEEP | Select-String 'Power Setting GUID|Current AC Power Setting Index'
|
||||
```
|
||||
|
||||
### On the local machine (the one with the RDP client)
|
||||
|
||||
```powershell
|
||||
# Disable local sleep so RDP TCP stays alive
|
||||
powercfg /change standby-timeout-ac 0
|
||||
|
||||
# Practical habit: put mstsc on a monitor you're NOT actively working on.
|
||||
# Don't minimize. Alt-tab is fine; minimize is not.
|
||||
```
|
||||
|
||||
## Mid-run discipline
|
||||
|
||||
While the agent is running:
|
||||
- **Don't minimize mstsc.** Visible-but-unfocused is OK; minimized is not.
|
||||
- **Don't close mstsc with the X.** If you have to step away, fine — leave it open.
|
||||
- **Don't disconnect or reconnect RDP.** Stay continuously connected for the duration of the run.
|
||||
- **Don't sign out** on either end.
|
||||
- If you do step away and the screen locks (despite the setup above), reconnect/unlock and the agent's `Test-PtSessionStillInteractive` guard (if used) will resume; otherwise items mid-execution will be BLK-ENV.
|
||||
|
||||
## Post-run cleanup (restore)
|
||||
|
||||
```powershell
|
||||
# Restore the values you captured to $bk before starting
|
||||
# (e.g. typical defaults: standby 30min, monitor 15min, screensaver 600s, lock 600s)
|
||||
powercfg /change standby-timeout-ac 30
|
||||
powercfg /change standby-timeout-dc 15
|
||||
powercfg /change monitor-timeout-ac 15
|
||||
powercfg /change monitor-timeout-dc 10
|
||||
powercfg /change hibernate-timeout-ac 0 # often default
|
||||
|
||||
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '1'
|
||||
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '600'
|
||||
|
||||
$origLock = Get-Content "$bk.lock" -EA SilentlyContinue
|
||||
if ($origLock) {
|
||||
Set-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' `
|
||||
-Name InactivityTimeoutSecs -Value ([int]$origLock) -EA SilentlyContinue
|
||||
}
|
||||
```
|
||||
|
||||
(Values above are typical; adjust to your environment policy.)
|
||||
|
||||
## Diagnostic before you start
|
||||
|
||||
Run `scripts/pt-session-diagnose.ps1` from the agent shell. Expected output for a GO:
|
||||
|
||||
```
|
||||
PASS - this shell can drive interactive PowerToys tests.
|
||||
```
|
||||
|
||||
If it prints FAIL with a `psexec -i <consoleSession> -s pwsh.exe` hint, you're in a non-console session — relaunch the agent shell as suggested before starting verification.
|
||||
|
||||
## Why this isn't in the global SKILL.md
|
||||
|
||||
These are **human prep steps**, not agent instructions. The agent needs to *detect* a bad environment (via `Test-PtInteractiveDesktop` in pre-flight + `Test-PtSessionStillInteractive` mid-run); the user needs to *prevent* one. Different audiences, different docs.
|
||||
|
||||
## Related
|
||||
|
||||
- `scripts/pt-session-diagnose.ps1` — one-shot session diagnostic
|
||||
- `scripts/pt-foreground-guard.ps1` — `Test-PtForeground` / `Force-PtForeground` / `Assert-PtForegroundOrAbort` used by agent
|
||||
- `SKILL.md` pitfall #7 — short pointer to this doc
|
||||
- `references/pre-flight.md` pre-flight check #4 — agent reads this doc when it detects a bad env
|
||||
@@ -1,226 +0,0 @@
|
||||
# Explorer context-menu flow — driving PowerToys shell-menu modules end-to-end
|
||||
|
||||
**Audience**: agents verifying any PowerToys module whose entry point is the **Windows Explorer right-click context menu** — i.e. **File Locksmith, Image Resizer, PowerRename, New+ (NewPlus)**, and similar.
|
||||
|
||||
This is the *true user flow*: open Explorer → select file(s) → right-click → click the module's menu item. Use it when an item's assertion is specifically about the **context menu** (e.g. "the entry appears / no longer appears", "right-click → X launches the module on the selection"). For the module's *internal* behavior you can still prefer a faster back-door (CLI / `last-run.log` / Named Event) — see each module profile — but the menu presence/launch itself can only be observed this way.
|
||||
|
||||
All the machinery lives in **`scripts/pt-explorer-contextmenu.ps1`** — dot-source it and call the functions; this doc only shows the invocations, not re-implementations. Functions:
|
||||
`Test-PtDesktopInteractive`, `Open-PtExplorerWindow`, `Open-PtBackgroundContextMenu`, `Open-PtExplorerContextMenu`, `Open-PtShowMoreOptionsMenu`, `Open-PtShiftRightClickMenu`, `Expand-PtModernSubmenu`, `Get-PtContextMenuItems`, `Invoke-PtContextMenuItem`, `Set-PtModuleEnabledViaSettingsUI`.
|
||||
|
||||
**One pattern for every menu:** an `Open-Pt…ContextMenu` opener returns a menu HWND; then `Get-PtContextMenuItems -MenuHwnd <h>` reads it and `Invoke-PtContextMenuItem -MenuHwnd <h> -ItemName <n>` acts on it — identically for the background, file, and legacy `#32768` menus.
|
||||
|
||||
## Which approach first? (CLI / back-door vs synthetic menu)
|
||||
|
||||
**Pick the tool by what the item ASSERTS — not "always synthetic" or "always CLI".**
|
||||
|
||||
| The item asserts… | First approach | Why |
|
||||
|---|---|---|
|
||||
| **The menu itself** — entry *appears / no longer appears*, "right-click → select X", caption / localization of the entry | **Synthetic Explorer menu (this doc)** — the *only* valid observer | The CLI/back-door is **blind to the menu**: it runs even when the entry is correctly hidden, so it gives a false PASS (the L652 trap). If the desktop is locked → `BLK-ENV`; do **not** substitute the CLI. |
|
||||
| **Module behavior** — engine finds the lockers, images get resized, files get renamed (the menu is just the trigger) | **CLI / back-door** (`FileLocksmithCLI.exe`, `last-run.log`, Named Event, DSC) | Instant, deterministic, foreground-free, works on a locked desktop. Synthetic adds ~10s + foreground/retry fragility without changing the assertion. |
|
||||
|
||||
**Golden-path rule (do once per module):** run **one** full synthetic right-click → invoke-the-item → confirm-launch. That proves the menu→launch wiring is actually registered *and* validates that the fast back-door is behaviorally equivalent to the real menu (e.g. File Locksmith L641 `step-04/05` did exactly this). After that one golden run, trust the back-door for the remaining behavior items.
|
||||
|
||||
Net: for a context-menu module, **most items are behavior → CLI-first**; the **menu-presence/absence/launch/localization items → synthetic-first**; plus one golden-path synthetic launch.
|
||||
|
||||
**Prefer selecting a real file over the folder-background menu for present/absent/icon observation.** Right-clicking a selected file (`Open-PtExplorerContextMenu -FileName <f>`) gives a **deterministic, verifiable** target (`Document.SelectedItems()`) and a stable ITEM menu, whereas the background menu depends on "focus in the file-list + nothing selected". Use `Open-PtBackgroundContextMenu` only when the entry lives **only** on the folder background (New+), or when specifically asserting background-menu presence. Two rules for either menu: **open one menu per fresh window**, and **assert the menu TYPE + focus, not module presence** — a wrong-menu open (tree node / preview pane) can still be a populated menu, so "module entry absent" alone doesn't tell you the right menu opened. Also note the Win11 menu **populates asynchronously**: base verbs (Open/Cut/Copy) render first; 3rd-party packaged entries (PowerRename, File Locksmith) land a beat later and lower — a too-early read can miss them. **Let the menu settle (~0.5s) and/or re-poll before asserting a module entry is absent.**
|
||||
|
||||
## Is it stable?
|
||||
|
||||
**Yes — with the robust variant below.** Two rules make it reliable; ignore them and it gets flaky:
|
||||
|
||||
1. **Invoke the menu item by UIA InvokePattern, not a coordinate left-click.** The menu item exposes `InvokePattern` (`isInvokable=True`). `winapp ui invoke <selector> -w <menuHwnd>` is robust and needs no foreground/coordinates for the *click*. A synthetic left-click at the item's pixel center also works but is the fragile part (DPI, menu repositioning near screen edges, scrolled menus).
|
||||
2. **The OPEN step is coordinate-free too — Shift+F10 on the COM-selected item.** `Open-PtExplorerContextMenu` COM-selects the target (deterministic, verifiable via `Document.SelectedItems()`), restores keyboard focus to it (`winapp ui focus`), then presses **Shift+F10** — no pixel is clicked. A coordinate `winapp ui click --right` is kept only as a fallback (its resolved point can miss with an open preview pane / DPI change / wide Details row / screen-edge or scrolled row). Two rules keep the coordinate-free path reliable: (a) **open one menu per fresh Explorer window** — recycling a single window through many open→Esc cycles can drop later menus (menu-state decay); (b) **keyboard focus must be in the file-list** (not the nav tree / address bar), which the helper enforces via `winapp ui focus` — otherwise Shift+F10 opens the *focused pane's* menu instead (e.g. the OneDrive tree node's menu, where PowerRename is legitimately absent but File Locksmith shows — a false "entry missing"). Confirm with `winapp ui get-focused`.
|
||||
|
||||
**Hard prerequisite — unlocked interactive desktop.** Synthetic right-click injects into the session input stream, so it requires foreground. If the workstation is locked / RDP minimized (`GetForegroundWindow()=0`), this flow is `BLK-ENV` — there is no foreground-free way to open a context menu. `Open-PtExplorerContextMenu` throws a clear BLK-ENV error in that case. (A 4-hour idle auto-lock is the common culprit — see `references/environment-setup.md`.)
|
||||
|
||||
**Window hygiene when opening one window per menu (pitfall #13).** Because you open a **fresh Explorer window per menu**, and a module UI launched from the menu (or via CLI) can appear **behind** those windows, close each Explorer window when the item's menu tests end — and `Force-PtForeground -AppId <moduleAppId>` the launched UI before observing/screenshotting. This keeps a recording on the right window and stops stale windows piling up in the z-order. Close by matching your disposable-fixture folder name so you never quit the user's own Explorer windows: `(New-Object -ComObject Shell.Application).Windows() | Where-Object { $_.LocationURL -match '<fixture-tag>' } | ForEach-Object { $_.Quit() }`.
|
||||
|
||||
**Other constraints:**
|
||||
- **Settings for these modules live in a module-OWNED file, not the PT-store `settings.json`** — see `SKILL.md` pitfall #12. The context-menu handler reads e.g. `power-rename-settings.json` / `file-locksmith-settings.json` / `image-resizer-settings.json` / `NewPlus\settings.json` at launch; editing the PT-store `<Module>\settings.json` (what `Get-PtModuleSettings` reads) often has **no effect** on the live handler. Drive icon/extended-menu/feature toggles via the module-owned file + relaunch (restart runner+Explorer for the menu handlers), then restore.
|
||||
- This is the **Win11 packaged** context menu (`Microsoft.UI.Content.PopupWindowSiteBridge` / "PopupHost"). The packaged module commands appear **only** here — not in classic `Shell.Application.Verbs()` and not via `CoCreate` of the command CLSID (`REGDB_E_CLASSNOTREGISTERED`). On Win10, or under "Show more options", you'd get the classic menu instead (different structure) — see **[Reading the legacy "Show more options" (`#32768`) menu](#reading-the-legacy-show-more-options-32768-menu)** below.
|
||||
- The menu exists in the UIA tree **only while open** — you must open it with real input first; you can't enumerate it cold.
|
||||
- A menu-launched module UI runs **non-elevated** (Explorer's integrity), even if your agent shell is elevated. Mind elevation-visibility (e.g. a non-elevated File Locksmith can't see higher-IL processes — match locker integrity with `scripts/pt-nonelevated.ps1`).
|
||||
|
||||
## Recipe (robust)
|
||||
|
||||
```powershell
|
||||
. "$skill\scripts\pt-explorer-contextmenu.ps1"
|
||||
|
||||
# 0) Guard: must be an unlocked desktop
|
||||
if (-not (Test-PtDesktopInteractive)) { <# mark BLK-ENV, cite references/environment-setup.md #> }
|
||||
|
||||
# 1) Open Explorer on the target folder and get its CabinetWClass HWND
|
||||
$hwnd = Open-PtExplorerWindow -Path $dir
|
||||
|
||||
# 2) Open the real context menu (synthetic right-click, auto-retry)
|
||||
$menu = Open-PtExplorerContextMenu -ExplorerHwnd $hwnd -FileName 'target.txt'
|
||||
|
||||
# 3a) ASSERT PRESENCE / ABSENCE (e.g. "entry no longer appears" when the module is disabled)
|
||||
$present = (Get-PtContextMenuItems -MenuHwnd $menu) -contains 'Unlock with File Locksmith'
|
||||
|
||||
# 3b) LAUNCH the module via the real menu (UIA invoke by NAME — robust)
|
||||
$ok = Invoke-PtContextMenuItem -MenuHwnd $menu -ItemName 'Unlock with File Locksmith'
|
||||
|
||||
# 4) Verify the module launched (its process/window appears) — e.g.:
|
||||
Start-Sleep 4
|
||||
$ui = Get-Process PowerToys.FileLocksmithUI -EA SilentlyContinue # or PowerToys.ImageResizer, PowerToys.PowerRename
|
||||
```
|
||||
|
||||
To **assert absence** after disabling a module: re-open the menu and check `Get-PtContextMenuItems` no longer contains the caption (the packaged `GetState` re-reads the enabled flag live, so no Explorer restart is needed between toggles).
|
||||
|
||||
## Enabling / disabling the module — via the Settings-UI toggle
|
||||
|
||||
For any *"check enable/disable of the module works"* item, flip the module through the **real Settings
|
||||
UI enable switch**. Two reasons the UI toggle is the correct method:
|
||||
|
||||
1. **It's the faithful user flow** the checklist is describing (a user flipping the module's switch),
|
||||
and it exercises the **Settings → runner IPC enable/disable path**.
|
||||
2. **It takes effect live** — the context-menu entry appears/disappears the instant you flip the
|
||||
toggle, no runner restart. For New+ the toggle additionally
|
||||
runs the enable-time `CopyTemplateExamples` that seeds the default templates (see
|
||||
`references/modules/new-plus.md`).
|
||||
|
||||
Use the shipped helper **`Set-PtModuleEnabledViaSettingsUI`** (`scripts/pt-explorer-contextmenu.ps1`).
|
||||
It opens Settings straight on the module page via `--open-settings=<tag>`, discovers the enable
|
||||
ToggleSwitch by its `[on]/[off]` state (the AutomationId carries a per-session suffix — never
|
||||
hard-code it), flips it only if needed, and cross-checks the `enabled.<key>` flag for evidence:
|
||||
|
||||
```powershell
|
||||
. "$skill\scripts\pt-explorer-contextmenu.ps1"
|
||||
try {
|
||||
$r = Set-PtModuleEnabledViaSettingsUI -PageTag PowerRename -Enabled $false -EnabledKey PowerRename
|
||||
# $r.State -eq 'off', $r.EnabledFlag -eq $false → now assert the menu entry is GONE
|
||||
$menu = Open-PtBackgroundContextMenu -ExplorerHwnd $hwnd
|
||||
$gone = -not ((Get-PtContextMenuItems -MenuHwnd $menu) -match 'Rename with PowerRename')
|
||||
}
|
||||
finally {
|
||||
Set-PtModuleEnabledViaSettingsUI -PageTag PowerRename -Enabled $true -EnabledKey PowerRename # restore
|
||||
# close the Settings window the helper opened, e.g. winapp ui invoke btn-close-<id> -w $r.SettingsHwnd
|
||||
}
|
||||
```
|
||||
|
||||
Per-module arguments (`-PageTag` for `--open-settings`, `-EnabledKey` = the `enabled.<key>` to read
|
||||
back as evidence — note the key uses the **display name with spaces**):
|
||||
|
||||
| Module | `-PageTag` | `-EnabledKey` | Menu caption to assert |
|
||||
|---|---|---|---|
|
||||
| PowerRename | `PowerRename` | `PowerRename` | `Rename with PowerRename` |
|
||||
| File Locksmith | `FileLocksmith` | `File Locksmith` | `Unlock with File Locksmith` |
|
||||
| Image Resizer | `ImageResizer` | `Image Resizer` | `Resize with Image Resizer` |
|
||||
| New+ | `NewPlus` | `NewPlus` | `New+` |
|
||||
|
||||
> The toggle needs an unlocked interactive desktop. If the desktop is locked / RDP-minimized so the
|
||||
> Settings UI can't be driven, mark the item **`BLK-ENV`** (cite `references/environment-setup.md`).
|
||||
|
||||
|
||||
## Which menu — decided per module (see the module profile)
|
||||
|
||||
**Whether a module's entry lives on the file menu, the folder-background menu, or both is
|
||||
module-specific — it is recorded in each `references/modules/<module>.md` (the module's
|
||||
"Context-menu routing" note), not here.** This doc only provides the openers and the machinery;
|
||||
the module profile says which opener to call and the exact caption to match.
|
||||
|
||||
## The openers & operations — the machinery
|
||||
|
||||
**Openers** (each returns a menu HWND):
|
||||
|
||||
- **`Open-PtExplorerWindow -Path <dir>`** — opens Explorer on a folder and returns its `CabinetWClass`
|
||||
HWND (int), the handle the menu openers below take. (Discovery via `list-windows`; polls until the
|
||||
window appears.)
|
||||
- **`Open-PtBackgroundContextMenu -ExplorerHwnd <h>`** — folder-background menu via **Shift+F10 with
|
||||
nothing selected**. Coordinate-free; it first **focuses the file-list** (`winapp ui focus` on the
|
||||
"Items View" list) so Shift+F10 can't open the nav-tree / address-bar menu instead, then *validates* it
|
||||
opened a background menu (View/Sort by/Group by) or throws. Use for **background-only** entries (New+).
|
||||
- **`Open-PtExplorerContextMenu -ExplorerHwnd <h> -FileName <name>`** — a specific file's menu (the
|
||||
**preferred** opener for present/absent/icon). It COM-selects the item (reliable, never opens/renames it),
|
||||
restores keyboard focus to it, then opens the menu **coordinate-free via Shift+F10** (fallback:
|
||||
`winapp ui click --right`, then a coordinate right-click on the row point), and **validates it got a FILE
|
||||
menu (Open/Cut/Copy/Delete)** — if it only got the background menu it retries, then **throws** rather than
|
||||
passing a wrong menu off as the file menu.
|
||||
- **`Open-PtShowMoreOptionsMenu -MenuHwnd <modernMenu>`** — from an open modern menu, expands "Show more
|
||||
options" and returns the legacy `#32768` menu's HWND (**non-extended** verbs). See [Reading the legacy menu](#reading-the-legacy-show-more-options-32768-menu).
|
||||
- **`Open-PtShiftRightClickMenu -ExplorerHwnd <h> -FileName <name>`** — opens the **extended** `#32768`
|
||||
menu (`CMF_EXTENDEDVERBS`) via a **genuine Shift+right-click** on the COM-selected file, and returns its
|
||||
HWND. **Required** for any *"Extended context menu only"* entry — "Show more options" won't show those.
|
||||
|
||||
**Operations** (take any menu HWND from the openers above — modern *or* classic):
|
||||
|
||||
- **`Get-PtContextMenuItems -MenuHwnd <h>`** — returns the menu's item captions (present/absent).
|
||||
- **`Invoke-PtContextMenuItem -MenuHwnd <h> -ItemName <name>`** — invokes an entry by caption (`$false` = absent).
|
||||
|
||||
> **The OPEN step is now coordinate-free.** `Open-PtExplorerContextMenu` opens the file menu with
|
||||
> **Shift+F10 on the COM-selected, focus-restored item** — no pixel is clicked. The old coordinate `winapp ui click --right` is retained only as a fallback; in an *adverse*
|
||||
> layout (open **preview pane** / DPI change / wide Details row / scrolled or screen-edge row) its resolved
|
||||
> point can miss and open the background menu. The helper **fails loudly** rather than passing the
|
||||
> background menu off as the file menu. Mitigations if even the fallbacks miss: close the preview pane
|
||||
> (Alt+P) for a deterministic layout, or mark the item `BLK-ENV` — never accept the background menu as a
|
||||
> stand-in for the file menu.
|
||||
|
||||
## Reading the legacy "Show more options" (`#32768`) menu
|
||||
|
||||
The two openers above (and `Get-PtContextMenuItems`) only reach the **modern** Win11 menu
|
||||
(`PopupWindowSiteBridge`). The legacy `#32768` classic Win32 popup — what you get under **"Show more
|
||||
options"**, or on Win10 — has a different structure and needs a different read path. **There are two
|
||||
distinct ways in, and they are NOT interchangeable:**
|
||||
|
||||
| You need… | Opener | What it gives |
|
||||
|---|---|---|
|
||||
| The classic menu **without** extended verbs (e.g. "is X *also* in Show more options") | `Open-PtShowMoreOptionsMenu -MenuHwnd <modernMenu>` (invokes "Show more options") | `#32768` populated with `CMF_NORMAL` verbs. **Extended-verbs-only entries are (correctly) ABSENT here.** |
|
||||
| The **extended** menu (`CMF_EXTENDEDVERBS`) — REQUIRED for any *"Extended context menu only"* / "Appear only in extended menu" entry (e.g. PowerRename L394 combos 3–4) | `Open-PtShiftRightClickMenu -ExplorerHwnd <h> -FileName <f>` (**genuine Shift+right-click**) | `#32768` populated with normal **+ extended** verbs. |
|
||||
|
||||
> **Do not use "Show more options" to look for an extended-only entry.** `Show more options` does **not**
|
||||
> pass `CMF_EXTENDEDVERBS`, so an entry a module registered as extended-only is legitimately missing there —
|
||||
> reading it that way yields a false "absent". Only a real **Shift+right-click** sets the flag. Neither
|
||||
> keyboard `Shift+F10` nor `Show more options` yields extended verbs; the extended menu needs the Shift +
|
||||
> mouse right-click, which `Open-PtShiftRightClickMenu` performs (Shift held around an element-resolved
|
||||
> right-click on the COM-selected file), then resolves the `#32768` HWND.
|
||||
|
||||
**The one gotcha:** the `#32768` window is **not returned by `winapp ui list-windows`**. So
|
||||
`Get-PtContextMenuItems` (which discovers the menu HWND *from* `list-windows`) can't reach it, and
|
||||
neither can the modern openers above. That does **not** mean it's invisible to UIA — its item subtree is
|
||||
fully readable once you obtain the HWND, which both openers do via Win32
|
||||
`FindWindow('#32768', $null)`; then read it with `Get-PtContextMenuItems` / `winapp ui inspect -w <hwnd>` —
|
||||
an exact, locale-accurate present/absent signal, no OCR or screenshot guessing.
|
||||
|
||||
```powershell
|
||||
. "$skill\scripts\pt-explorer-contextmenu.ps1"
|
||||
|
||||
# (a) Non-extended classic menu — via "Show more options":
|
||||
$menu = Open-PtExplorerContextMenu -ExplorerHwnd $hwnd -FileName 'a.txt' # or Open-PtBackgroundContextMenu
|
||||
$classic = Open-PtShowMoreOptionsMenu -MenuHwnd $menu # returns the #32768 HWND (int)
|
||||
$items = Get-PtContextMenuItems -MenuHwnd $classic # SAME reader as the modern menu
|
||||
|
||||
# (b) EXTENDED menu — via a genuine Shift+right-click (use for extended-only entries):
|
||||
$ext = Open-PtShiftRightClickMenu -ExplorerHwnd $hwnd -FileName 'a.txt' # #32768 with CMF_EXTENDEDVERBS
|
||||
$items = Get-PtContextMenuItems -MenuHwnd $ext
|
||||
$present = $items -match 'Rename with PowerRename' # exact caption match
|
||||
|
||||
# Need to screenshot the classic menu? You already have its HWND:
|
||||
# winapp ui screenshot -w $classic -o <png>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- **A screenshot is corroborating evidence only**, not the verifier — `winapp ui screenshot -w $classic -o <png>` is fine to attach, but the pass/fail signal is the UIA MenuItem name text above.
|
||||
- **Duplicate entries are normal on Win11**: a module can surface **twice** in the classic menu (the packaged `IExplorerCommand` *and* the legacy classic `IContextMenu` handler both appear). For a present/absent assertion either satisfies it; don't treat the duplicate as a bug.
|
||||
|
||||
## Matching the caption
|
||||
|
||||
Match the module's entry by its **visible caption** (a string), NOT the AutomationId — Explorer
|
||||
assigns per-session numeric IDs like `32012` whose value/order varies. **The exact caption per module
|
||||
lives in that module's profile**; discover/confirm it at runtime with `Get-PtContextMenuItems`
|
||||
(enable the module, open the applicable menu, read the exact string), then hard-match it for
|
||||
present/absent assertions.
|
||||
|
||||
## Common failure modes → fixes
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `BLK-ENV: ... GetForegroundWindow()=0` | desktop locked / RDP minimized | unlock & keep mstsc un-minimized (`references/environment-setup.md`); mark `BLK-ENV`, not a test failure |
|
||||
| "popup not found after N attempts" | foreground not settled (esp. first right-click after Explorer opens) | the helper already retries 3×; raise `-MaxTries`, or pre-foreground the window once before calling |
|
||||
| menu item `invoke` returns but nothing launches | matched the wrong node / item disabled | match `type -eq 'MenuItem'` by exact Name; confirm the module is enabled |
|
||||
| caption not found though module enabled | wrong/old caption string, or it's only under "Show more options" (classic `#32768` menu — which `list-windows` can't see) | for the modern menu, `Get-PtContextMenuItems`; for the classic menu, `Open-PtShowMoreOptionsMenu` then the same `Get-PtContextMenuItems` — see [Reading the legacy menu](#reading-the-legacy-show-more-options-32768-menu) |
|
||||
| launched UI shows nothing | menu-launched UI is non-elevated and can't see higher-IL targets | match target integrity (`scripts/pt-nonelevated.ps1`) |
|
||||
|
||||
## Referenced by
|
||||
- `references/modules/file-locksmith.md` (L641/L652 — real right-click launch + menu present/absent)
|
||||
- *(future)* `references/modules/image-resizer.md`, `references/modules/power-rename.md`, `references/modules/new-plus.md` — reference this doc for their context-menu items.
|
||||
@@ -1,114 +0,0 @@
|
||||
# Per-module verification profiles (`references/modules/`)
|
||||
|
||||
This folder holds **one short profile per PowerToys module**. Each profile is self-contained guidance specific to that module — paths, entry-paths, capability/control recipes, common BLOCKED traps, fixture lists, source citations.
|
||||
|
||||
## When to read
|
||||
|
||||
When this skill runs for a specific module, check whether `references/modules/<module>.md` exists here. If yes: **read it BEFORE walking the SKILL.md drive-stack** — it tells you which entry-paths actually work for this module's quirks and which BLOCKED traps to avoid.
|
||||
|
||||
If no profile exists, fall back to SKILL.md + the helper scripts.
|
||||
|
||||
## Shared cross-module flows
|
||||
|
||||
Some flows are common to several modules and live in their own top-level docs (not per-module):
|
||||
- **`../references/explorer-context-menu-flow.md`** — driving the real Win11 Explorer right-click context menu end-to-end (open + assert present/absent + launch). Referenced by File Locksmith and any future **Image Resizer / PowerRename / New+** profiles.
|
||||
|
||||
## Why per-module (not just one big SKILL.md)
|
||||
|
||||
- Each module has its own quirks (Peek's `_isFromCli` guard, CmdPal's TextChanged-broken state, PT Run's mini-popup HWND, Workspaces' snapshot-elevation rules). Bundling all of them into the global SKILL.md bloats context and forces every verification to load 25+ KB of mostly-irrelevant text.
|
||||
- A profile lets a focused verification run with only the relevant 5-10 KB.
|
||||
- New gotchas discovered during a module verification round get added to that module's profile, not the global one — keeps the global doc stable.
|
||||
|
||||
## Profile catalog
|
||||
|
||||
| Module | Profile | Status |
|
||||
|---|---|---|
|
||||
| Peek | `peek.md` | ✅ written 2026-06-08 |
|
||||
| File Locksmith | `file-locksmith.md` | ✅ written 2026-06-08 |
|
||||
| Image Resizer | `image-resizer.md` | ✅ written 2026-06-09 |
|
||||
| PowerRename | `power-rename.md` | ✅ written 2026-06-10; **reworked 2026-07-08** — recipe reduced to *capability → control* + a Read-out notes block, self-contradiction & meta-hedges removed. Rows 4–12 are author-derived (control IDs discover-at-runtime); confirm on the next live run |
|
||||
| New+ | `new-plus.md` | ✅ written 2026-06-18 (registration-gate for menu presence; Settings-UI toggle drives template auto-copy) |
|
||||
| Command Palette | `command-palette.md` | ✅ written 2026-07-07 (CmdPal AppX foreground-lock / TextChanged-broken / alias-keystroke / Esc-filtered quirks — moved out of the global SKILL.md pitfalls) |
|
||||
| (other modules to be added as we encounter sign-off needs) | — | — |
|
||||
|
||||
## For Explorer-context-menu modules: read the canonical flow doc first
|
||||
|
||||
If you're writing a profile for a module that registers an entry in Explorer's Win11 right-click menu (PowerRename, File Locksmith, Image Resizer, New+, Preview Pane, RegistryPreview), **read `../references/explorer-context-menu-flow.md` first**. It has the canonical synthetic-right-click + UIA-invoke recipe with:
|
||||
|
||||
- Which-approach-first decision rule (CLI back-door vs synthetic menu, with the false-positive trap warning)
|
||||
- Stability rules (UIA InvokePattern, retry on first right-click)
|
||||
- Recipe (robust 5-step flow)
|
||||
- Multi-file selection notes
|
||||
- Module captions table (per-module menu-item display names)
|
||||
- Common failure modes
|
||||
- The unlocked-desktop requirement (BLK-ENV gating)
|
||||
|
||||
The shared helper is `scripts/pt-explorer-contextmenu.ps1` (`Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems`).
|
||||
|
||||
Your module profile then only documents the **module-specific** quirks: settings.json schema keys, expected verb caption regex, capability/control recipes, source citations, ceiling.
|
||||
|
||||
`power-rename.md` is the model — ~9 KB despite covering 18 items because the generic mechanics live in the canonical flow doc.
|
||||
|
||||
## Profile template
|
||||
|
||||
A profile holds **only module-specific logic** an agent can't infer from the SKILL engine. It has **4 required sections + 2 optional**. Do NOT pad it with sections that have no content — omit them. No Ceiling/Don'ts sections: a PASS-rate number drifts every release, and "don'ts" are just traps phrased negatively (put them in BLOCKED traps).
|
||||
|
||||
**Required (always):** ① metadata header · ② Entry-paths · ③ Recipes · ④ BLOCKED traps.
|
||||
**Optional (include only if non-empty):** Fixtures · Source citations.
|
||||
|
||||
```markdown
|
||||
# <Module> — module verification profile
|
||||
|
||||
# ① metadata header (REQUIRED) — bootstrap facts. Drop any line that doesn't apply.
|
||||
**PT module**: `<ModuleKey>` (one-line description)
|
||||
**Source**: `src\modules\<dir>\`
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\<dir>\settings.json`
|
||||
**Exe**: `<full path>`
|
||||
**Default hotkey**: `<keys>` (+ settings ActivationShortcut path)
|
||||
**Named Event**: `Local\<name>` (friendly name in pt-shared-events.ps1 catalog)
|
||||
**DSC resource**: `Microsoft.PowerToys/<Name>Settings`
|
||||
**Last verified**: `<build>` · `<date>` (bump whenever you re-drive the module)
|
||||
|
||||
## Entry-paths (try in order) # ② REQUIRED — how to launch & reach the UI, fastest first
|
||||
### 1. <fastest path> <code + when to use + source citation>
|
||||
### 2. <alternate path>
|
||||
### 3. <last-resort path>
|
||||
|
||||
## Recipes — control/observation map, NOT an answer key # ③ REQUIRED
|
||||
| # | Capability | Drive (control / settings key) | Observe (where result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | <module capability> | <AutomationId / control / settings key> | <preview / settings.json / disk / log / menu> |
|
||||
|
||||
> Mapping: read item → find capability row → drive the control, design your OWN inputs+assertions. No canned inputs/expected values (they go stale + invite copying). New capability ⇒ add a row.
|
||||
|
||||
## BLOCKED traps # ④ REQUIRED — false-block + gotcha prevention (absorbs old "Don'ts"/"gotchas")
|
||||
- <mistake prior agents made → the fix>; <module quirk that misleads driving>
|
||||
|
||||
## Fixtures # OPTIONAL — only if the module needs canned files (else omit)
|
||||
## Source citations # OPTIONAL — PT-repo file:line for surprising behavior (else inline in traps)
|
||||
```
|
||||
|
||||
## Hygiene
|
||||
|
||||
- **4 required + 2 optional sections only** (header · entry-paths · recipes · BLOCKED traps; fixtures + source citations if non-empty). No Ceiling, no Don'ts — fold negative guidance into BLOCKED traps. Omit empty sections rather than writing "None".
|
||||
- **Keep each profile under ~10 KB.** If it grows beyond that, the module has too many quirks — escalate to maintainer review of the upstream checklist.
|
||||
- **The recipe table is a capability → control MAP, not a mini-checklist.** Two attributes only: *Capability → Control (how to drive it)*. It must **not** carry inputs, expected outputs, or an `Observe` column — those overlap the checklist, which owns the inputs *and* the expected result. Put *where/how to read* an outcome in a short **Read-out notes** block under the table (a readout location/technique, never an expected value).
|
||||
- **Tables are capability-keyed, NOT line-keyed.** Upstream checklist line numbers (`L<n>`) **must not appear** — they drift between releases. PT-source-code citations should prefer **file + symbol** (a line number is fine only as an *as-of-build* hint).
|
||||
- **Cite source by file + symbol** (e.g. `Settings.cpp CSettings::Load`) where module behavior surprises (CLI guards, debounce timings, fallback chains) so reviewers can verify — symbols survive refactors, bare line numbers rot.
|
||||
- **Update the profile after every verification round**; promote any new technique into the right helper script if it generalizes beyond this module.
|
||||
|
||||
## Filling a profile: provenance & keeping it fresh
|
||||
|
||||
Profiles are written by an agent and may sit unreviewed, so a hallucination can read exactly like a verified truth (that's how `power-rename.md` ended up telling agents to read a non-existent HKCR `Icon` value, contradicting its own "modern-menu-only on Win11" thesis). Guard against that with **content discipline, not hedges.**
|
||||
|
||||
**1. State a fact, or give the discovery instruction — never hedge.** If a value is confirmed, state it plainly. If it's a guess or *volatile* (changes per launch/build — e.g. PowerRename's per-launch `txt-textbox-XXXX` IDs), **don't write the guessed value at all** — write the runtime-discovery instruction instead ("discover the AutomationId at runtime by name/role"). **Do NOT** add `[UNVERIFIED] — confirm yourself` tags, "review notes", or any commentary about the doc's own reliability: it doesn't help the consuming agent (which re-checks at runtime anyway) and makes it distrust otherwise-good content. If something's wrong, fix it; if it's uncertain, turn it into a discovery instruction.
|
||||
|
||||
**2. Fill from evidence, not speculation.** Don't AI-generate a whole profile up front (that's what produced the HKCR hallucination). **Seed** a thin one (metadata + a source scan for durable facts), then let **each run add** the controls/keys it actually confirmed, correct anything that failed, and append BLOCKED traps from that run's §G retrospective. The profile grows *out of* runs.
|
||||
|
||||
**3. Durable vs volatile.** Durable facts (settings-file path, "modern-menu-only on Win11", the two-settings-files gotcha) belong here as plain statements. Volatile facts (per-launch/per-build IDs, layout) are **discovered at runtime**, never pinned.
|
||||
|
||||
**4. State each fact once.** Duplication is the #1 staleness amplifier: when a fact lives in the recipe table, an entry-path, and three BLOCKED traps, one edit leaves the others stale → contradiction. Shared mechanics go in the cross-flow doc; module facts once; everything else cross-references.
|
||||
|
||||
**5. Prefer symbol over line for source citations.** `Settings.cpp CSettings::Load` survives a refactor; `Settings.cpp:307` rots on the next edit. A line number is fine only as an *as-of-build* hint.
|
||||
|
||||
**6. Freshness.** The only provenance marker a profile carries is a compact `**Last verified**: <build> (<date>)` line in the header — no per-fact date tags in the body. Treat a profile as stale after ~2 releases or on a detected UI redesign; it should get **one human review** before it's trusted (necessary, not sufficient — the same discipline the skill applies to checklists). An optional run-start lint (do the named control IDs resolve? do the settings keys exist?) can flag drift.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Advanced Paste — module verification profile
|
||||
|
||||
**PT module**: `AdvancedPaste` (clipboard transform: plain/markdown/json + AI custom paste)
|
||||
**Source**: `src\modules\AdvancedPaste\`
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\AdvancedPaste\settings.json`
|
||||
**Exe**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.AdvancedPaste.exe`
|
||||
**Named Event**: `Local\PowerToys_AdvancedPaste_ShowUI` (friendly: `AdvancedPaste.ShowUI`)
|
||||
**Default UI hotkey**: Win+Shift+V (`advanced-paste-ui-hotkey`); paste-as-plain Win+Ctrl+Alt+V
|
||||
**Settings UI section keys**: IsAIEnabled, AutoCopySelectionForCustomActionHotkey, paste-as-{plain,markdown,json}-hotkey, additional-actions, custom-actions
|
||||
|
||||
## Entry-paths (try in order)
|
||||
1. Enable module: master `settings.json` `enabled.AdvancedPaste=true` + `Restart-PtRunner` (it ships DISABLED). Then `Invoke-PtSharedEvent -Name AdvancedPaste.ShowUI`. Window title "Advanced Paste", class WinUIDesktopWin32WindowClass.
|
||||
2. Paste options are a ListView: invoke `itm-pasteasplaintex-*` / `itm-pasteasmarkdown-*` / `itm-pasteasjson*` (suffix is dynamic — match by `Select-String 'pasteas...'`). They paste into the previously-focused window.
|
||||
|
||||
## Recipes — control/observation map
|
||||
| Capability | Drive | Observe |
|
||||
|---|---|---|
|
||||
| Strip formatting | invoke paste-as-plain ListItem | clipboard format-diff: `HTML Format` removed, `UnicodeText` kept |
|
||||
| Markdown convert | invoke paste-as-markdown | target app content (Ctrl+A,Ctrl+C read-back) |
|
||||
| JSON convert (CSV/XML) | invoke paste-as-json | target content read-back |
|
||||
| Color swatch | clip a `#RRGGBB` string, ShowUI | preview row shows swatch |
|
||||
| Clipboard history panel | enable Windows history, seed entries, invoke `btn-clipboardhistor-*` | `itm-*` history ListItems enumerable |
|
||||
| AI custom paste | `InputTxtBox` | disabled unless IsAIEnabled + provider key (BLK-EXTERNAL otherwise) |
|
||||
|
||||
## Common BLOCKED traps
|
||||
- IsAIEnabled=false ⇒ InputTxtBox/AIProviderButton `[disabled]` → all AI items BLK-EXTERNAL.
|
||||
- Clipboard-history button collapsed (`ShowClipboardHistoryButton`, MainPage.xaml:172) until Windows clipboard history is ON. Enable `HKCU\Software\Microsoft\Clipboard\EnableClipboardHistory=1` + seed entries first — it IS UIA-enumerable, not BLK-VISUAL-RENDER.
|
||||
- Notepad RichEdit isn't UIA-readable; read content via Ctrl+A/Ctrl+C then `Clipboard.GetText`.
|
||||
- Don't mark the clipboard-history panel BLK-VISUAL-RENDER: AP is WinUI 3 → fully UIA-capable. A control absent from the tree is Collapsed by a view-model flag, not opaque — enable its precondition + confirm via XAML `AutomationProperties` before blocking.
|
||||
|
||||
## Source citations
|
||||
- `src\modules\AdvancedPaste\` (settings keys above)
|
||||
- MainPage.xaml:172 `ShowClipboardHistoryButton`; :192 history ListView (AutomationProperties.Name per item)
|
||||
@@ -1,54 +0,0 @@
|
||||
# Command Palette (CmdPal) — module verification profile
|
||||
|
||||
**PT module**: `CmdPal` / display name **"Command Palette"** (WinUI 3 packaged launcher — search box + extensible result providers/aliases)
|
||||
**Source**: `src\modules\cmdpal\`
|
||||
**Settings**: via `Get-CmdPalSettings` (`scripts/pt-state.ps1`) — CmdPal keeps its **own** packaged-app store, **not** the PT-store `<Module>\settings.json` that `Get-PtModuleSettings` reads.
|
||||
**App identity**: packaged AppX, UIA AppId **`Microsoft.CmdPal.UI`**
|
||||
**Named Event**: friendly name **`CmdPal.Show`** in the `pt-shared-events.ps1` catalog (foreground-free open)
|
||||
**Helper**: **`scripts/pt-cmdpal-recycle.ps1`** — `Reset-CmdPalAppX`, `Reset-CmdPalToHome`, `Test-CmdPalDegraded`, `Invoke-CmdPalQuery` (CmdPal-specific lifecycle: TextChanged-broken recovery, BackButton navigation, AppX recycle)
|
||||
|
||||
> CmdPal is an **AppX WinUI 3** app, so it inherits the WinUI-island UIA limitations (SKILL.md pitfall #5)
|
||||
> and an AppX foreground-lock that makes external `SetForegroundWindow` unreliable (see BLOCKED traps).
|
||||
> Prefer the Named Event + the `pt-cmdpal-recycle.ps1` helpers over raw SendInput wherever possible.
|
||||
|
||||
## Entry-paths (try in order)
|
||||
|
||||
### 1. Named Event — fastest, foreground-free
|
||||
```powershell
|
||||
. "$skill\scripts\pt-shared-events.ps1"
|
||||
Invoke-PtSharedEvent -Name 'CmdPal.Show' # opens CmdPal without keyboard/foreground
|
||||
```
|
||||
The deterministic, UIPI-immune way to open CmdPal (same downstream path the runner's hotkey handler signals). Verify via `winapp ui list-windows` for the `Microsoft.CmdPal.UI` window.
|
||||
|
||||
### 2. Query helper — drive a search + read results
|
||||
```powershell
|
||||
. "$skill\scripts\pt-cmdpal-recycle.ps1"
|
||||
Invoke-CmdPalQuery -Text 'notepad' # types a PLAIN query; auto-handles degraded state
|
||||
```
|
||||
Use for plain-text queries (result list assertions). It routes through the helper so a TextChanged-broken
|
||||
session is detected + recycled first. **Aliases are different — see trap below.**
|
||||
|
||||
### 3. Activation hotkey — only when the chord binding itself is under test
|
||||
Press the configured activation shortcut (Settings → Command Palette). SendInput requires an attached
|
||||
input desktop (SKILL.md pitfall #7) **and** you must foreground CmdPal first (trap below); otherwise
|
||||
prefer entry-path 1.
|
||||
|
||||
## Recipes — control/observation map, NOT an answer key
|
||||
|
||||
| # | Capability | Drive (control / command) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | CmdPal opens & is listening | `Invoke-PtSharedEvent -Name 'CmdPal.Show'` (or the hotkey) | `Microsoft.CmdPal.UI` window appears (`winapp ui list-windows`) |
|
||||
| 2 | Plain-text query returns results | `Invoke-CmdPalQuery -Text '<q>'` / `winapp ui set-value` the search box | results list updates to matching items |
|
||||
| 3 | Alias engages (`=` `<` `>` `:` `$` `??` `)`) | **real keystrokes** — `Assert-PtForegroundOrAbort -AppId Microsoft.CmdPal.UI` → `Send-PtChord` (NOT `set-value`) | the alias provider activates (e.g. `=` → calculator result) |
|
||||
| 4 | Dismiss / navigate back | `winapp ui invoke BackButton` (NOT Esc — filtered) / `Reset-CmdPalToHome` | returns to home / closes |
|
||||
| 5 | Recover a wedged session | `Test-CmdPalDegraded` → `Reset-CmdPalAppX` | CmdPal responds to input again |
|
||||
| 6 | A setting round-trips | edit via Settings UI → `Get-CmdPalSettings` | the CmdPal store reflects the change |
|
||||
|
||||
> Mapping: read the checklist item → find the capability row → drive that control and design your own
|
||||
> inputs/assertions. New capability ⇒ add a row.
|
||||
|
||||
## BLOCKED traps
|
||||
- **AppX foreground-lock — SendInput keys leak to your terminal.** External `SetForegroundWindow` on the CmdPal AppX window is unreliable: Windows' foreground-lock blocks it after the first call, so synthetic keys go to whatever is actually foreground (often your caller). **Always `Assert-PtForegroundOrAbort -AppId Microsoft.CmdPal.UI` immediately before any SendInput**; prefer the Named Event (entry-path 1) which needs no foreground.
|
||||
- **TextChanged-broken state every ~30 probes.** After heavy scripted querying the search box stops raising TextChanged and results freeze. Detect with `Test-CmdPalDegraded` and recover with `Reset-CmdPalAppX` before continuing.
|
||||
- **Alias detection requires REAL keystrokes.** `winapp ui set-value` on the search box bypasses TextChanged, so alias prefixes (`=`, `<`, `>`, `:`, `$`, `??`, `)`) never fire. Use `Send-PtChord` (after `Assert-PtForegroundOrAbort`) for aliases; reserve `set-value` for plain queries.
|
||||
- **Esc is filtered** by the WinUI 3 raw-input hook, so "press Esc to go back/close" doesn't work via injection. Use `winapp ui invoke BackButton` (or `Reset-CmdPalToHome`) instead.
|
||||
@@ -1,100 +0,0 @@
|
||||
# File Locksmith — module verification profile
|
||||
|
||||
**PT module**: `File Locksmith` (shows which processes are using selected files/dirs and lets you kill them)
|
||||
**Source**: `<PT-repo>\src\modules\FileLocksmith\` (PT repo)
|
||||
**Settings file (module)**: `%LOCALAPPDATA%\Microsoft\PowerToys\File Locksmith\settings.json` (`{"properties":{"bool_show_extended_menu":{...}}}`) and `file-locksmith-settings.json` (`{"showInExtendedContextMenu":bool}`)
|
||||
**Enable flag**: `%LOCALAPPDATA%\Microsoft\PowerToys\settings.json` → `enabled."File Locksmith"` (general settings; runner-owned; **read-only for verification** — flip enable/disable via the Settings-UI toggle)
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\File Locksmith\FileLocksmithUI\Logs\…`
|
||||
**Exes**: UI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.FileLocksmithUI.exe`; CLI = `%LOCALAPPDATA%\PowerToys\FileLocksmithCLI.exe`
|
||||
**Context menu**: Win11 packaged `IExplorerCommand` CLSID `{AAF1E27D-4976-49C2-8895-AAFA743C0A7E}` (sparse pkg `Microsoft.PowerToys.FileLocksmithContextMenu`); legacy `FileLocksmithExt.dll`. Caption resource = "What's using this file?".
|
||||
**Named Event / DSC**: no Named Event. DSC resource `microsoft.powertoys.FileLocksmith.settings` exists (controls module settings, not the master enable flag).
|
||||
**No global hotkey** — entry is the Explorer context menu only.
|
||||
|
||||
**Context-menu routing (File Locksmith-specific):** appears on a **selected file OR a selected folder**
|
||||
(verified both) — but **NOT** on the folder-background menu. Select the file/folder first
|
||||
(`Select-PtExplorerFiles`, Shell COM) then open its menu with `Open-PtExplorerContextMenu -FileName <f>`
|
||||
(see `references/explorer-context-menu-flow.md`). Verified visible caption:
|
||||
**`Unlock with File Locksmith`** (the resource string "What's using this file?" is not what renders).
|
||||
Confirm at runtime with `Get-PtContextMenuItems`.
|
||||
|
||||
## The two back-doors that make this module fully drivable (no Explorer needed)
|
||||
|
||||
### 1. `FileLocksmithCLI.exe` — deterministic engine ground-truth (PREFER for assertions)
|
||||
Accepts paths as args; `--json`, `--kill`, `--wait`, `--timeout`. Uses the **same** `find_processes_recursive` engine as the UI.
|
||||
```powershell
|
||||
$cli = "$env:LOCALAPPDATA\PowerToys\FileLocksmithCLI.exe"
|
||||
& $cli "<file|dir|drive>" --json | ConvertFrom-Json # {processes:[{pid,name,user,files[]}]}
|
||||
& $cli "<path>" --kill # terminate lockers (== End task)
|
||||
```
|
||||
Detection = open **File handles** + **loaded modules** under the path (exact file, exact dir, dir-prefix recursive). `FileLocksmith.cpp:18-113`.
|
||||
|
||||
### 2. `last-run.log` IPC + launch UI — exercises the REAL UI code path
|
||||
The context-menu handler writes selected paths to `…\File Locksmith\last-run.log` then launches the UI; the UI reads them in `MainViewModel()` ctor. Reproduce it:
|
||||
```powershell
|
||||
# UTF-16LE, each path + WCHAR \n, trailing empty-line terminator, NO BOM
|
||||
function Write-LastRun([string[]]$Paths){
|
||||
$f="$env:LOCALAPPDATA\Microsoft\PowerToys\File Locksmith\last-run.log"; $ms=[IO.MemoryStream]::new()
|
||||
foreach($p in $Paths){$b=[Text.Encoding]::Unicode.GetBytes($p);$ms.Write($b,0,$b.Length);$n=[Text.Encoding]::Unicode.GetBytes("`n");$ms.Write($n,0,$n.Length)}
|
||||
$n=[Text.Encoding]::Unicode.GetBytes("`n");$ms.Write($n,0,$n.Length);[IO.File]::WriteAllBytes($f,$ms.ToArray())
|
||||
}
|
||||
Write-LastRun @("C:\path\to\file"); Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.FileLocksmithUI.exe"
|
||||
```
|
||||
Source: `ExplorerCommand.cpp:182-227`, `dllmain.cpp:94-159`, `IPC.cpp`, `NativeMethods.cpp:62-97`.
|
||||
|
||||
### UI selectors (winapp ui)
|
||||
- Window title: `Administrator: File Locksmith` (elevated) vs `File Locksmith` (non-elevated).
|
||||
- Header button = the path label (`btn-<pathname>-…`); top-right `btn-…` = **Reload** (tooltip "Reload").
|
||||
- Per-row End task button = `btn-…` (parent of `lbl-endtask-…`); invoke it (`InvokePattern`).
|
||||
- `RestartAsAdminBtn` = shield icon, **visible only when non-elevated** (`MainPage.xaml:72-82`).
|
||||
- `ProcessesListView` is virtualized; use `winapp ui scroll ProcessesListView --direction down/up`.
|
||||
|
||||
## Recipes — a control/observation map, NOT a per-test-case answer key
|
||||
|
||||
> Maps each capability to **how to drive it** and **where the result shows**. No canned process counts / paths / assertions — design those at runtime from the actual checklist item.
|
||||
|
||||
| # | Capability | Drive (entry-path / control) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | A locked file lists all its locking processes | CLI + UI on one locked file (with multiple lockers) | each locker shows as a ListItem |
|
||||
| 2 | "End task" kills the locker and de-lists it | `winapp ui invoke` the End-task button | locker PID dies + row removed |
|
||||
| 3 | Reload rediscovers a locker started after the UI opened | start a new locker → invoke Reload | the new locker appears |
|
||||
| 4 | Closing a locker externally auto-removes it | external `Stop-Process` on a locker | auto-delisted via `WatchProcess`; empty state shown |
|
||||
| 5 | Directory path finds lockers recursively | CLI/UI with a directory path | lockers inside the tree are listed |
|
||||
| 6 | Drive root lists many lockers without crashing | CLI/UI with a drive root | large list renders; no crash |
|
||||
| 7 | Non-elevated FL does NOT see the elevated runner | run CLI/UI non-elevated via scheduled task `RunLevel Limited` | `PowerToys.exe` absent (medium-IL FL can't see elevated procs) |
|
||||
| 8 | "Restart as administrator" surfaces elevated-only lockers | non-elev UI shows the button; elevated run shows them | elevated run lists `PowerToys.exe` (UAC consent click NOT automatable) |
|
||||
| 9 | Scrolling a large list doesn't crash | UI on a drive root + `winapp ui scroll` | process alive + responsive after scroll |
|
||||
| 10 | Disabling FL removes the Explorer context-menu entry | **Settings-UI toggle** via `Set-PtModuleEnabledViaSettingsUI -PageTag FileLocksmith -EnabledKey 'File Locksmith'` (see `references/explorer-context-menu-flow.md` → "Enabling / disabling the module") | `enabled."File Locksmith"`→false; entry gone (live, no restart); `GetState→ECS_HIDDEN` (source) |
|
||||
|
||||
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control and design your own inputs + assertions. If no row matches, drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
|
||||
|
||||
## Common BLOCKED traps (avoid)
|
||||
- **Don't BLOCK the lock-detection / End-task / scroll-doesn't-crash items as "needs a real installer / right-click"** — both the CLI and the `last-run.log`+UI back-door fully drive them with any locked file. The context menu literally just writes `last-run.log` + launches the same exe.
|
||||
- **Launching the UI from an elevated shell makes it elevated** (title "Administrator: …") and hides the `RestartAsAdminBtn`. To test the non-elevated case (Recipes 7-8), launch via a **scheduled task with `-RunLevel Limited` / LogonType Interactive** — it lands on the user's desktop at medium IL.
|
||||
- **`set-value` does fire the Reload** here (TogglePattern/Invoke work); no TextChanged gotchas.
|
||||
|
||||
## Elevation semantics (non-elevated FL invisibility — Recipes 7-8 core)
|
||||
A medium-IL File Locksmith can't `DuplicateHandle`/read modules of higher-integrity processes, so the **elevated PowerToys.exe runner is invisible** to a non-elevated FL and **visible** to an elevated FL (which also calls `SetDebugPrivilege`, `App.xaml.cs:53-61`). Per-user installs put PowerToys under `%LOCALAPPDATA%\PowerToys`, not "Program Files" — use the PT install dir as the stand-in and note the caveat.
|
||||
|
||||
## Context-menu disable gate (Recipe 10)
|
||||
`GetEnabled()` reads general `enabled."File Locksmith"` (`Settings.cpp:53-77`). When false: Win11 `GetState→ECS_HIDDEN` (`dllmain.cpp:81-84`); legacy `QueryContextMenu→E_FAIL` (`ExplorerCommand.cpp:116-119`). Disabling does NOT unregister the sparse package (stays `Status Ok`) — the entry is hidden dynamically. The packaged `IExplorerCommand` is **not** enumerable via Shell `Verbs()` and **not** `CoCreate`-able from a non-Explorer host (`REGDB_E_CLASSNOTREGISTERED`), so the pixel-level render is the only un-automatable bit (`BLK-VISUAL-RENDER` if you need it).
|
||||
|
||||
## Fixture files needed
|
||||
None pre-canned. Create a temp file and lock it with a helper process (pwsh holding `File.Open(path, OpenOrCreate, Read, ReadWrite)` so multiple lockers coexist).
|
||||
|
||||
## Source citations
|
||||
- `FileLocksmithLibInterop\FileLocksmith.cpp:18-113` — `find_processes_recursive` (handles + modules, recursive).
|
||||
- `FileLocksmithLibInterop\NativeMethods.cpp:62-140` — `ReadPathsFromFile`, `StartAsElevated` (runas/--elevated).
|
||||
- `FileLocksmithLib\IPC.cpp`, `Constants.h` — last-run.log format/path.
|
||||
- `FileLocksmithUI\…\MainViewModel.cs:80-183` — load/EndTask/WatchProcess/RestartElevated.
|
||||
- `FileLocksmithUI\…\MainPage.xaml` — control layout + RestartAsAdminBtn visibility.
|
||||
- `FileLocksmithExt\ExplorerCommand.cpp`, `FileLocksmithContextMenu\dllmain.cpp`, `FileLocksmithLib\Settings.cpp` — enable gate.
|
||||
|
||||
## Real right-click verification (Recipe 10) — works on an unlocked desktop
|
||||
**Use the shared flow: `references/explorer-context-menu-flow.md` + `scripts/pt-explorer-contextmenu.ps1`.** FL's shipped caption is **"Unlock with File Locksmith"** (not the checklist's "What's using this file?"). The disable-removes-menu item is behaviorally verifiable; `GetState` re-reads `enabled."File Locksmith"` live (no Explorer restart). The right-click test needs an unlocked interactive desktop (idle auto-lock → `GetForegroundWindow()=0` → BLK-ENV).
|
||||
```powershell
|
||||
. "$skill\scripts\pt-explorer-contextmenu.ps1"
|
||||
$menu = Open-PtExplorerContextMenu -ExplorerHwnd $hwnd -FileName 'target.txt' # synthetic right-click (+retry)
|
||||
(Get-PtContextMenuItems -MenuHwnd $menu) -contains 'Unlock with File Locksmith' # true enabled / false disabled
|
||||
Invoke-PtContextMenuItem -MenuHwnd $menu -ItemName 'Unlock with File Locksmith' # launches PowerToys.FileLocksmithUI.exe (non-elevated)
|
||||
```
|
||||
- Don't expect `Shell.Application.Verbs()` to show the FL entry — Win11 packaged command, invisible to classic verbs. Don't kill by name (`Stop-Process -Id <pid>`). For the disable test, flip enable/disable with `Set-PtModuleEnabledViaSettingsUI -PageTag FileLocksmith` and **restore by re-toggling on** (`-Enabled $true`); close the spawned UI + the Settings window afterward.
|
||||
@@ -1,82 +0,0 @@
|
||||
# Image Resizer — module verification profile
|
||||
|
||||
**PT module**: `Image Resizer` (resize images via Explorer right-click; WinUI 3 GUI + headless CLI)
|
||||
**Source**: `<PT-repo>\src\modules\imageresizer\` (PT repo)
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\Image Resizer\settings.json` (PowerToys-wrapper shape: `{ "properties": { "imageresizer_*": { "value": … } }, "name": "Image Resizer", "version": "1" }`). A legacy `sizes.json` mirrors `imageresizer_sizes`; `image-resizer-settings.json` is `{}` (unused).
|
||||
**Enable flag**: `%LOCALAPPDATA%\Microsoft\PowerToys\settings.json` → `enabled."Image Resizer"` (runner-owned; **read-only for verification** — flip enable/disable via the Settings-UI toggle).
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\Image Resizer\Logs\…`
|
||||
**Exes**: GUI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.ImageResizer.exe`; **CLI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.ImageResizerCLI.exe`**.
|
||||
**Context menu**: Win11 packaged `IExplorerCommand` (sparse pkg `ImageResizerContextMenuPackage.msix`, dllmain.cpp) + legacy classic `ImageResizerExt.dll` (`dll/ContextMenuHandler.cpp`). **Shipped caption = "Resize with Image Resizer"** (`IDS_IMAGERESIZER_CONTEXT_MENU_ENTRY`.
|
||||
**No global hotkey / no Named Event / no DSC for the engine** — entry is the Explorer menu (or direct exe launch).
|
||||
|
||||
**Context-menu routing (Image Resizer-specific):** appears **only on a selected image file** — NOT on
|
||||
the folder-background menu, and NOT on non-image files. Select an image first (`Select-PtExplorerFiles`,
|
||||
Shell COM) then open the file menu with `Open-PtExplorerContextMenu -FileName <img>` (see
|
||||
`references/explorer-context-menu-flow.md`). Verified caption **`Resize with Image Resizer`**.
|
||||
|
||||
## The back-door that makes this module ~fully drivable (no Explorer needed)
|
||||
|
||||
### `PowerToys.ImageResizerCLI.exe` — the deterministic engine (PREFER for all resize-behavior items)
|
||||
Shares the exact `ResizeBatch.FromCliOptions` → `ResizeBatch.ProcessAsync` → `ResizeOperation.ExecuteAsync` engine as the GUI (`ui/Cli/ImageResizerCliExecutor.cs:76-108`, `App.xaml.cs:102`). Reads live `Image Resizer\settings.json` then applies CLI overrides (`CliSettingsApplier.cs`).
|
||||
```
|
||||
--width/-w --height/-h --unit/-u {Centimeter|Inch|Percent|Pixel} --fit/-f {Fill|Fit|Stretch}
|
||||
--size <presetIndex> --shrink-only --replace/-r --ignore-orientation --remove-metadata
|
||||
--quality/-q --keep-date-modified --filename/-n "<%1..%6>" --destination/-d <dir>
|
||||
--show-config --help <files…> (also accepts \\.\pipe\<name> and stdin file list)
|
||||
```
|
||||
`--show-config` dumps the effective settings (great pre/post check). `-d <dir>` keeps outputs isolated. Assert output dimensions with `[System.Drawing.Image]::FromFile(p)`.
|
||||
**Caveat**: `--ignore-orientation`/`--shrink-only`/`--replace`/`--keep-date-modified`/`--remove-metadata` are *flags* — they can only set the value **true**; to test the **false** case, temporarily edit `settings.json` (back up + restore).
|
||||
|
||||
### Direct GUI launch — for the two UI-only items (gif warning, size-list populated)
|
||||
`Start-Process PowerToys.ImageResizer.exe "<file>"` opens the window pre-loaded (argv/stdin via `ResizeBatch.FromCommandLine`). Behaviorally identical to the context-menu launch (`dllmain.cpp:219-245` just writes a pipe + launches the same exe). Then drive with `winapp ui`:
|
||||
- Size selector: `SizeComboBox` → `winapp ui invoke SizeComboBox -w <hwnd>` to expand, then `inspect` shows `itm-<name>-XXXX` ListItems.
|
||||
- Gif warning: `Message Text "Gif files with animations may not be correctly resized."` InfoBar, bound to `ViewModel.HasGifFiles` (set when any file ends `.gif`).
|
||||
|
||||
## Engine facts (verified from source — cite these for the resize items)
|
||||
- `ResizeFit`: **Fill=0, Fit=1, Stretch=2** (`ResizeFit.cs`). Fit=`min(scaleX,scaleY)`; Fill=`max`+centered-crop; Stretch=independent (`ResizeOperation.cs:449-498`).
|
||||
- `ResizeUnit`: **Centimeter=0, Inch=1, Percent=2, Pixel=3** (`ResizeUnit.cs`). Inch=`v*dpi`; cm=`v*dpi/2.54`; Percent=`v/100*orig`; Pixel=`v` (`ResizeSize.cs:109-123`). **Outputs depend on image DPI** — read actual DPI and compute expectations from it (a 120-DPI fixture gives 10cm→472px, 4in→480px).
|
||||
- Filename `%1..%6` → original-name, size-name, selected-W, selected-H, **output**-W, **output**-H (`Settings.cs:229-239`, `ResizeOperation.cs:593-601`).
|
||||
- ShrinkOnly: if target scale>1, returns `noTransformNeeded` (file copied unchanged) (`ResizeOperation.cs:462-475`).
|
||||
- KeepDateModified: `SetLastWriteTimeUtc(out, GetLastWriteTimeUtc(src))` (`ResizeOperation.cs:146-149`).
|
||||
- Replace: `File.Replace(out, src, backup)` then recycle backup — no copy left (`ResizeOperation.cs:151-156`).
|
||||
- IgnoreOrientation swap: gated by `IgnoreOrientation && !HasAuto && Unit != Percent` (`ResizeOperation.cs:419-444`).
|
||||
|
||||
## Recipes — a control/observation map, NOT a per-test-case answer key
|
||||
|
||||
> Maps each capability to **which control/CLI flag drives it** and **where the result shows**. CLI flag *names* and fit-mode/unit/field enumerations are stable IR knowledge and stay; concrete flag *values*, fixtures, and expected outputs are per-test-case — design those at runtime.
|
||||
|
||||
| # | Capability | Drive (control / CLI flag) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | Module disabled → context-menu entry absent | **Settings-UI toggle** via `Set-PtModuleEnabledViaSettingsUI -PageTag ImageResizer -EnabledKey 'Image Resizer'` (locked desktop → `BLK-ENV`); synthetic menu (only valid observer) | "Resize with Image Resizer" absent. Gate: `dllmain.cpp:87-91` (ECS_HIDDEN), `ContextMenuHandler.cpp:70-71,383-385`. Locked desktop → BLK-ENV |
|
||||
| 2 | Module enabled → entry present (modern + classic), click launches the GUI | synthetic menu + invoke | `Get-PtContextMenuItems` shows "Resize with Image Resizer"; classic "Show more options" too; invoke → `PowerToys.ImageResizer.exe` launches |
|
||||
| 3 | Remove a built-in size / add a custom size | edit `imageresizer_sizes` (INTEGER Ids!) + launch GUI | `SizeComboBox` reflects the edit (removed gone, custom present) |
|
||||
| 4 | Resize one / multiple files end-to-end | CLI `--size <id> [files…]` | outputs at the size's Fit dimensions |
|
||||
| 5 | GIF animation warning on `.gif` input | GUI on a `.gif` | warning InfoBar present (`winapp ui inspect`) |
|
||||
| 6 | Fit modes (Fill / Fit / Stretch) | CLI `--width --height --fit <mode>` | output shape matches the mode (crop / letterbox / exact) |
|
||||
| 7 | Unit conversion (cm / inch / percent / pixel) | CLI `--unit <u>` | output px = unit converted at the image's DPI |
|
||||
| 8 | Custom filename format (`%1`..`%6` fields) | CLI `--filename <fmt>` | output filename follows the format fields |
|
||||
| 9 | "Keep date modified" | CLI `--keep-date-modified` | output mtime == source mtime (control: without the flag, differs) |
|
||||
| 10 | "Shrink only" | CLI `--shrink-only` | an already-small image is untouched (control: a large one still shrinks) |
|
||||
| 11 | "Replace original" | CLI `--replace` | original replaced in place; no `(name) (1)` copy |
|
||||
| 12 | "Ignore orientation" | settings (false) vs flag (true) | on a portrait target over a landscape image: false→no W/H swap, true→swap |
|
||||
|
||||
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control/flag and design your own inputs + assertions. If no row matches, drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
|
||||
|
||||
## Common BLOCKED traps (avoid)
|
||||
- **Don't mark the resize-behavior items BLOCKED for "needs a real right-click".** The CLI fully drives them with the identical engine; the menu is just the trigger (prove the menu→launch wiring once with the golden path in Recipe 2).
|
||||
- **PowerShell `ConvertTo-Json` writes computed numbers as doubles (`"Id": 3.0`)** → `System.Text.Json` rejects `imageresizer_sizes` and the app silently falls back to the 4 built-in default presets (Small/Medium/Large/Phone). Cast Ids to `[int]` or regex-strip `\.0`. This bit the remove-size-add-custom item (Recipe 3) the first time.
|
||||
- **cm/inch outputs depend on the fixture's DPI, not 96.** System.Drawing saves at the session display DPI (here 120). Compute expectations from the actual DPI.
|
||||
- **Idle auto-lock = BLK-ENV for the disabled-absent + enabled-present items (Recipes 1-2)** (synthetic right-click needs foreground). Disable lock/sleep before the run (`references/environment-setup.md`).
|
||||
- Don't expect `Shell.Application.Verbs()` to list the entry — Win11 packaged command (`CoCreate` → `REGDB_E_CLASSNOTREGISTERED`). Kill by `-Id <pid>` not name. Restore by re-toggling **on** via `Set-PtModuleEnabledViaSettingsUI -PageTag ImageResizer -Enabled $true`; revert any `settings.json`/`sizes.json` edits and close the Settings window.
|
||||
|
||||
## Fixture files needed
|
||||
None pre-canned. Generate with `System.Drawing`: a landscape (e.g. 1200×800) and portrait (800×1200) JPEG, a small (100×100) PNG, a square (400×400) PNG, a `.gif` (single frame is fine — the warning is extension-based), and 3 identical images for the multi-file batch.
|
||||
|
||||
## Source citations
|
||||
- `ui/Cli/ImageResizerCliExecutor.cs`, `ui/Models/CliOptions.cs`, `ui/Cli/CliSettingsApplier.cs`, `ui/Cli/Commands/ImageResizerRootCommand.cs` — CLI surface + engine reuse.
|
||||
- `ui/Models/ResizeOperation.cs:419-501,572-617,146-156` — dimension math, filename, keep-date, replace.
|
||||
- `ui/Models/ResizeSize.cs:78-124`, `ResizeFit.cs`, `ResizeUnit.cs` — unit/fit math + enum order.
|
||||
- `ui/Properties/Settings.cs:65,105-111,229-239,431-552` — paths, defaults, JSON property names, FileNameFormat.
|
||||
- `ImageResizerContextMenu/dllmain.cpp:49,79-133,219-245,284` — modern menu title, enable+image gate, launch, caption.
|
||||
- `dll/ContextMenuHandler.cpp:21,46-48,70-71,383-385` — classic menu caption + enable gate.
|
||||
- `ui/ViewModels/InputViewModel.cs:76,143`, `ImageResizerXAML/Views/InputPage.xaml:293-299`, `Strings/en-us/Resources.resw:148-149` — gif warning.
|
||||
@@ -1,76 +0,0 @@
|
||||
# New+ — module verification profile
|
||||
|
||||
**PT module**: `NewPlus` (Explorer right-click → "New+" submenu that creates files/folders from a user templates folder)
|
||||
**Source**: `<PT-repo>\src\modules\NewPlus\` (shell ext) + `<PT-repo>\src\settings-ui\Settings.UI\ViewModels\NewPlusViewModel.cs` (Settings UI)
|
||||
**Module-owned settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\settings.json` — **folder is `NewPlus`, NOT `New`** (matches SKILL.md pitfall #12 table). Keys: `HideFileExtension`, `HideStartingDigits`, `TemplateLocation`, `ReplaceVariables`, `BuiltInNewHidePreference`.
|
||||
**Templates folder (default)**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\Templates` (per `TemplateLocation`)
|
||||
**Default-templates source**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\Assets\NewPlus\Templates` (also `%ProgramFiles%\PowerToys\...` on machine installs)
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\NewPlus.ShellExtension\Logs\v<ver>\log_<date>.log`
|
||||
**Packaged command**: sparse MSIX `Microsoft.PowerToys.NewPlusContextMenu`; command CLSID `{FF90D477-E32A-4BE8-8CC5-A502A97F5401}`
|
||||
**Named Event**: none. **DSC**: n/a.
|
||||
|
||||
> Read **`../references/explorer-context-menu-flow.md` first** — New+ is a Win11 packaged-IExplorerCommand context-menu module; the menu can only be eyeballed via a real synthetic right-click on an **unlocked interactive desktop**. On a locked/RDP-minimized desktop (`Test-PtDesktopInteractive=False`) all "menu appears / template appears / hidden-caption" assertions are BLK-ENV / BLK-VISUAL-RENDER, not product FAILs.
|
||||
|
||||
## Entry-paths (try in order)
|
||||
|
||||
### 1. Enable/disable + registration gate (menu presence/absence)
|
||||
Drive enable/disable through the **Settings-UI toggle** (entry-path 2 / the shared helper) — it's the faithful user flow, runs the Settings→runner IPC path, and (uniquely for New+) triggers the enable-time `CopyTemplateExamples` that seeds the default templates. Observe the gate:
|
||||
- CLSID registered ⇒ `Test-Path "HKCU:\Software\Classes\CLSID\{FF90D477-E32A-4BE8-8CC5-A502A97F5401}"` is `True` (enabled) / `False` (disabled).
|
||||
- Log lines `New+ context menu registered` / `... unregistered` + `Runtime registration completed for CLSID ...`.
|
||||
- Sparse package stays `Status Ok` even when disabled (hidden dynamically — SKILL.md pitfall #11).
|
||||
|
||||
> The toggle needs an unlocked interactive desktop. If it's locked / RDP-minimized so the Settings UI can't be driven, mark the item `BLK-ENV` (`references/environment-setup.md`).
|
||||
|
||||
### 2. Settings UI toggle via the shared helper — **required for template auto-copy**
|
||||
Use `Set-PtModuleEnabledViaSettingsUI -PageTag NewPlus -EnabledKey NewPlus` (`scripts/pt-explorer-contextmenu.ps1`; see `references/explorer-context-menu-flow.md` → "Enabling / disabling the module"). It opens `--open-settings=NewPlus`, finds the enable toggle by its `[on]/[off]` state, and flips it. Manual equivalent — `winapp ui invoke <btn> -w <settingsHwnd>`:
|
||||
- Enable toggle: discover by state (e.g. `btn-new-XXXX` under `NewPlusEnableToggle`) — **the enable transition runs `CopyTemplateExamples`** (Settings-UI side, `NewPlusViewModel.IsEnabled` setter), so templates are seeded only when you flip the actual toggle.
|
||||
- `btn-hidethefileexte-XXXX` (Hide file extension), `btn-hideleadingdigi-XXXX` (Hide leading digits). AutomationIds carry a per-session suffix — re-`inspect` to get the live id.
|
||||
|
||||
### 3. Synthetic right-click on the folder **BACKGROUND** (the menu-render observer) — needs unlocked desktop
|
||||
New+ lives in the folder-background ("New") menu, **not** a file's context menu — so
|
||||
`Open-PtExplorerContextMenu` (which right-clicks a *file item*) is the wrong entry. Use
|
||||
**`Open-PtBackgroundContextMenu -ExplorerHwnd <h>`** (coordinate-free Shift+F10 with nothing selected;
|
||||
validates it got the background menu), then expand the `New+` submenu with
|
||||
**`Expand-PtModernSubmenu`** (the modern-menu sibling of `Open-PtShowMoreOptionsMenu`; both in
|
||||
`scripts/pt-explorer-contextmenu.ps1`):
|
||||
```powershell
|
||||
$bg = Open-PtBackgroundContextMenu -ExplorerHwnd $hwnd # folder-background ("New") menu
|
||||
$sub = Expand-PtModernSubmenu -MenuHwnd $bg -ItemName 'New+' ` # invokes New+, returns the child popup HWND
|
||||
-ContainsItem 'Open templates' # disambiguates parent vs child PopupWindowSiteBridge
|
||||
Get-PtContextMenuItems -MenuHwnd $sub # templates are 1:1 with the Templates-folder entries
|
||||
# Invoke-PtContextMenuItem -MenuHwnd $sub -ItemName '<template>' # select a template (creates it in the current folder)
|
||||
```
|
||||
`Expand-PtModernSubmenu` reuses `Invoke-PtContextMenuItem` for the click and resolves the child
|
||||
`PopupWindowSiteBridge` popup (preferring the one that contains `-ContainsItem`, else a newly-appeared
|
||||
popup). Template items render with **caption transforms applied** (HideFileExtension strips `.txt`; HideStartingDigits strips `01. `). Selecting a template creates it in the current folder + enters rename mode. BLK-ENV only if `Test-PtDesktopInteractive` is False. **No Explorer restart needed** for setting A/B — the handler re-reads `NewPlus\settings.json` on each menu build.
|
||||
|
||||
## Recipes — a control/observation map, NOT an answer key
|
||||
|
||||
| # | Capability | Drive (control / settings key) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | Menu entry present when enabled | enable via **Settings-UI toggle** (`Set-PtModuleEnabledViaSettingsUI -PageTag NewPlus`; locked desktop → `BLK-ENV`) | CLSID registered in `HKCU\…\CLSID`, log `context menu registered`; *visible submenu* = synthetic menu only (BLK-VISUAL-RENDER if locked) |
|
||||
| 2 | Menu entry absent when disabled | disable | CLSID absent, log `context menu unregistered`; package still `Status Ok` |
|
||||
| 3 | Templates folder created empty | shell ext `create_folder_if_not_exist(root)` on menu build (delete folder → right-click) | folder recreated **empty** — needs synthetic menu (BLK-ENV if locked) |
|
||||
| 4 | Default templates copied when empty | `CopyTemplateExamples` on Settings-UI **enable** transition (`btn-new-248c` off→on) while folder empty | Templates folder repopulated from install Assets (filesystem — headless-safe) |
|
||||
| 5 | A template (file/folder) shows + creates on select | put item in Templates folder; select it in the New+ submenu | submenu item (1:1 with dir entries) + `SHFileOperation FO_COPY` to target — synthetic menu only |
|
||||
| 6 | Hide file extension | `HideFileExtension` / `btn-hidethefileexte-24a0` | strips ext from **menu caption only** (`get_menu_title`, `show_extension=false`); created file keeps ext — caption is BLK-VISUAL-RENDER if locked |
|
||||
| 7 | Hide starting digits/spaces/dots | `HideStartingDigits` / `btn-hideleadingdigi-24a8` | strips leading digits+separator from **both** menu caption and **created filename** (`remove_starting_digits_from_filename` via `get_menu_title` + `copy_template`); needs a digit-prefixed template + render |
|
||||
|
||||
> Verify a setting actually drives behavior by editing the **module-owned** `NewPlus\settings.json` (not the PT-store mirror) and relaunching; the Settings toggles round-trip into this same file.
|
||||
|
||||
## Common BLOCKED traps
|
||||
- **Default templates are only seeded by the Settings-UI enable transition** (`NewPlusViewModel.IsEnabled` → `CopyTemplateExamples`) — always drive enable via the UIA toggle for any template-auto-copy item.
|
||||
- **Menu render is invisible without a real right-click** — packaged command is not `CoCreate`-able (`REGDB_E_CLASSNOTREGISTERED`) and not in classic `Shell.Application.Verbs()`. Locked desktop ⇒ BLK-ENV; do not substitute a CLI/back-door (there isn't one, and it'd be a false PASS).
|
||||
- **No template-count observable** — `saved_number_of_templates` is an in-memory static (`new_utilities.cpp`), not registry/log.
|
||||
- Don't edit `…\PowerToys\New\settings.json` — wrong path; the file is under `NewPlus\`. Re-reads per menu build, so don't restart Explorer to apply a setting.
|
||||
- Use the folder **background** ("New") menu, not `Open-PtExplorerContextMenu`; **expand the `New+` submenu** before enumerating templates. Locked desktop ⇒ menu-render items are BLK-ENV, not product FAIL.
|
||||
|
||||
## Fixture files needed
|
||||
- A plain file (e.g. `test.txt`) and a folder-with-files to drop into Templates (template-appears items).
|
||||
- A digit-prefixed template (e.g. `01. Test.txt`) to exercise Hide-starting-digits.
|
||||
|
||||
## Source citations
|
||||
- `src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp` — `get_menu_title` (hide-extension), `remove_starting_digits_from_filename`, `copy_object_to`.
|
||||
- `src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h` — `copy_template`, `create_folder_if_not_exist`, `get_newplus_setting_hide_*`, `register_msix_package`.
|
||||
- `src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp` — `create_folder_if_not_exist(root)` + template enumeration.
|
||||
- `src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs` — `CopyTemplateExamples` (creates dir; copies examples only when files==0 && dirs==0), called from `IsEnabled` setter / `OpenNewTemplateFolder` / `DashboardViewModel`.
|
||||
@@ -1,120 +0,0 @@
|
||||
# Peek — module verification profile
|
||||
|
||||
**PT module**: `Peek` (file previewer activated on Ctrl+Space with Explorer file selected)
|
||||
**Source**: `<PT-repo>\src\modules\Peek\` (PT repo)
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\Peek\settings.json`
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\Peek\Logs\v<ver>\log_<date>.log`
|
||||
**Exes**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.Peek.UI.exe`
|
||||
**Default hotkey**: `Ctrl+Space` (modifiers=`ctrl`, code=32; see `settings.json` → `ActivationShortcut`)
|
||||
**Named Event**: `Local\ShowPeekEvent` (friendly name: `Peek.Show` in `pt-shared-events.ps1` catalog)
|
||||
**DSC resource**: `Microsoft.PowerToys/PeekSettings`
|
||||
|
||||
## Three entry-paths (try in order)
|
||||
|
||||
### 1. CLI back-door — fastest, no Explorer needed
|
||||
```powershell
|
||||
# MUST run non-elevated (see the elevation warning below) — from an elevated agent shell use:
|
||||
# . "$skill\scripts\pt-nonelevated.ps1"; Start-PtNonElevated -Exe $peekExe -Arguments '"<file>"'
|
||||
Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.Peek.UI.exe" -ArgumentList "<file>"
|
||||
```
|
||||
**Source**: `Peek.UI\PeekXAML\App.xaml.cs:106-134` — when last arg is not int (=runner PID) and is an existing file, it sets `_launchedFromCli=true`, builds `SelectedItemByPath`, calls `OnShowPeek()`. Bypasses hotkey + Explorer foreground.
|
||||
|
||||
**Use for**: single-file previewer rendering tests (Recipes 1-2) and the CLI-accepts-path assertion (Recipe 8).
|
||||
|
||||
> **⚠️ ELEVATION — launch Peek NON-ELEVATED for previewer tests.** If you `Start-Process` the exe
|
||||
> from an **elevated** agent shell, Peek runs at high IL and its **WebView2** host fails to
|
||||
> initialize: `BrowserControl.xaml.cs::PreviewWV2_Loaded` logs *"WebView2 loading failed. Object
|
||||
> reference not set to an instance of an object."* and the dev/text, **markdown, PDF and HTML**
|
||||
> previews spin forever on the "Busy" `ProgressBar` (only the non-WebView2 **image** previewer works).
|
||||
> This is **not** a product bug — a real user's Peek is spawned non-elevated by the runner. Launch it
|
||||
> the same way for CLI tests via `Start-PtNonElevated -Exe <peekExe> -Arguments '"<file>"'`
|
||||
> (`scripts/pt-nonelevated.ps1`), and confirm medium IL with `Test-ProcessElevated`. Verified 2026-07-07
|
||||
> on 0.100.1.0: elevated → NRE + endless Busy; non-elevated → `PreviewBrowser` + `RootWebArea` render.
|
||||
> The Shell-COM + Ctrl+Space path (entry-path 2) already spawns Peek non-elevated via the runner.
|
||||
|
||||
**Cannot use for**: navigation tests (Recipes 4-7, 10-11) — source has `if (_isFromCli) return;` guard that disables arrow navigation, and CLI mode spawns a fresh process every call (no pin-state-across-reopen).
|
||||
|
||||
### 2. Shell.Application COM + Ctrl+Space — Explorer-driven, supports navigation
|
||||
This is the canonical "do what a real user would do" path that drives all the navigation/pin tests.
|
||||
|
||||
```powershell
|
||||
# Dot-source helpers first
|
||||
. "$skill\scripts\pt-explorer-com.ps1"
|
||||
. "$skill\scripts\pt-sendinput-chord.ps1"
|
||||
|
||||
# Set up multi-file selection in Explorer + trigger Peek in one call:
|
||||
$peekHwnd = Invoke-PtPeekWithExplorerSelection `
|
||||
-FolderPath 'D:\fixtures' `
|
||||
-FileNames 'test-markdown.md','test-html.html','test-source.cs'
|
||||
|
||||
# Now Peek is open over a 3-file IShellItemArray. Test:
|
||||
winapp ui invoke 'PinButton' -w $peekHwnd # pin
|
||||
# (move window via SetWindowPos)
|
||||
Send-PtChord -Key 0x27 # Right arrow → switch file
|
||||
# verify the pinned position stuck
|
||||
```
|
||||
|
||||
**Use for**: pin behavior, multi-file navigation, file switching (Recipes 4-7, 10-11).
|
||||
|
||||
**Requires**: interactive desktop session (`Test-PtInteractiveDesktop` must show both `ForegroundOk=True` and `ShellComOk=True`).
|
||||
|
||||
### 3. Named Event signal — quick smoke
|
||||
```powershell
|
||||
Invoke-PtSharedEvent -Name 'Peek.Show'
|
||||
```
|
||||
Wakes the resident Peek process (different from CLI back-door — respects current Explorer foreground selection). Used by some framework tests for the "Peek is enabled and listening" assertion.
|
||||
|
||||
## Recipes — a control/observation map, NOT a per-test-case answer key
|
||||
|
||||
> Maps each Peek *capability* to **how to drive it** and **where the result shows**. It does NOT prescribe concrete fixtures/coords/inputs or expected values — design those at runtime from the actual checklist item. Only a real UI/behavior change should force an edit here.
|
||||
|
||||
| # | Capability | Drive (control / command) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | File-type previewer renders (image / text+code / markdown / PDF / HTML / archive / unsupported) | `Peek.UI.exe <fixture>` (entry-path 1) → `winapp ui inspect -w <hwnd> --depth 7` | the type's previewer node present (`ImagePreview Image`; `PreviewBrowser Pane` for dev/text/md/HTML; archive tree for zip; File-Type/Size/Date view for unsupported). Prefer `winapp ui search` for an in-fixture marker over OCR |
|
||||
| 2 | "Open with default app" via button | `winapp ui invoke LaunchAppButton` | a new editor process/window for `<file>` appears (PID diff) |
|
||||
| 3 | "Open with default app" via Enter | `Assert-PtForegroundOrAbort` → `Send-PtChord -Key <Enter>` | same as #2 |
|
||||
| 4 | Pin keeps window position when switching files | Shell COM + Ctrl+Space (entry-path 2) → `winapp ui invoke PinButton` → move window → navigate to next file | window stays at the pinned coordinates |
|
||||
| 5 | Pin position persists across close + reopen | pinned → Esc to close (graceful — **don't `Stop-Process`**, it bypasses the pin-save handler) → reopen via Shell COM + Ctrl+Space | new window opens at the same pinned coordinates |
|
||||
| 6 | Unpin releases the lock; switching file reverts to default | `winapp ui invoke PinButton` again (unpin) → navigate | window moves to the default position |
|
||||
| 7 | Unpinned reopen uses default position | unpinned → Esc-close → reopen | new window at default, not the stale pinned coords |
|
||||
| 8 | `Peek.UI.exe <file>` CLI opens Peek | entry-path 1 | covered by #1 across file types |
|
||||
| 9 | Concurrent Peek sessions don't crash/interfere | launch `Peek.UI.exe` several times on different files, leaving windows open | each spawns its own process/window; no error in `Peek\Logs` |
|
||||
| 10 | Arrow keys cycle between selected files | Shell COM multi-file selection → Ctrl+Space → `Send-PtChord` Right/Left | window title updates to each file in sequence, wraps at the ends |
|
||||
| 11 | Multi-file selection scopes navigation | select a subset of a folder → navigate | only the selected files cycle, not the rest |
|
||||
| 12 | Activation-hotkey reassignment takes effect | edit `Peek\settings.json` `properties.ActivationShortcut` → `Restart-PtRunner` (**not hot-reloaded** — see Gotchas) → press the new chord, then the old chord | new chord opens Peek; old chord does nothing |
|
||||
|
||||
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control and design your own inputs + assertions for *that* item. If no row matches, it's a NEW capability — drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
|
||||
|
||||
|
||||
|
||||
## BLOCKED traps (single source of truth)
|
||||
|
||||
If the agent only tried the CLI back-door and marked the pin / navigation tests BLOCKED → **misdiagnosis**, try entry-path #2 (Shell.Application COM + Ctrl+Space).
|
||||
|
||||
If the agent tried Shell COM + Ctrl+Space and got `GetForegroundWindow()=0` + `SendInput → ACCESS_DENIED (5)` → **environment**, not framework. The session has no attached input desktop (RDP minimized, screen locked, screensaver). See `SKILL.md` pitfall #7 and `references/environment-setup.md`. Mark BLK-ENV with mitigation citation.
|
||||
|
||||
Module quirks that mislead driving:
|
||||
- **Elevated Peek breaks WebView2 previews.** Launching `Peek.UI.exe` from an elevated shell → high-IL Peek → `PreviewWV2_Loaded` NRE + endless "Busy" for dev/md/PDF/HTML (image still works). Launch **non-elevated** (`Start-PtNonElevated`); NOT a product bug. See entry-path 1 warning.
|
||||
- **Activation-shortcut is NOT hot-reloaded.** Editing `Peek\settings.json` `ActivationShortcut` does nothing until `Restart-PtRunner`. Restart after the change AND after restoring.
|
||||
- **PinButton spawns a `PopupHost` teaching-tip** (~192x63) that surfaces first in `list-windows`. Match by title suffix `- Peek`, or cache the Peek HWND before invoking PinButton.
|
||||
- **Win11 Notepad tabs/session-restore** muddy open-with-default tests: spawned Notepad restores prior tabs. Match `"<file> - Notepad"` explicitly.
|
||||
- **Don't `Stop-Process PowerToys.Peek.UI`** to close between iterations — bypasses the pin-save handler. Use Esc / `winapp ui invoke CloseButton`.
|
||||
- CLI back-door does NOT support navigation (`_isFromCli` guard); use Shell COM + Ctrl+Space for nav. Don't OCR when UIA exposes `ImagePreview`/`PreviewBrowser`/`PinButton`.
|
||||
|
||||
## Fixture files needed
|
||||
|
||||
Put these in a workspace `fixtures/` folder before starting:
|
||||
- `small-image.png` (any 200x150 PNG)
|
||||
- `Program.cs` (any C# file)
|
||||
- `readme.md` (markdown with H1 + bold + bullet list)
|
||||
- `test-pdf.pdf` (PDF with embedded text "PDF_FIXTURE_OK" + "PDF_MARKER_42")
|
||||
- `page.html` (HTML with `<h1>` containing "HTMLPEEKMARKER")
|
||||
- `archive.zip` (zip containing 1 small text file)
|
||||
- `unsupported.xyz` (any small binary)
|
||||
- 3 differently-sized images for the pin-position tests (e.g. 320x240, 800x600, 1920x1080)
|
||||
|
||||
## Source citations
|
||||
|
||||
- `src\modules\Peek\Peek.UI\PeekXAML\App.xaml.cs:106-134` — CLI arg parsing, `_isFromCli` flag, OnShowPeek call.
|
||||
- `src\modules\Peek\Peek.UI\PeekXAML\Models\NavigationManager.cs` — `if (_isFromCli) return;` guards.
|
||||
- `src\common\interop\shared_constants.h` — `ShowPeekEvent` name.
|
||||
@@ -1,135 +0,0 @@
|
||||
# PowerRename — module verification profile
|
||||
|
||||
**PT module**: `PowerRename` (bulk-rename UI launched via Explorer context menu on selected files/folders)
|
||||
**Source**: `<PT-repo>\src\modules\PowerRename\` (PT repo)
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\settings.json`
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\Logs\v<ver>\log_<date>.log`
|
||||
**Exe**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.PowerRename.exe`
|
||||
**Activation**: Explorer right-click → "Rename with PowerRename" (Win11 Tier-1 menu; **no classic HKCR verb on Win11**); optional global hotkey if user-configured
|
||||
**Last verified**: `0.100.2.0` (2026-07-08 — full 17-item run)
|
||||
|
||||
## Shared mechanics
|
||||
|
||||
For the context-menu machinery (the two openers, packaged-menu facts, BLK-ENV, honesty guard) see
|
||||
**`references/explorer-context-menu-flow.md`** + **`scripts/pt-explorer-contextmenu.ps1`**. Don't
|
||||
duplicate; cite by section.
|
||||
|
||||
**Context-menu routing (PowerRename-specific):**
|
||||
- **Caption:** `Rename with PowerRename` (Win11 Tier-1). Launched exe: `PowerToys.PowerRename.exe`.
|
||||
- **Which menu:** appears on **both** the selected-file menu **and** the folder-**background** menu.
|
||||
Prefer **`Open-PtExplorerContextMenu -FileName <f>`** (COM-select a real file → Shift+F10, coordinate-free
|
||||
and with a **verifiable** target) for present/absent/icon/extended-menu assertions. The folder-background
|
||||
opener (`Open-PtBackgroundContextMenu`) also works since PR registers there, but it depends on "focus in
|
||||
the file-list + nothing selected"; use it only if a test specifically asserts background-menu presence.
|
||||
Open **one menu per fresh Explorer window**, and let the menu **settle ~0.5s** before reading (the PR entry
|
||||
populates a beat after the base verbs).
|
||||
|
||||
For the Win11 IExplorerCommand vs classic HKCR distinction, see `scripts/pt-shell-verbs.ps1` header — PR is **modern-menu-only on Win11**, so classic-verb enumeration via Shell.Application **will not find it**.
|
||||
|
||||
## Entry-paths (try in order)
|
||||
|
||||
### 1. Direct CLI launch with file args — PREFERRED for UI-driven tests (verified 2026-06-10)
|
||||
```powershell
|
||||
$tmp = New-Item -ItemType Directory -Path "$env:TEMP\pr-fixture-$(Get-Random)"
|
||||
1..3 | ForEach-Object { 'x' | Set-Content "$($tmp.FullName)\file$_.txt" }
|
||||
|
||||
# quote each path — Start-Process -ArgumentList joins args with spaces and does NOT auto-quote,
|
||||
# so a path containing a space (e.g. "Hi World.txt") gets split into invalid args → empty file list.
|
||||
$files = Get-ChildItem $tmp.FullName -File | ForEach-Object { '"{0}"' -f $_.FullName }
|
||||
Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.PowerRename.exe" -ArgumentList $files
|
||||
|
||||
Start-Sleep -Milliseconds 1500
|
||||
Force-PtForeground -AppId PowerToys.PowerRename # bring PR to top — else it opens BEHIND the temp Explorer window (pitfall #13); needed for recordings + reliable screenshots
|
||||
# main window HWND — skip transient PopupHost/tooltip windows (see references/winapp-ui-testing.md)
|
||||
$pr = (winapp ui list-windows -a PowerToys.PowerRename --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.title -ne 'PopupHost' } | Select-Object -First 1).hwnd
|
||||
winapp ui inspect -w $pr --depth 5 -i 2>$null | Out-String | Select-String 'CheckBox "file\d\.txt"'
|
||||
# Expect 3 hits (file1/2/3.txt, [on] by default)
|
||||
```
|
||||
Bypasses the context menu entirely; same code path inside the exe (it parses argv as the file list). **Use for every UI-driven option/regex/preview test** (Recipes 3–11 below).
|
||||
|
||||
### 2. Synthetic right-click + Invoke-PtContextMenuItem — for "menu entry present/absent" assertions (Recipes 1–2)
|
||||
Use the openers from `references/explorer-context-menu-flow.md`. **Prefer selecting a real file** — it gives
|
||||
a deterministic, verifiable target and opens **coordinate-free** (COM-select → Shift+F10). PowerRename also
|
||||
appears on the folder background, so `Open-PtBackgroundContextMenu` is a valid alternative, but the file
|
||||
opener is more robust. The menu-presence assertion is the ONE thing the CLI back-door cannot prove (it works
|
||||
even if the entry is correctly hidden — the false-positive trap in that doc).
|
||||
|
||||
```powershell
|
||||
. "$skill\scripts\pt-explorer-contextmenu.ps1"
|
||||
# Disposable fixtures folder (same convention as Entry-path 1)
|
||||
$fx = New-Item -ItemType Directory -Path "$env:TEMP\pr-fixture-$(Get-Random)"
|
||||
'x' | Set-Content "$($fx.FullName)\a.txt"
|
||||
# Open Explorer on it and grab its CabinetWClass HWND
|
||||
$hwnd = Open-PtExplorerWindow -Path $fx.FullName
|
||||
$menu = Open-PtExplorerContextMenu -ExplorerHwnd $hwnd -FileName 'a.txt' # COM-select + Shift+F10 (coordinate-free)
|
||||
Start-Sleep -Milliseconds 500 # let the menu settle (PR populates after base verbs)
|
||||
$items = Get-PtContextMenuItems -MenuHwnd $menu # returns MenuItem name strings
|
||||
$has = $items | Where-Object { $_ -match 'Rename with PowerRename' }
|
||||
# assert $has -> entry present
|
||||
```
|
||||
|
||||
### 3. Shell COM classic verb (does NOT work on Win11 stock install)
|
||||
```powershell
|
||||
Invoke-PtShellVerb -Path "$($fx.FullName)\a.txt" -NamePattern 'PowerRename' # -> False (reuses $fx from Entry-path 2)
|
||||
```
|
||||
Returns False on Win11 because PT registers PR only via IExplorerCommand, not as a classic HKCR shell verb. **Use only for negative checks** (and prefer the synthetic-menu enumeration above, which observes the actual Tier-1 menu).
|
||||
|
||||
## Recipes — a capability → control map (NOT a mini-checklist)
|
||||
|
||||
> **What this is (and isn't):** it maps each PowerRename *capability* to **which control drives it** (AutomationId / settings key) — nothing more. It is **not** the checklist: the checklist owns the *inputs* AND the *expected result*; this table only says *how to reach & poke the control*. **Where/how to read the outcome** is in the Read-out notes below — no expected values live here. That keeps the table stable across checklist rewordings; only a real UI redesign should force an edit.
|
||||
|
||||
| # | Capability | Control — how to drive it (AutomationId / settings key) |
|
||||
|---|---|---|
|
||||
| 1 | Context-menu entry present when enabled, gone when disabled | Settings-UI enable toggle: `Set-PtModuleEnabledViaSettingsUI -PageTag PowerRename -EnabledKey PowerRename` (see `references/explorer-context-menu-flow.md`) |
|
||||
| 2 | Shell integration: menu **placement** + **icon** (4-combination matrix) | **Placement:** *Show PowerRename in* dropdown → "Default and extended" (`ExtendedContextMenuOnly=false`) vs "Extended only" (`=true`). **Icon:** expand that row's ˅ → **"Hide icon in context menu"** checkbox (`ShowIcon`/`bool_show_icon_on_menu`, **inverted**: checked = hide). Both persist to `power-rename-settings.json`, re-read per menu build (no Explorer restart). |
|
||||
| 3 | Any search/replace option toggle (regex, match-all, case-sensitive, autocomplete, last-use) | `winapp ui invoke checkBox_regex` / `checkBox_matchAll` / `checkBox_case` (etc.); persists to `power-rename-settings.json` |
|
||||
| 4 | Case conversion — lowercase / UPPERCASE / Title Case / Capitalize (mutually exclusive) | `toggleButton_lowerCase` / `upperCase` / `titleCase` / `capitalize` (single-select) |
|
||||
| 5 | Which item types get renamed — include/exclude Files, Folders, Subfolders | `toggleButton_includeFiles` / `includeFolders` / `includeSubfolders` |
|
||||
| 6 | Apply to whole "name + extension" / name only / extension only | `comboBox_renameParts` → items `itm-filenameonly` / `itm-extensiononly` (default = filename + extension) |
|
||||
| 7 | Enumerate items — inserts an incrementing counter | `toggleButton_enumItems`; Replace accepts `${start=,increment=,padding=}`. **Prerequisite: a matching Search** — enumeration only substitutes on matched rows (empty Search → no rename, Apply disabled); use `.*` with regex on to rewrite the whole name. |
|
||||
| 8 | Datetime tokens | Replace accepts `$DD` `$MMMM` `$YYYY` `$hh` `$mm` `$ss` `$fff` |
|
||||
| 9 | Boost library — extended/Perl regex (e.g. lookbehind) beyond .NET | `UseBoostLib` key in `power-rename-settings.json` — read at launch, so relaunch PR after toggling |
|
||||
| 10 | Per-row include/exclude in the preview | invoke a preview row's checkbox to uncheck it |
|
||||
| 11 | Filter preview / select-all (NOT a column-header click — `TxtBlock_Original`/`TxtBlock_Renamed` are non-interactive labels) | `btn-filter-XXXX` → `button_showAll` / `button_showRenamed`; `checkBox_selectAll` |
|
||||
|
||||
> **Read-out notes** — where/how to read each outcome (the *expected value* always lives in the checklist, never here):
|
||||
> - **Preview Renamed column** (rows 3–10): the Renamed value is the row's **last** `lbl-…` Text — `winapp ui inspect <itm-row> -w $hwnd --depth 4` (no `-i`; the first `lbl-…` is the Original). Or `winapp ui get-property <chkId> --property HelpText` with the **resolved** CheckBox id (e.g. `chk-0-2806` from `winapp ui search '<orig-name>' -w $hwnd --json`) — not the numeric Name (`0`) nor the `itm-…` id (both return `null`). Src: `ExplorerItem.xaml` `AutomationProperties.HelpText`→Renamed.
|
||||
> - **Preview must be populated first** (rows 7–11): after setting Search/Replace, wait ~500 ms for the regex engine before reading rows or invoking row/filter controls.
|
||||
> - **Menu presence** (rows 1–2): read the synthetic menu with `Get-PtContextMenuItems` (see `references/explorer-context-menu-flow.md`). Locked desktop → `BLK-ENV`.
|
||||
> - **Icon, per menu** (row 2): modern tier-1 → the PR `MenuItem` gains/loses a UIA child `Image` (via `inspect`); legacy/extended `#32768` → the icon is a Win32 `MIIM_BITMAP` **invisible to UIA**, so pixel-check the row's icon gutter in a **screenshot** (not OCR). Src `PowerRenameContextMenu/dllmain.cpp GetIcon` (modern), `PowerRenameExt.cpp` `MIIM_BITMAP` (legacy).
|
||||
> - **Placement** (row 2): "Extended only" → PR **absent from tier-1**, **present under a genuine Shift+right-click** (`CMF_EXTENDEDVERBS`) — NOT the non-Shift "Show more options" (`#32768`) menu.
|
||||
> - **Enumerate behavior** (row 7): the counter advances **only for rows the Search matched** (`enumIndex++` on replacement; empty Search returns early — `PowerRenameRegEx.cpp`); non-matching rows don't consume an index. Formula: `start + index*increment` (0-based), zero-padded to `padding` digits (`lib/Enumerating.h`).
|
||||
|
||||
> **Mapping process**: read the actual checklist item → find its capability row → drive the named control (discovering control IDs at runtime where the row says so) → read the outcome via the Read-out notes → assert against the checklist's *own* expected value. No row matches ⇒ a NEW capability: drive it ad-hoc, then add a row (capability + control only).
|
||||
|
||||
|
||||
|
||||
## Fixture files needed
|
||||
|
||||
In a workspace `fixtures/` folder:
|
||||
- `a.txt`, `b.txt`, `c.txt` — multi-select
|
||||
- `IMG_001.png`, `IMG_002.png`, `IMG_003.png` — regex capture
|
||||
- subfolder `subdir/` with 2 inner files — folder/subfolder exclusion
|
||||
- `Foo_A_A_A.txt` — match-all
|
||||
- `MIXED.txt` — case-sensitive
|
||||
|
||||
Always copy fixtures to a disposable temp folder before running actual rename operations.
|
||||
|
||||
## BLOCKED traps
|
||||
|
||||
- **TWO settings files — PR reads `power-rename-settings.json`, NOT `settings.json`** (verified 2026-06-10). `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\` holds both: (1) `settings.json` = PT-store, keys `bool_mru_enabled`/`bool_persist_input`/`bool_show_icon_on_menu`/`bool_show_extended_menu`/`bool_use_boost_lib`/`int_max_mru_size` (what `Get-PtModuleSettings` + the Settings UI bind to); (2) `power-rename-settings.json` = the module's own store, keys `ShowIcon`/`ExtendedContextMenuOnly`/`PersistState`/`MRUEnabled`/`MaxMRUSize`/`UseBoostLib` — **this is the file the PR UI exe and the context-menu COM handlers actually read at launch** (`lib/Settings.cpp` `CSettings::Load→ParseJson`). The runner (`dll/dllmain.cpp`, settings-changed handler) syncs PT-store→module-store only on a Settings-UI *change event*; the PT-store file can sit stale for days. **To drive ShowIcon / ExtendedContextMenuOnly / MRUEnabled / PersistState / UseBoostLib deterministically, edit `power-rename-settings.json` directly, then restore.** The **context-menu** settings (ShowIcon, ExtendedContextMenuOnly) are re-read **per menu build** - no Explorer restart, just re-open the menu; the **PR UI-exe** settings (MRUEnabled, PersistState, UseBoostLib) are read at launch, so relaunch `PowerToys.PowerRename.exe`. Map (settings.json key → user-facing control): ShowIcon→"Hide icon in context menu" checkbox (**inverted**; inside the *Show PowerRename in* expander), ExtendedContextMenuOnly→"Show PowerRename in" dropdown ("Extended context menu only"), MRUEnabled→autocomplete, PersistState→"Show values from last use", UseBoostLib→"Use Boost library". MRU values live in `search-mru.json`/`replace-mru.json`; last-used (persist) in `power-rename-last-run-data.json`.
|
||||
- **Opening the extended / legacy menus** — the classic `#32768` ("Show more options") menu IS winapp-enumerable (`Open-PtShowMoreOptionsMenu` → `Get-PtContextMenuItems`; see `references/explorer-context-menu-flow.md` → "Reading the legacy menu"), **but it does not pass `CMF_EXTENDEDVERBS`**, so it is the **wrong observer for "extended-only"** (PR is correctly absent there). To see the extended entry, open a **genuine Shift+right-click** menu: `Open-PtShiftRightClickMenu -ExplorerHwnd <h> -FileName <f>` (Shift held around an element-resolved right-click on the COM-selected file → `FindWindow('#32768')`), then `Get-PtContextMenuItems`. *(What to assert lives in Read-out → Placement.)* Src: `PowerRenameContextMenu/dllmain.cpp` `GetState` returns `ECS_HIDDEN` (hides it from Tier-1); `PowerRenameExt.cpp` `QueryContextMenu` returns `E_FAIL` unless `CMF_EXTENDEDVERBS`.
|
||||
- **PR registers on the directory *background* menu too**, but menu targeting (prefer selecting a real file over the background menu; one menu per fresh window; settle ~0.5s) is already covered once in "Context-menu routing" + Entry-path 2 above — don't re-derive it here.
|
||||
- **`set-value` on search/replace DOES fire the preview** (TextChanged works, unlike CmdPal) — Apply button enabling/disabling is a reliable match/no-match signal. The search/replace Edit AutomationIds are random per launch (`txt-textbox-XXXX`); discover them each launch by name (`Edit "Search for"` / `Edit "Replace with"`).
|
||||
- **Disk mutation is real** — run renames against `$env:TEMP\pr-test-<random>`, not real fixtures.
|
||||
- **Foreground + window hygiene (pitfall #13):** the PR window (and Settings) can open **behind** the temp Explorer window you opened for the context-menu test, so a recording/screenshot shows the wrong window. **(a)** After `Start-Process …PowerToys.PowerRename.exe` (or opening Settings), call `Force-PtForeground -AppId PowerToys.PowerRename` (or `-AppId PowerToys.Settings`) before observing. **(b)** Since each menu test opens a **fresh** Explorer window, close them **per item** so they don't stack in the foreground: `(New-Object -ComObject Shell.Application).Windows() | Where-Object { $_.LocationURL -match 'pr-fixture|pr-enum' } | ForEach-Object { $_.Quit() }` (match your disposable-fixture folder name so you never close the user's Explorer windows).
|
||||
- **COM cache staleness** when re-checking verbs after enable/disable — call `Reset-PtShellComCache` from `scripts/pt-shell-verbs.ps1`.
|
||||
- **Don't** try `Invoke-PtShellVerb 'PowerRename'` — returns False on Win11 (no classic registration). Use synthetic menu via `Invoke-PtContextMenuItem` or direct-CLI.
|
||||
- **Don't skip the synthetic-menu test for menu-presence** — the CLI back-door false-PASSes when the entry is correctly hidden (see `references/explorer-context-menu-flow.md`).
|
||||
|
||||
## Source citations
|
||||
|
||||
- `src\modules\PowerRename\dllmain.cpp` — IExplorerCommand registration (no classic HKCR shadow on Win11).
|
||||
- `src\modules\PowerRename\PowerRenameUILib\` — XAML for main PR window (toggle/checkbox AutomationIds).
|
||||
- `src\modules\PowerRename\PowerRenameLib\Settings.cpp` — settings.json schema canonical property names.
|
||||
@@ -1,122 +0,0 @@
|
||||
# Pre-flight checks, bootstrap, and state hygiene
|
||||
|
||||
This doc covers the **agent-runtime** environment probing and lifecycle hooks. Read alongside `SKILL.md` (the playbook) and `references/environment-setup.md` (one-time user env prep).
|
||||
|
||||
## Pre-flight checks (do these first; abort if any fails)
|
||||
|
||||
1. **Admin check** — `Test-PtAdmin` must return the elevation level matching `[ADMIN: YES]` items in the module's checklist. If the module contains `[ADMIN: YES]` items and `Test-PtAdmin` returns `False`, **STOP** and tell the user "this module requires an elevated session". Do NOT silently mark those items BLOCKED-LACK-ADMIN — that hides a fixable env issue.
|
||||
|
||||
2. **PT runner present** — `Test-PtRunnerAdmin` should show the runner exists. If it doesn't exist, start PowerToys (`Start-Process "$env:LOCALAPPDATA\PowerToys\PowerToys.exe"`).
|
||||
|
||||
3. **Module installed** — `Get-PtModuleSettings -ModuleDir <ModuleDir>` (or `Get-CmdPalSettings` for CmdPal) returns non-null.
|
||||
|
||||
4. **Interactive-desktop availability + session attachment** — the single most common cause of false-BLOCKED reports is a session mismatch where the agent runs in an elevated **non-console session** (e.g. RDP that's been disconnected/minimized, fast user switching, run-as-different-user, or scheduled-task-with-highest-privilege). In that scenario `Test-PtAdmin=True` but `GetForegroundWindow()=0` and `SendInput` returns `ERROR_ACCESS_DENIED (5)` — input injection cannot reach the active desktop.
|
||||
|
||||
```powershell
|
||||
# Sessions
|
||||
$agentSession = [Diagnostics.Process]::GetCurrentProcess().SessionId
|
||||
$consoleSession = (Get-Process explorer -EA SilentlyContinue | Select-Object -First 1).SessionId
|
||||
"Agent session=$agentSession Console explorer session=$consoleSession"
|
||||
|
||||
# Foreground + Shell COM probe (use scripts/pt-session-diagnose.ps1 for the full version)
|
||||
Add-Type 'using System; using System.Runtime.InteropServices; public class FG4 { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }'
|
||||
$hasFg = $false
|
||||
for ($i = 0; $i -lt 5; $i++) { if ([FG4]::GetForegroundWindow() -ne [IntPtr]::Zero) { $hasFg=$true; break }; Start-Sleep -Milliseconds 200 }
|
||||
$shellOk = $false
|
||||
try { $shellOk = (@((New-Object -ComObject Shell.Application).Windows()).Count -ge 0) } catch {}
|
||||
"Interactive desktop: ForegroundOk=$hasFg ShellComOk=$shellOk"
|
||||
|
||||
if (-not $hasFg -and $agentSession -ne $consoleSession) {
|
||||
Write-Host "===========================================================" -ForegroundColor Red
|
||||
Write-Host "NON-INTERACTIVE SESSION DETECTED" -ForegroundColor Red
|
||||
Write-Host "Agent is in Session $agentSession but the active console is Session $consoleSession." -ForegroundColor Red
|
||||
Write-Host "SendInput, global hotkeys, and arrow-key navigation will NOT work here." -ForegroundColor Red
|
||||
Write-Host "Items requiring input injection will be marked BLK-ENV up-front." -ForegroundColor Red
|
||||
Write-Host "Mitigation: see references/environment-setup.md, or relaunch in console session:" -ForegroundColor Yellow
|
||||
Write-Host " psexec -accepteula -h -i $consoleSession -s pwsh.exe" -ForegroundColor Yellow
|
||||
Write-Host "===========================================================" -ForegroundColor Red
|
||||
# Continue verification — schema/UIA/CLI-based tests still produce real evidence
|
||||
}
|
||||
```
|
||||
|
||||
**Key distinction** (all rows assume `Admin=True`):
|
||||
- **ForegroundOk + ShellComOk** → Everything works — interactive elevated session.
|
||||
- **ShellComOk only (ForegroundOk false)** → Non-interactive (e.g. Session ≠ console, RDP minimized, screen locked, screensaver). Only schema / UIA-invoke / CLI / Named-Event tests work. Mark input-injection items as `BLK-ENV` and **cite `references/environment-setup.md` in the report** so the user can fix env and re-run.
|
||||
- **Neither (ShellComOk false)** → Session 0 / service context — even Shell COM fails. Very few tests possible.
|
||||
|
||||
5. **Discipline: try AT LEAST 2 distinct entry-paths before marking BLOCKED.** For Peek/FZ/Workspaces/Image Resizer/PowerRename/File Locksmith specifically, the obvious entry-path is the global hotkey but Shell.Application COM driving Explorer also works — see per-module profiles under `references/modules/`. Marking BLOCKED after trying only the CLI launch (a common trap) hides easily-PASS-able items in an interactive session.
|
||||
|
||||
## Bootstrap (paste at start of your verification script)
|
||||
|
||||
```powershell
|
||||
$skill = '<this skill folder>' # the folder containing SKILL.md
|
||||
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
|
||||
|
||||
$workspace = "$env:TEMP\verify-<Module>-$(Get-Date -Format yyyyMMdd-HHmmss)"
|
||||
New-Item -ItemType Directory -Path $workspace, "$workspace\artifacts" | Out-Null
|
||||
$report = "$workspace\verify-<Module>.md"
|
||||
|
||||
"# <Module> verification — $(Get-Date -Format 'yyyy-MM-dd HH:mm')" | Set-Content $report
|
||||
"" | Add-Content $report
|
||||
"## Pre-flight" | Add-Content $report
|
||||
"- IsAdmin: $(Test-PtAdmin)" | Add-Content $report
|
||||
"- PT runner: PID=$((Test-PtRunnerAdmin).Pid) Elevated=$((Test-PtRunnerAdmin).Elevated)" | Add-Content $report
|
||||
|
||||
# Then proceed with pre-flight checks #4-#6 above and write their results into the report.
|
||||
```
|
||||
|
||||
## State hygiene (CRITICAL — always restore)
|
||||
|
||||
Wrap any settings/registry mutation in try/finally:
|
||||
|
||||
```powershell
|
||||
# Per-item: settings.json edits
|
||||
$bk = Backup-PtModuleSettings -ModuleDir <ModuleDir>
|
||||
try {
|
||||
# ... mutate + assert ...
|
||||
} finally {
|
||||
Restore-PtModuleSettings -ModuleDir <ModuleDir> -BackupPath $bk
|
||||
}
|
||||
|
||||
# After GPO/admin tests
|
||||
Remove-Item HKLM:\Software\Policies\PowerToys -Recurse -Force -EA SilentlyContinue
|
||||
Remove-Item HKCU:\Software\Policies\PowerToys -Recurse -Force -EA SilentlyContinue
|
||||
Remove-Item 'C:\Windows\PolicyDefinitions\PowerToys.admx' -Force -EA SilentlyContinue
|
||||
Remove-Item 'C:\Windows\PolicyDefinitions\en-US\PowerToys.adml' -Force -EA SilentlyContinue
|
||||
|
||||
# Spawned processes (notepad, regedit, etc.) — kill by PID, not by name
|
||||
foreach ($pid in $spawnedPids) { Stop-Process -Id $pid -Force -EA SilentlyContinue }
|
||||
```
|
||||
|
||||
## Final wrap-up (run AFTER all per-item tables are written)
|
||||
|
||||
1. **Run state-hygiene cleanup** above for everything that wasn't restored per-item.
|
||||
2. **Write the top-of-report summary** per `references/reporting-format.md` §B.
|
||||
3. **Write the §G Retrospective** — reflect on the run itself: every friction (classified by source + severity + minutes/attempts cost + suggested fix), or `Everything was smooth — no friction encountered.` See `references/reporting-format.md` §G. Don't skip it; it's how the skill improves.
|
||||
4. **Verify every screenshot referenced in the report actually exists on disk** (before the move, while paths still resolve under `$workspace`):
|
||||
```powershell
|
||||
$missing = Get-Content $report | Select-String 'artifacts/L\d+/step-\d+-[^\.\s]+\.(png|txt|log|json|ps1)' -AllMatches |
|
||||
ForEach-Object { $_.Matches.Value } | Sort-Object -Unique |
|
||||
Where-Object { -not (Test-Path (Join-Path $workspace $_)) }
|
||||
if ($missing) { Write-Warning "Missing artifacts: $($missing -join ', ')" }
|
||||
```
|
||||
5. **Move the workspace to the sign-off archive** (LAST step, after the report + artifact check pass):
|
||||
```powershell
|
||||
$signoff = "$env:OneDrive\PowerToys\Module-Signoff"
|
||||
New-Item -ItemType Directory -Path $signoff -Force | Out-Null
|
||||
$final = Join-Path $signoff (Split-Path $workspace -Leaf)
|
||||
Move-Item -Path $workspace -Destination $final -Force
|
||||
$report = Join-Path $final (Split-Path $report -Leaf)
|
||||
```
|
||||
The report uses **relative** `artifacts/…` paths, so the whole tree moves intact.
|
||||
6. **Print the FINAL (moved) report path** as the very last line of your response — the `…\Module-Signoff\verify-<Module>-<timestamp>\verify-<Module>.md` path, NOT the temp path.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Never silently send keys via SendInput** to a target window without first calling `Assert-PtForegroundOrAbort -AppId <id>`. Keys silently leak to your terminal if the target isn't foreground.
|
||||
- **Never mark BLOCKED without trying at least 2 distinct entry-paths from the drive-stack** (SKILL.md §2). If you can't drive the item, name the specific obstacle (not "I can't").
|
||||
- **Never assume any external repo is cloned locally.** The helpers under `scripts/` are self-contained. Use `Test-Path` guards before referencing any external path.
|
||||
- **Never invent test steps for a `[CLARITY: VAGUE-*]` item** — mark it **FAIL (cause: checklist-ambiguous)** and quote the original wording so the user can fix the checklist. The checklist is test code; an undefinable test is a broken test.
|
||||
- **Always restore state** before exiting (even on error). State hygiene wraps every mutation in try/finally.
|
||||
- **Separate the two FAIL causes**: *product* FAILs are bugs to file; *checklist* FAILs (stale feature or ambiguous spec) are items to rewrite/prune. If a large share of a module's items are checklist-FAILs, the checklist needs an overhaul before re-verifying — don't punt drivable items into a FAIL.
|
||||
- **Never continue past 3 consecutive errors against the same item** — mark it BLOCKED with the concrete symptom/obstacle and move on. Per-item budget is ~5 minutes; if stuck longer, it's BLOCKED (name the wall).
|
||||
@@ -1,72 +0,0 @@
|
||||
# Advanced Paste — PowerToys release checklist
|
||||
|
||||
> Source: baseline split from `release-checklist-annotated.md` (0.96-aligned), extended with items
|
||||
> derived from Advanced Paste PRs merged v0.96 → v0.100 (see "New / updated since v0.96"). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with two metadata tags:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin
|
||||
|
||||
**Clarity**:
|
||||
- (no marker) - clear, has explicit assert
|
||||
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
|
||||
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
|
||||
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
|
||||
- `[REWRITTEN]` - original wording was vague; rewritten concrete. Original preserved in italics.
|
||||
- `[EXTERNAL: API-KEY]` - needs an AI provider key/endpoint; without one the item is `BLK-EXTERNAL`, not a failure.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Paste (32 items)
|
||||
|
||||
### Plain text / Markdown / JSON (baseline)
|
||||
|
||||
> Fixture: each paste test below begins by copying source content. Re-copy fresh source before each
|
||||
> paste so the source format is on the clipboard; the asserts are the "paste …" lines.
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L866) Copy rich text (mixed colors, bold, underline); paste with Ctrl+V into a rich editor - rich text pasted with colors/formatting intact
|
||||
- [ ] **[ADMIN: NO]** (L867) Copy rich text; paste with the Paste-as-Plain-Text hotkey - plain text pasted, formatting stripped
|
||||
- [ ] **[ADMIN: NO]** (L868) Paste again with Ctrl+V - still plain (Paste-as-Plain leaves the clipboard as plain UnicodeText, HTML/RTF removed)
|
||||
- [ ] **[ADMIN: NO]** (L870) Copy rich text; open AP, click "Paste as plain text" - plain text pasted, formatting stripped
|
||||
- [ ] **[ADMIN: NO]** (L872) Copy rich text; open AP, press Ctrl+1 - plain text pasted, formatting stripped
|
||||
- [ ] **[ADMIN: NO]** (L874) Open Settings, set the Paste-as-Markdown direct hotkey - chord saved to settings.json
|
||||
- [ ] **[ADMIN: NO]** (L876) Copy HTML (Markdown-convertible); paste with the set hotkey - converted to Markdown
|
||||
- [ ] **[ADMIN: NO]** (L878) Copy HTML; open AP, click "Paste as markdown" - converted to Markdown
|
||||
- [ ] **[ADMIN: NO]** (L880) Copy HTML; open AP, press Ctrl+2 - converted to Markdown
|
||||
- [ ] **[ADMIN: NO]** (L882) Open Settings, set the Paste-as-JSON direct hotkey - chord saved to settings.json
|
||||
- [ ] **[ADMIN: NO]** (L884) Copy XML or CSV; paste with the set hotkey - converted to JSON
|
||||
- [ ] **[ADMIN: NO]** (L886) Copy XML or CSV; open AP, click "Paste as JSON" - converted to JSON
|
||||
- [ ] **[ADMIN: NO]** (L888) Copy XML or CSV; open AP, press Ctrl+3 - converted to JSON
|
||||
|
||||
### AI custom format (baseline)
|
||||
|
||||
- [ ] **[ADMIN: NO]** [EXTERNAL: API-KEY] (L890) Open Settings, set OpenAI key for AI custom format
|
||||
- [ ] **[ADMIN: NO]** [EXTERNAL: API-KEY] (L891) Copy text, open AP, custom input "Insert smiley after every word", verify result
|
||||
- [ ] **[ADMIN: NO]** [EXTERNAL: API-KEY] (L893) Open AP, input query, regenerate, select and paste
|
||||
- [ ] **[ADMIN: NO]** (L894) Create custom actions, set hotkey, test ctrl+<num>, enable/disable, reorder
|
||||
- [ ] **[ADMIN: NO]** (L895) Disable Custom format preview - result pasted right away
|
||||
- [ ] **[ADMIN: NO]** (L896) Disable Enable Paste with AI - Custom Input text box disabled
|
||||
|
||||
### Clipboard history & enable/disable (baseline)
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L898) Open AP, click Clipboard history, delete entry, verify gone from Win+V
|
||||
- [ ] **[ADMIN: NO]** (L899) Open AP, click Clipboard history, click entry, verify put on top of Win+V
|
||||
- [ ] **[ADMIN: NO]** (L900) Disable Windows clipboard history (Settings > System > Clipboard) - the Clipboard history button in AP is disabled/hidden
|
||||
- [ ] **[ADMIN: NO]** (L901) Disable Advanced Paste, then press its hotkeys - nothing happens (AP window does not open; no process spawned)
|
||||
|
||||
### New / updated since v0.96 (PR-derived)
|
||||
|
||||
- [ ] **[ADMIN: NO]** (#43990) Copy a hex color string (e.g. `#3478F6`), open Clipboard history in AP - that entry shows an RGB color-swatch preview next to the value
|
||||
- [ ] **[ADMIN: NO]** (#44021) Copy an image to the clipboard, open AP - image is accepted as input (image-input handling); a custom/AI action can run on it (image preview shown, no error)
|
||||
- [ ] **[ADMIN: NO]** (#44767) Select text in an editor and invoke a custom-action hotkey WITHOUT pressing Ctrl+C first - AP auto-copies the selection and acts on it; clipboard is preserved
|
||||
- [ ] **[ADMIN: NO]** (#46486) Repeat the auto-copy custom-action hotkey in an Electron/Chromium app (VS Code / Chrome) - selection is still auto-copied (no longer fails on those apps)
|
||||
- [ ] **[ADMIN: NO]** (#45242) Settings > Advanced Paste: toggle "Enable Paste with AI" off/on - the AI paste section (custom-input text box + provider selector) hides/appears in the AP window accordingly
|
||||
- [ ] **[ADMIN: NO]** [EXTERNAL: API-KEY] (#44293, #45362, #43716) Settings: switch AI provider (OpenAI / Azure OpenAI / Gemini / Foundry Local) - the configured endpoint persists per provider (Gemini does not keep an Azure placeholder; Foundry local port change is picked up on the fly)
|
||||
- [ ] **[ADMIN: NO]** (#44862, #45207, #45699) Open the Advanced Paste Settings page after upgrading from an older settings.json - page loads without crashing (settings upgraded safely)
|
||||
- [ ] **[ADMIN: NO]** (#44212) Clipboard history: select the same item twice - no duplicate entry is created
|
||||
- [ ] **[ADMIN: NO]** (#48124) With an unreadable/locked clipboard (e.g. another app holds an open clipboard handle), trigger Paste as JSON - AP fails gracefully (no crash, error toast/no-op); then copy valid XML/CSV and confirm Paste as JSON converts correctly again
|
||||
@@ -1,45 +0,0 @@
|
||||
# Environment Variables — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with two metadata tags:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
**Clarity**:
|
||||
- (no marker) - clear, has explicit assert
|
||||
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
|
||||
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
|
||||
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
|
||||
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables (20 items)
|
||||
|
||||
- [ ] **[ADMIN: YES]** (L791) Launch as administrator ON - Launch Environment Variables and confirm that SYSTEM variables ARE editable and Add variable button is enabled
|
||||
- [ ] **[ADMIN: YES]** (L792) Launch as administrator OFF - Launch Environment Variables and confirm that SYSTEM variables ARE NOT editable and Add variable button is disabled
|
||||
- [ ] **[ADMIN: NO]** (L795) Add new User variable. Open OS Environment variables window and confirm that added variable is there. Also, confirm that it's added to "Applied variables" list.
|
||||
- [ ] **[ADMIN: NO]** (L796) Edit one User variable. Open OS Environment variables window and confirm that variable is changed. Also, confirm that change is applied to "Applied variables" list.
|
||||
- [ ] **[ADMIN: NO]** (L797) Remove one User variable. Open OS Environment variables window and confirm that variable is removed. Also, confirm that variable is removed from "Applied variables" list.
|
||||
- [ ] **[ADMIN: NO]** (L801) Add new profile with no variables and name it "Test_profile_1" (referenced below by name)
|
||||
- [ ] **[ADMIN: NO]** (L802) Edit "Test_profile_1": Add one new variable to profile e.g. name: "profile_1_variable_1" value: "profile_1_value_1"
|
||||
- [ ] **[ADMIN: NO]** (L803) Add new profile "Test_profile_2": From "Add profile dialog" add two new variables (profile_2_variable_1:profile_2_value_1 and profile_2_variable_2:profile_2_value_2). Set profile to enabled and click Save. Open OS Environment variables window and confirm that all variables from the profile are applied correctly. Also, confirm that "Applied variables" list contains all variables from the profile.
|
||||
- [ ] **[ADMIN: NO]** (L804) Apply "Test_profile_1" while "Test_profile_2" is still applied. Open OS Environment variables window and confirm that all variables from Test_profile_2 are unapplied and that all variables from Test_profile_1 are applied. Also, confirm that state of "Applied variables" list is updated correctly.
|
||||
- [ ] **[ADMIN: NO]** (L805) Unapply applied profile. Open OS Environment variables window and confirm that all variables from the profile are unapplied correctly. Also, confirm that "Applied variables" list does not contain variables from the profile.
|
||||
- [ ] **[ADMIN: NO]** (L808) To "Test_profile_1" add one existing variable from USER variables, e.g. TMP. After adding, change it's value to e.g "test_TMP" (or manually add variable named TMP with value test_TMP).
|
||||
- [ ] **[ADMIN: NO]** (L809) Apply "Test_profile_1". Open OS Environment variables window and confirm that TMP variable in USER variables has value "test_TMP". Confirm that there is backup variable "TMP_PowerToys_Test_profile_1" with original value of TMP var. Also, confirm that "Applied variables" list is updated correctly - there is TMP profile variable, and backup User variable..
|
||||
- [ ] **[ADMIN: NO]** (L810) Unapply "Test_profile_1". Open OS Environment variables window and confirm that TMP variable in USER variable has original value and that there is no backup variable. Also, confirm that "Applied variables" list is updated correctly.
|
||||
- [ ] **[ADMIN: NO]** (L813) In "Applied variables" list confirm that PATH variable is shown properly: value of USER Path concatenated to the end of SYSTEM Path.
|
||||
- [ ] **[ADMIN: NO]** (L814) To "Test_profile_1" add variable named PATH with value "path1;path2;path3" and click Save. Confirm that PATH variable in profile is shown as list (list of 3 values and not as path1;path2;path3).
|
||||
- [ ] **[ADMIN: NO]** (L815) Edit PATH variable from "Test_profile_1". Try different options from ... menu (Delete, Move up, Move down, etc...). Click Save.
|
||||
- [ ] **[ADMIN: NO]** (L816) Apply "Test_profile_1". Open OS Environment variables window and confirm that profile is applied correctly - Path value and backup variable. Also, in "Applied variables" list check that Path variable has correct value: value of profile PATH concatenated to the end of SYSTEM Path.
|
||||
- [ ] **[ADMIN: NO]** (L819) Close the app and reopen it. Confirm that the state of the app is the same as before closing.
|
||||
- [ ] **[ADMIN: NO]** (L821) "Test_profile_1" should still be applied (if not apply it). Delete "Test_profile_1". Confirm that profile is unapplied (both in OS Environment variables window and "Applied variables" list).
|
||||
- [ ] **[ADMIN: NO]** (L822) Delete "Test_profile_2". Check profiles.json file and confirm that both profiles are gone.
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# File Locksmith — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with an admin-requirement tag:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
---
|
||||
|
||||
## File Locksmith (10 items)
|
||||
|
||||
- [ ] **[ADMIN: COND]** (L641) **Right-click the PowerToys installer executable** and select `Unlock with File Locksmith`. Confirm the entry is present and launches the File Locksmith window (titled **File Locksmith**). The window lists the processes using the file — **two entries** (each showing the process name + an **End task** button), because the installer starts two processes.
|
||||
- [ ] **[ADMIN: COND]** (L642) Click **End task** on each listed installer process in the File Locksmith UI. Confirm each **End task** terminates that process (the installer window closes) and its **row is removed** from the list.
|
||||
- [ ] **[ADMIN: COND]** (L643) **Start the installer executable again**, then click the **Reload** button (top-right refresh) in the File Locksmith UI. Confirm the newly-started process(es) using the file are **rediscovered and added** to the list.
|
||||
- [ ] **[ADMIN: COND]** (L644) **Close the installer window** (let its process exit) **without** pressing Reload. Confirm File Locksmith **auto-delists** the exited process(es) within ~1–2 s (no manual refresh), and close the window.
|
||||
- [ ] **[ADMIN: COND]** (L646) **Right-click the directory** containing the executable and select `Unlock with File Locksmith`. Confirm the entry appears and the UI lists the locking process(es) found **recursively** inside that directory tree.
|
||||
- [ ] **[ADMIN: COND]** (L647) **Right-click the drive** where the executable is located and select `Unlock with File Locksmith`. Confirm the entry appears and the UI lists the locking process(es) on that volume. (You can close the PowerToys installer now.)
|
||||
- [ ] **[ADMIN: COND]** (L649) With File Locksmith launched **non-elevated**, **right-click "Program Files"** (the folder holding the elevated PowerToys runner) and select `Unlock with File Locksmith`. Confirm **`PowerToys.exe` does NOT appear** in the list — a non-elevated File Locksmith cannot read the higher-integrity (elevated) runner process.
|
||||
- [ ] **[ADMIN: YES]** (L650) In that same non-elevated File Locksmith window, click **Restart as an administrator** (shield button, shown only when non-elevated) and approve the UAC prompt. Confirm that after relaunching **elevated** (title becomes **Administrator: File Locksmith**), **`PowerToys.exe` now appears** in the list — the elevated File Locksmith can see higher-integrity processes.
|
||||
- [ ] **[ADMIN: YES]** (L651) **Right-click the drive where Windows is installed** and select `Unlock with File Locksmith`. With the (large) process list shown, **scroll all the way down and back up** and confirm File Locksmith **does not crash** while rendering all those entries. **Repeat** after clicking **Restart as an administrator**, and confirm no crash in the elevated view either.
|
||||
- [ ] **[ADMIN: COND]** (L652) **Disable** File Locksmith in PowerToys Settings. Right-click a file and confirm `Unlock with File Locksmith` is **absent** from **both** the Win11 tier-1 (modern) menu **and** the classic "Show more options" (`#32768`) menu, while sibling PowerToys entries (e.g. `Rename with PowerRename`) **remain present** — proving the menu still renders and only File Locksmith was gated out (not a render failure). **Re-enable** and confirm the entry **reappears** in both menus.
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Image Resizer — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with an admin-requirement tag:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
## Fixtures & conventions
|
||||
|
||||
- Prepare test images at a **known DPI** (the pixel outputs below assume **96 DPI**): `landscape 1200×800`, `portrait 800×1200`, `small 100×100`, `square 400×400`, `anim.gif` (animated), and three `batch*` images `1000×1000`.
|
||||
- Default presets are **Small 854×480, Medium 1366×768, Large 1920×1080, Phone 320×568** — all **Fit**, unit **Pixel**.
|
||||
- Fit modes: **Fit** = scale by `min(scaleX,scaleY)` (letterbox, no crop); **Fill** = scale by `max(scaleX,scaleY)` then centered-crop to the box; **Stretch** = apply scaleX and scaleY independently (distorts).
|
||||
- Unit → pixels: **Pixel** = literal value; **Percent** = `value/100 × original` (DPI-independent); **Inch** = `value × DPI`; **Centimeter** = `value × DPI / 2.54`.
|
||||
- Always resize **copies** in a disposable folder unless the item is specifically testing in-place replace.
|
||||
|
||||
---
|
||||
|
||||
## Image Resizer (18 items)
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L309) In PowerToys Settings, **disable** Image Resizer. Right-click an image and confirm `Resize with Image Resizer` is **absent** from **both** the Win11 tier-1 (modern) menu **and** the classic "Show more options" (`#32768`) menu, while sibling PowerToys entries (e.g. `Rename with PowerRename`, `Unlock with File Locksmith`) **remain present** — proving the menu still renders and only the Image Resizer command was gated out (not a render failure).
|
||||
- [ ] **[ADMIN: NO]** (L310) **Enable** Image Resizer. Right-click an image and confirm `Resize with Image Resizer` is **present, with its icon**, in **both** the Win11 tier-1 (modern) menu **and** the classic "Show more options" (`#32768`) menu. Invoking the entry launches `PowerToys.ImageResizer.exe` on the selection (window titled "Image Resizer").
|
||||
- [ ] **[ADMIN: NO]** (L311) In Settings → Image Resizer → *Image sizes*, **remove** a built-in preset (e.g. `Phone`) and **add** a custom size (e.g. `Web` = 1024×768 px, Fit). Open the Image Resizer window from the context menu and open the size selector; confirm the removed preset (`Phone`) is **gone** and the added preset (`Web`) is **listed** — the presets round-trip from settings to the window.
|
||||
- [ ] **[ADMIN: NO]** (L312) Select a **single** `1200×800` image and resize with the **Small** preset (854×480, Fit). Confirm exactly one output `<name> (Small).jpg` is produced at **720×480** (Fit scale = min(854/1200, 480/800) = 0.6 → 720×480) and the source file is untouched.
|
||||
- [ ] **[ADMIN: NO]** (L313) Select **three** `1000×1000` images and resize them in **one** operation with the **Small** preset (854×480, Fit). Confirm all three outputs `<name> (Small).jpg` are produced, each at **480×480** (Fit = min(854/1000, 480/1000) = 0.48 → 480×480).
|
||||
- [ ] **[ADMIN: NO]** (L314) Open Image Resizer on an **animated `.gif`** and confirm the yellow InfoBar warning **"Gif files with animations may not be correctly resized."** appears (shown whenever any selected file has a `.gif` extension).
|
||||
- [ ] **[ADMIN: NO]** (L316) Resize a `1200×800` image to a **custom 400×400** target with the **Fill** mode. Confirm output is exactly **400×400**, produced by scaling with max(400/1200, 400/800) = 0.5 (→600×400) then **centered-cropping** to 400×400 (fills the box, no letterboxing, overflow cropped).
|
||||
- [ ] **[ADMIN: NO]** (L317) Resize a `1200×800` image to a **custom 400×400** target with the **Fit** mode. Confirm output is **400×267** — aspect ratio preserved via min-scale (0.333), letterboxed inside the box, **not** cropped.
|
||||
- [ ] **[ADMIN: NO]** (L318) Resize a `1200×800` image to a **custom 400×400** target with the **Stretch** mode. Confirm output is exactly **400×400** by applying scaleX=0.333 and scaleY=0.5 **independently** (image distorted). Contrast: same target gives 400×267 with Fit and 400×400-via-crop with Fill.
|
||||
- [ ] **[ADMIN: NO]** (L320) Resize using unit **Centimeters**: on a `1200×800` image at **96 DPI**, custom `10cm × 5cm` with **Stretch**. Confirm output = **378×189** (cm→px = value × DPI / 2.54 → 10×96/2.54 ≈ 378, 5×96/2.54 ≈ 189). At another DPI the output scales with DPI (e.g. at 120 DPI → 472×236).
|
||||
- [ ] **[ADMIN: NO]** (L321) Resize using unit **Inches**: on a `1200×800` image at **96 DPI**, custom `4in × 3in` with **Stretch**. Confirm output = **384×288** (inch→px = value × DPI → 4×96, 3×96). At 120 DPI → 480×360.
|
||||
- [ ] **[ADMIN: NO]** (L322) Resize using unit **Percent**: on a `1200×800` image, width = **50%**. Confirm output = **600×400** (percent→px = value/100 × original, DPI-independent; height scales by the same 50%).
|
||||
- [ ] **[ADMIN: NO]** (L323) Resize using unit **Pixels**: on a `1200×800` image, custom `500×300` with **Stretch**. Confirm output = exactly **500×300** (pixel target used literally).
|
||||
- [ ] **[ADMIN: NO]** (L325) Set Filename format to `%1 - %2 - %3 - %4 - %5 - %6` and resize a `1200×800` image with the **Small** preset. Confirm the output filename is `<orig-name> - Small - 854 - 480 - 720 - 480.jpg`, where **%1**=original filename, **%2**=size/preset name, **%3**=selected width, **%4**=selected height, **%5**=actual output width, **%6**=actual output height.
|
||||
- [ ] **[ADMIN: NO]** (L326) Check **Use original date modified**. Set a source image's last-modified time to a known past date (e.g. `2020-01-15 08:30`), resize it, and confirm the **output's** modified date **equals the source's** (`2020-01-15 08:30`). Control: with the option **unchecked**, the output's modified date is the **current** time instead.
|
||||
- [ ] **[ADMIN: NO]** (L327) Check **Make pictures smaller but not larger**. Resize a **100×100** image with a target larger than it (e.g. Small 854×480) and confirm the output stays **100×100** (never enlarged). Control: a larger `1200×800` image with the same target is **still shrunk** (→720×480).
|
||||
- [ ] **[ADMIN: NO]** (L328) Check **Resize the original pictures (don't create copies)**. Resize a `1200×800` image with the **Small** preset and confirm the original file is **overwritten in place** at **720×480** and **no** separate `(Small)` copy is created — the folder still holds exactly one file with the original name.
|
||||
- [ ] **[ADMIN: NO]** (L329) **Uncheck** "Ignore the orientation of pictures" and resize a **landscape** `1200×800` image with a **portrait** target `100×200` (Fit, Pixel): confirm the target is applied **as-is** → **100×67** (no swap). Then **check** the option and repeat: the target is **swapped to 200×100** to match the source orientation → **150×100**.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Release checklist — per-module index
|
||||
|
||||
One file per module; a verification run loads only its module's file.
|
||||
|
||||
> **Scope:** only modules that have been verified end-to-end (with a sign-off report) are checked in here so far. The remaining modules' checklists will be added as each is verified.
|
||||
|
||||
| Module | Items | File |
|
||||
|---|---:|---|
|
||||
| Advanced Paste | 41 | `advanced-paste.md` |
|
||||
| Environment Variables | 20 | `environment-variables.md` |
|
||||
| File Locksmith | 10 | `file-locksmith.md` |
|
||||
| Image Resizer | 18 | `image-resizer.md` |
|
||||
| New+ | 9 | `new-plus.md` |
|
||||
| Peek | 18 | `peek.md` |
|
||||
| PowerRename | 17 | `power-rename.md` |
|
||||
@@ -1,33 +0,0 @@
|
||||
# New+ — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06); items rewritten to be
|
||||
> definitive against the New+ sign-off runs (`verify-NewPlus-*`) and `../modules/new-plus.md`.
|
||||
> One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with an admin-requirement tag:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
> **Note:** New+ appears on the folder-**background** ("New") menu and **only** on the Win11 tier-1
|
||||
> (modern) context menu — **not** the classic "Show more options" (`#32768`) menu, so do not assert
|
||||
> both tiers as you would for PowerRename/Image Resizer. See `../modules/new-plus.md` for driving
|
||||
> details (settings paths, default-template source, and the synthetic-menu flow).
|
||||
|
||||
---
|
||||
|
||||
## New+ (9 items)
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L969) **Enable** New+ in PowerToys Settings. Right-click empty space inside a folder to open the Win11 tier-1 background ("New") menu and confirm the **`New+`** submenu is **present**; expanding it shows at least **`Open templates`**. (New+ is Win11 tier-1 background-menu only; may need an Explorer restart the first time. Corroborating signals: CLSID `{FF90D477-…}` registered under `HKCU:\Software\Classes\CLSID`, and the shell-ext log line `New+ context menu registered`.)
|
||||
- [ ] **[ADMIN: NO]** (L971) **Disable** New+ via the Settings toggle. Right-click empty space in a folder and confirm **`New+`** is **absent** from the background menu, while the rest of the "New" menu still renders (proving only New+ was gated, not a render failure). Corroborating: the CLSID `{FF90D477-…}` is **unregistered** (`HKCU` key gone) and the log shows `New+ context menu unregistered` (the sparse package itself stays `Status Ok` — hidden dynamically). **Re-enable** and confirm `New+` reappears.
|
||||
- [ ] **[ADMIN: NO]** (L973) **Delete the entire Templates folder** (`%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\Templates`), then open the New+ submenu (building the menu triggers the shell-ext to recreate the folder). Confirm the Templates folder is **recreated and empty** (0 items) and the submenu offers **only `Open templates`** (no template items). (This is the shell-ext `create_folder_if_not_exist` path — distinct from L977's default-templates copy, which only happens via the Settings-UI enable transition.)
|
||||
- [ ] **[ADMIN: NO]** (L974) Copy a **file** template (e.g. `MyTemplate.txt`) into the Templates folder. Open the New+ submenu and confirm a matching item **appears** (caption `MyTemplate` with the extension hidden if *Hide extension* is on, else `MyTemplate.txt`). **Select** the item and confirm the file **`MyTemplate.txt` is created** in the current folder (copied via `SHFileOperation FO_COPY`) and enters inline rename mode.
|
||||
- [ ] **[ADMIN: NO]** (L975) Copy a **folder-with-files** template into the Templates folder (e.g. `MyFolder\` containing `fileA.txt` and `sub\fileB.txt`). Open the New+ submenu and confirm **`MyFolder`** appears. **Select** it and confirm the folder is created **whole and recursively** in the current folder — i.e. `MyFolder\fileA.txt` **and** `MyFolder\sub\fileB.txt` all exist.
|
||||
- [ ] **[ADMIN: NO]** (L976) **Empty** the Templates folder (delete all contents but keep the folder itself). Open the New+ submenu and confirm **no template items** are listed — only the always-present **`Open templates`** command remains.
|
||||
- [ ] **[ADMIN: NO]** (L977) With the Templates folder still **empty** (from L976), **disable** and then **re-enable** New+ via the Settings toggle. Confirm re-enabling **copies the default example templates** into the Templates folder — `Example folder\` (containing `Example txt file.txt` and `Another example txt file.txt`) and `Any files or folders placed in the template folder are available via New+.txt` — and that these appear in the New+ submenu. (The copy is driven by `CopyTemplateExamples` on the Settings-UI enable transition, and only when the folder was empty; a runner-only restart does **not** copy templates.)
|
||||
- [ ] **[ADMIN: NO]** (L979) Test **Hide template filename extension** (Settings → New+; key `HideFileExtension`). With a `MyTemplate.txt` template: **ON** → the submenu caption is **`MyTemplate`** (extension hidden); **OFF** → the caption is **`MyTemplate.txt`**. In **both** cases the **created file keeps its extension** (`MyTemplate.txt`) — the option affects the menu caption only. Takes effect without an Explorer restart (handler re-reads settings per menu build).
|
||||
- [ ] **[ADMIN: NO]** (L980) Test **Hide template filename starting digits, spaces and dots** (key `HideStartingDigits`). Use a digit-prefixed template `01. Digits.txt` (set *Hide extension* OFF to isolate this option): **ON** → **both** the submenu caption **and** the created filename are **`Digits.txt`** (leading `01. ` stripped from both); **OFF** → both are **`01. Digits.txt`** (preserved). Unlike *Hide extension*, this transform applies to the created filename as well as the caption.
|
||||
@@ -1,59 +0,0 @@
|
||||
# Peek — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06); items rewritten to be
|
||||
> definitive against the Peek sign-off runs (`verify-Peek-*`) and `../modules/peek.md`.
|
||||
> One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with an admin-requirement tag:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
## Fixtures & conventions
|
||||
|
||||
- **Opening Peek**: select a file in Explorer and press **Ctrl+Space** (default); Peek opens a window
|
||||
titled **`<file> - Peek`**.
|
||||
- **Prepare a fixtures folder** with one file per previewer type, each carrying an in-file marker so you
|
||||
can confirm the *content* rendered (not just that a pane appeared): an **image** (PNG), a **source/text**
|
||||
file (e.g. `.cs`), a **markdown** file (H1 + bold + bullet list), a **PDF** with known marker text, an
|
||||
**HTML** file with an `<h1>` marker, a **`.zip`** containing ≥1 file, and an **unsupported** file
|
||||
(e.g. `.xyz` of random bytes). For the pin/position items also prepare **3 differently-sized images**
|
||||
(e.g. 320×240, 800×600, 1920×1080).
|
||||
- See `../modules/peek.md` for driving mechanics (CLI back-door `PowerToys.Peek.UI.exe <file>`,
|
||||
Shell-COM multi-select, chord codes, and the exact UIA selectors `ImagePreview` / `PreviewBrowser` /
|
||||
`PinButton` / `LaunchAppButton`).
|
||||
|
||||
---
|
||||
|
||||
## Peek (18 items)
|
||||
|
||||
> **L697–L703 preview the following file types.** For each, open Peek on the fixture and confirm the window `<file> - Peek` shows the expected previewer rendering the fixture's content.
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L697) **Image** — Peek an image (e.g. `small-image.png`, 200×150). Confirm the **image previewer** renders the bitmap, sized to the source, with the pin + "Open with" buttons visible.
|
||||
- [ ] **[ADMIN: NO]** (L698) **Text or dev file** — Peek a source file (e.g. `Program.cs`). Confirm the **developer/text previewer** renders the code with **syntax highlighting and line numbers** (not raw plain text, not blank).
|
||||
- [ ] **[ADMIN: NO]** (L699) **Markdown file** — Peek a `.md` file containing an H1, bold text, and a bullet list. Confirm the preview renders **formatted markdown** (H1 styled, bold applied, list bulleted) — not the raw `#`/`**` source.
|
||||
- [ ] **[ADMIN: NO]** (L700) **PDF** — Peek a 1-page PDF with known marker text. Confirm the **PDF page renders** with the marker text visible and a PDF toolbar (zoom / page nav), not an error.
|
||||
- [ ] **[ADMIN: NO]** (L701) **HTML** — Peek an `.html` file whose `<h1>` contains a marker. Confirm the HTML is **rendered** (heading shown as a styled heading), not shown as raw markup.
|
||||
- [ ] **[ADMIN: NO]** (L702) **Archive files (.zip, .tar, .rar)** — Peek a `.zip` containing ≥1 file. Confirm the **archive previewer** lists the entries in a tree and shows directory/file counts + sizes (e.g. `0 directories | 1 files | … bytes`). The same previewer handles `.tar`/`.rar`.
|
||||
- [ ] **[ADMIN: NO]** (L703) **Unsupported file** — Peek a file of an unhandled type (e.g. `unsupported.xyz` / `.exe`). Confirm the **unsupported-file view** is shown with **File Type / Size / Date Modified** metadata (and a generic icon) instead of a previewer, and Peek does not crash.
|
||||
|
||||
> **L706–L709 pin / position behavior.** Use a selection of the 3 differently-sized images; move the window with a known position/size and read the window rect to assert.
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L706) **Pin, switch between different-sized images** — Open Peek on the 3-image selection, **pin** the window (pin toggle), move it to a known position/size, then switch between the images. Confirm the window stays at the **same position and size** across every switch (does not re-center/resize to each image).
|
||||
- [ ] **[ADMIN: NO]** (L707) **Pin, close and reopen** — With the window pinned at a known place/size, close Peek (`Esc`) and reopen it on the same selection. Confirm the new window opens at the **same pinned place and size**.
|
||||
- [ ] **[ADMIN: NO]** (L708) **Unpin, switch file** — From the pinned window, **unpin**, then switch to a different file. Confirm the window **moves to the default placement** (leaves the pinned coordinates and re-centers/sizes to the new file).
|
||||
- [ ] **[ADMIN: NO]** (L709) **Unpin, close and reopen** — With the window unpinned, close (`Esc`) and reopen Peek. Confirm the new window opens at the **default place/size**, not the previously-pinned coordinates.
|
||||
|
||||
> **L712–L713 open the file in its default app** (the "Open with <default app>" action).
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L712) **By clicking a button** — With a file peeked, click the **"Open with <default app>"** button. Confirm the file opens in its default application (a new app process/window for that file appears; match by the file's window title).
|
||||
- [ ] **[ADMIN: NO]** (L713) **By pressing Enter** — With the Peek window focused, press **Enter**. Confirm the file opens in its default application (same result as L712).
|
||||
- [ ] **[ADMIN: NO]** (L716) **Peek via command** — Run `PowerToys.Peek.UI.exe <file>`. Confirm it opens a `<file> - Peek` window previewing that file (the CLI accepts a file-path argument and shows the previewer).
|
||||
- [ ] **[ADMIN: NO]** (L717) **Peek works while a session is already on** — Open several Peek sessions on different files, leaving prior windows open. Confirm each launch opens its own window and previews correctly with the others still open (no crash/hang, no fatal errors in `Peek\Logs`).
|
||||
- [ ] **[ADMIN: NO]** (L719) **Folder navigation with Left/Right arrows** — With a **single** file selected in a folder, open Peek and press `RightArrow` / `LeftArrow`. Confirm you can cycle through **all files in the folder** (in order, wrapping at the ends).
|
||||
- [ ] **[ADMIN: NO]** (L720) **Selection-scoped navigation** — Select **multiple** files (a subset of a folder), open Peek and press `RightArrow` / `LeftArrow`. Confirm navigation cycles **only among the selected files** and never escapes into the folder's other files.
|
||||
- [ ] **[ADMIN: NO]** (L721) **Change the activation shortcut** — In Settings → Peek, change the activation shortcut (e.g. `Ctrl+Space` → `Ctrl+Shift+Space`). After the change takes effect (the runner re-registers the hook — it is **not** hot-reloaded), confirm the **new** chord opens Peek and the **old** chord no longer does. Restore the original shortcut afterward.
|
||||
@@ -1,44 +0,0 @@
|
||||
# PowerRename — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with an admin-requirement tag:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
---
|
||||
|
||||
## PowerRename (17 items)
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L393) Toggle the PowerRename enable switch **off**, then **on**, in PowerToys Settings. Confirm the `Rename with PowerRename` context-menu entry **disappears** when the module is disabled and **reappears** when it is enabled. (On Win11) With the module enabled, confirm `Rename with PowerRename` is present in **both** the Win11 tier-1 (modern) context menu **and** the classic "Show more options" (`#32768`) context menu.
|
||||
- [ ] **[ADMIN: NO]** (L394) **Shell integration — verify the `Show PowerRename in` dropdown together with the `Hide icon in context menu` checkbox across all 4 combinations.** Both live under Settings → PowerRename → *Shell integration*; **expand the "Show PowerRename in" row** (the ˅ at the far right, not the dropdown arrow) to reveal the "Hide icon in context menu" checkbox. The icon setting applies to **both** the modern (Win11 tier-1) and legacy/extended menus. Verify all four:
|
||||
- **`Default and extended context menu` + `Hide icon` unchecked** → `Rename with PowerRename` appears in the **default (modern) right-click menu, with** its icon.
|
||||
- **`Default and extended context menu` + `Hide icon` checked** → appears in the default menu, **without** an icon.
|
||||
- **`Extended context menu only` + `Hide icon` unchecked** → **absent from the default menu**; appears **only under Shift + right-click, with** its icon.
|
||||
- **`Extended context menu only` + `Hide icon` checked** → absent from the default menu; appears **only under Shift + right-click, without** an icon.
|
||||
- [ ] **[ADMIN: NO]** (L396) Toggle **autocomplete** (Settings → PowerRename → *Enable auto-complete for the search & replace fields*; settings key `MRUEnabled`, read at PR launch). With it **on** and prior search history present, launch PowerRename, focus the **Search** field and type a prefix (e.g. `p`) → a `SuggestionsPopup`/`SuggestionsList` of matching prior terms **appears**. With it **off** (relaunch), typing the same prefix shows **no** suggestions popup.
|
||||
- [ ] **[ADMIN: NO]** (L397) Toggle **Show recently used strings** (settings key `PersistState`, read at PR launch). With it **on**: run PowerRename once with Search=`persist`, Replace=`KEPT`, and Apply; relaunch on a fresh file → the Search/Replace fields are **pre-populated** with `persist`/`KEPT` (restored from `power-rename-last-run-data.json`) and the preview auto-applies. With it **off**: every launch starts with **empty** Search/Replace fields.
|
||||
- [ ] **[ADMIN: NO]** (L399) With `Hi World.txt` selected and a Replace producing `Hi World`, click each case toggle in turn and confirm the preview **Renamed** column: **Uppercase** → `HI WORLD.TXT`, **Titlecase** → `Hi World.txt`, **Lowercase** → `hi world.txt`. Confirm the buttons are **mutually exclusive** — selecting one **deselects** the previously active case button (only one active at a time).
|
||||
- [ ] **[ADMIN: NO]** (L400) Selection = one file (`aaa_file.txt`) + one folder (`aaa_folder` containing `aaa_inner.txt`); Search `aaa` → Replace `zzz`. Toggle **Include Files**, **Include Folders**, and **Include Subfolder Items** independently and confirm each gates only its row type in the preview:
|
||||
- **All include on** → all three renamed (`zzz_file.txt`, `zzz_folder`, `zzz_inner.txt`).
|
||||
- **Files off** → files' Renamed column empty; only `aaa_folder → zzz_folder`.
|
||||
- **Folders off** → folder unchanged; both files renamed.
|
||||
- **Subfolder Items off** → inner file excluded; top-level file + folder renamed.
|
||||
|
||||
Confirm the toggles **combine** (several can be off simultaneously).
|
||||
- [ ] **[ADMIN: NO]** (L401) File `aaa.aaa`, Search `aaa` → Replace `zzz`, Match-all on. Using the **Apply to** selector (single-select), confirm each scope changes only the targeted part of the name: **Filename + extension** (default) → `aaa.aaa → zzz.zzz`; **Filename only** → `aaa.aaa → zzz.aaa`; **Extension only** → `aaa.aaa → aaa.zzz`. Only **one** option can be selected at a time.
|
||||
- [ ] **[ADMIN: NO]** (L402) Enumerate Items. Test advanced enumeration using different values for every field `${start=10,increment=2,padding=4}`. With Enumerate items on and Replace = `item_${start=10,increment=2,padding=4}` applied to a 5-file selection, the preview **Renamed** column must show `item_0010`, `item_0012`, `item_0014`, `item_0016`, `item_0018` (counter = `start + index*increment` for 0-based index, zero-padded to `padding` digits; verify each field independently: `start=10` → first value 0010 not 0, `increment=2` → step of 2 not 1, `padding=4` → 4-digit zero-pad not unpadded).
|
||||
- [ ] **[ADMIN: NO]** (L403) File `MixedCase.txt`, Search `mixed` → Replace `XXX`. With **Case sensitive OFF** (default) → `mixed` matches `Mixed`, preview `MixedCase.txt → XXXCase.txt`. With **Case sensitive ON** → no match: the Renamed column is **empty** and the **Apply** button is **disabled**.
|
||||
- [ ] **[ADMIN: NO]** (L404) File `Foo_A_A_A.txt`, Search `A` → Replace `B`. With **Match all occurrences unchecked** (default) → only the **first** (left-to-right) match is replaced: `Foo_A_A_A.txt → Foo_B_A_A.txt`. With it **checked** → **all** occurrences are replaced: `Foo_A_A_A.txt → Foo_B_B_B.txt`.
|
||||
- [ ] **[ADMIN: NO]** (L406) Files `IMG_001.png`..`IMG_003.png`. Enable **Use regular expressions** and set Search = `(.*).png`. Confirm the regex matches every filename (capture group populated) — with Replace = `foo_$1.png` the preview shows `IMG_00n.png → foo_IMG_00n.png` for n=1..3.
|
||||
- [ ] **[ADMIN: NO]** (L407) Regex on, Search = `(.*).png`, Replace = `foo_$1.png` on files `IMG_001.png`..`IMG_003.png`. Confirm the capture-group back-reference `$1` is substituted with the matched stem: `IMG_001.png → foo_IMG_001.png`, `…002 → foo_IMG_002.png`, `…003 → foo_IMG_003.png` (with regex off, `$1` would appear literally).
|
||||
- [ ] **[ADMIN: NO]** (L408) File `orig.txt` with a known creation time (e.g. `07/02/2026 11:54:54`). With regex on, Search = `.*\.txt$`, Replace = `$YYYY_$MMMM_$DD__$hh-$mm-$ss.txt`. Confirm each token expands from the file's creation date/time so the preview exactly matches the expected string `2026_July_02__11-54-54.txt` (`$YYYY`→`2026`, `$MMMM`→`July`, `$DD`→`02`, `$hh`→`11`, `$mm`→`54`, `$ss`→`54`; also verify `$fff` milliseconds).
|
||||
- [ ] **[ADMIN: NO]** (L409) Enable **Use Boost library** (settings key `UseBoostLib`, read at PR launch — relaunch after toggling). Files `test.txt` and `nest.txt`; with regex on, Search = `(?<=t)est` (Perl lookbehind, unsupported by the default std::regex engine), Replace = `X`. Confirm the lookbehind evaluates without error and matches **only** where `est` is preceded by `t`: `test.txt → tX.txt`, while `nest.txt` is **unchanged**.
|
||||
- [ ] **[ADMIN: NO]** (L411) Files `file1.txt, file2.txt, file3.txt`; Search `file` → Replace `done`. Uncheck the `file2.txt` row in the preview (row shows `[ ]`), then **Apply**. Confirm on disk that only the checked rows were renamed: `done1.txt, done3.txt, file2.txt` — the unchecked file is **left untouched**.
|
||||
- [ ] **[ADMIN: NO]** (L412) Files `match1.txt, match2.txt, keepme.txt`; Search `match` → Replace `X` (`keepme.txt` does not match). Use the **Filter** (funnel) button above the file list: choosing **"Only show files that will be renamed"** hides the non-matching `keepme.txt` (only `match1.txt`, `match2.txt` visible); choosing **"Show all files"** restores all 3 rows.
|
||||
- [ ] **[ADMIN: NO]** (L413) With a 3-file list all initially checked, use the **Select/deselect all** checkbox (header "Select or deselect all") above the file list: one click **deselects all** (all rows `[ ]`); clicking again **selects all** (all rows `[x]`) — toggling every row in a single action.
|
||||
@@ -1,159 +0,0 @@
|
||||
# Reporting format
|
||||
|
||||
This doc defines the **required** report shape for every per-module verification run. Modeled on `PR-validation\Round1\PR-47211-validation\report.md` style — table-driven, reproducible, no prose narratives.
|
||||
|
||||
## §A — Per-item table (one per checklist item)
|
||||
|
||||
```markdown
|
||||
## Item L<line_num> — <verbatim description from the module's checklist> — **<PASS|FAIL|BLOCKED>** <emoji>
|
||||
|
||||
**Admin**: <NO|COND|YES> | **Clarity**: <CLEAR|VAGUE-*|REWRITTEN> | **Category**: <PASS: verification method (free text) · FAIL: cause = product | checklist-stale | checklist-ambiguous · BLOCKED: a BLK-* reason>
|
||||
|
||||
### Verification steps performed
|
||||
|
||||
| # | Step | winapp / probe commands | Evidence / result |
|
||||
|---|---|---|---|
|
||||
| 1 | <what step 1 does> | `<exact command>`<br>`<another command if multiple>` | <what you observed; reference artifact filename> |
|
||||
| 2 | <what step 2 does> | `<command>` | <evidence>; screenshot: `artifacts/L<line>/step-02-<name>.png` |
|
||||
| 3 | ... | ... | ... |
|
||||
|
||||
### Artifacts produced
|
||||
- `artifacts/L<line>/step-01-<name>.png` — <one-line description>
|
||||
- `artifacts/L<line>/step-02-<name>.txt` — full inspect dump
|
||||
- ...
|
||||
|
||||
### Verdict reasoning
|
||||
- ✅ <assertion 1 that PASSed, with reference to the line of code / settings key / log line that proves it>
|
||||
- ✅ <assertion 2>
|
||||
- ❌ <if BLOCKED, the specific obstacle: "BLK-HARDWARE because MWB needs 2 physical PCs; this session has 1 ([System.Windows.Forms.Screen]::AllScreens.Count = 1)">
|
||||
|
||||
### Caveats (optional)
|
||||
- <Any deviation from the user-documented flow, e.g. "Tested via settings.json write rather than UI checkbox because SelectionItemPattern.Select clobbers other selections in ListView.">
|
||||
```
|
||||
|
||||
## §B — Top-of-report summary (write LAST, after all per-item tables)
|
||||
|
||||
```markdown
|
||||
# <Module> verification report — <YYYY-MM-DD HH:MM>
|
||||
|
||||
## Summary
|
||||
- **PASS**: <n> · **FAIL (product)**: <n> · **FAIL (checklist)**: <n> · **BLOCKED**: <n> · **Total**: <n> · **PASS%**: <n>
|
||||
- **Top blocker categories**: <category>: <count>, <category>: <count>, ...
|
||||
- **Items needing follow-up**: L<line> (<reason>), L<line> (<reason>), ...
|
||||
- **State mutations performed + restored**: <count> settings.json edits restored, <count> registry keys removed, <count> fixture files deleted
|
||||
|
||||
## Pre-flight
|
||||
- IsAdmin: <true|false>
|
||||
- PT runner: PID=<n> Elevated=<true|false>
|
||||
- <Module> settings file: <path> (exists=<true|false>)
|
||||
- Interactive desktop: ForegroundOk=<true|false> ShellComOk=<true|false>
|
||||
|
||||
## Items
|
||||
<all per-item tables here, in line_num order>
|
||||
|
||||
## Cleanup performed
|
||||
- <list of every restore action taken>
|
||||
|
||||
## Retrospective (self-reflection on the run — write LAST)
|
||||
<Per §G. If the whole run was frictionless, write exactly: **Everything was smooth — no friction encountered.**>
|
||||
```
|
||||
|
||||
## §C — Required rules for step tables
|
||||
|
||||
1. **Every `winapp ui ...` command goes in the "winapp / probe commands" cell, verbatim, in backticks**, including `-w <hwnd>` / `-a <appId>` arguments and full selector strings. Reviewers will paste these into their own shell to reproduce.
|
||||
2. **Every screenshot path goes in the "Evidence" cell** of the step that produced it, formatted as `screenshot: artifacts/L<line>/step-NN-<name>.png`. Never embed screenshots as `` in the table body (breaks GitHub markdown rendering inside cells); just give the path.
|
||||
3. **If a step has multiple commands**, separate them in the same cell with `<br>` so they render as one cell with multiple lines.
|
||||
4. **PowerShell scriptlets > 3 lines**: write them to a separate `.ps1` in the artifacts folder and reference as ``script: `artifacts/L<line>/step-NN.ps1` `` in the cell. Keep the table cell to 1-3 lines.
|
||||
5. **`—` (em dash) is allowed for non-CLI steps** like "Read sign-off entry + diff", "Create validation folder", "Cleanup notepad". Don't fabricate a command for steps that were purely cognitive or file-system level.
|
||||
6. **Numbered steps must be contiguous** (1, 2, 3, ...). Don't skip numbers.
|
||||
7. **At least one screenshot per PASS item if the item is a user-visible behavioral test**. Schema-only assertions (settings.json key check) don't need screenshots; behavioral tests (popup shown, dialog appeared, theme switched) do.
|
||||
|
||||
## §D — Reporting style
|
||||
|
||||
- Be specific. "Verified via UIA inspect returned `itm-calculator-XXXX`" beats "verified UIA".
|
||||
- Include exact UIA selectors, log line text, settings.json keys, and screenshot filenames so the user can audit.
|
||||
- For BLOCKED items, the 1-sentence reason should name **what specifically blocks**, e.g.:
|
||||
- "BLK-HARDWARE: requires 2nd monitor; session has 1 (verified via `[System.Windows.Forms.Screen]::AllScreens.Count`)."
|
||||
- "BLK-DRAG-REQUIRED: synthetic mouse drag insufficient for FZ snap-and-drag; needs real cursor motion."
|
||||
- "BLK-ENV: SendInput returned ACCESS_DENIED (5) because Session $agentSession ≠ console Session $consoleSession. See `references/environment-setup.md`."
|
||||
- "BLK-EXTERNAL-APP: requires real OpenAI API key; no key provisioned in test env."
|
||||
|
||||
## §E — Reporting anti-patterns (extra strict)
|
||||
|
||||
- Do NOT collapse multiple probe commands into a single English sentence like "verified via UIA". List every `winapp ui ...` command verbatim in a step row.
|
||||
- Do NOT skip the step table for "trivial" items. Even a 1-step item (e.g. "Get-CmdPalSettings shows EnableDock=true") gets a 1-row table.
|
||||
- Do NOT write screenshot references as `` inside table cells (GitHub renders markdown images poorly in cells). Write them as plain text path: `screenshot: artifacts/L<line>/step-NN-<name>.png`.
|
||||
- Do NOT use "the test passed" as a screenshot caption — describe what's visible (e.g. "Settings page with FZ template grid showing 7 templates").
|
||||
- Do NOT reference screenshots that you didn't actually capture. The final wrap-up `Test-Path` loop (see `references/pre-flight.md` §Final wrap-up step 3) will catch missing files; failing that check means the report is invalid.
|
||||
- Do NOT cite source code line numbers (e.g. `CharacterMappings.cs:273`) without having actually read that line. If you cite source, the path must be real and the line number must contain what you claim.
|
||||
|
||||
## §F — Example item (reference: PR-47211 validation report style)
|
||||
|
||||
```markdown
|
||||
## Item L455 — Activate Quick Accent (left Alt + arrow key) on a character, verify accents popup — **PASS** ✅
|
||||
|
||||
**Admin**: NO | **Clarity**: CLEAR | **Category**: drove full UIA flow + asserted accents popup
|
||||
|
||||
### Verification steps performed
|
||||
|
||||
| # | Step | winapp / probe commands | Evidence / result |
|
||||
|---|---|---|---|
|
||||
| 1 | Locate Settings window | `winapp ui list-windows --json` | `hwnd=263304`, `PowerToys.Settings` PID 31740 |
|
||||
| 2 | Navigate to Quick Accent + expand language flyout | `winapp ui invoke QuickAccentNavItem -w 263304`<br>`winapp ui invoke btn-choosecharacter-1c4d -w 263304` | Page loaded; flyout expanded |
|
||||
| 3 | Enumerate language list + screenshot | `winapp ui inspect btn-choosecharacter-1c4d -w 263304 --depth 5`<br>`winapp ui screenshot -w 263304 -o "artifacts/L455/step-03-language-list.png"` | 38 spoken + 6 special languages, alphabetic. screenshot: `artifacts/L455/step-03-language-list.png` |
|
||||
| 4 | Single-language (French) popup test | `winapp ui invoke itm-french-1cac -w 263304`<br>`winapp ui inspect characters -w <popupHwnd> --depth 3`<br>`winapp ui screenshot -w <popupHwnd> -o "artifacts/L455/step-04-popup-FR-E.png"` | Popup chars for **E** = `é è ê ë €` (5), matches `FR.VK_E` in `CharacterMappings.cs:273`. screenshot: `artifacts/L455/step-04-popup-FR-E.png` |
|
||||
| 5 | Restore baseline | — | settings.json reverted to `selected_lang="ALL"` |
|
||||
|
||||
### Artifacts produced
|
||||
- `artifacts/L455/step-03-language-list.png` — Settings page with expanded language flyout
|
||||
- `artifacts/L455/step-03-language-list.txt` — full UIA inspect dump of the list
|
||||
- `artifacts/L455/step-04-popup-FR-E.png` — Popup with French only: `é è ê ë €`
|
||||
|
||||
### Verdict reasoning
|
||||
- ✅ Popup characters match `CharacterMappings.cs` entries exactly (5/5 for FR.VK_E)
|
||||
- ✅ Popup appeared within 500ms of hold-A; no crash
|
||||
- ✅ Language list ordering is alphabetic by localized name
|
||||
```
|
||||
|
||||
## §G — Retrospective (self-reflection)
|
||||
|
||||
After the run, reflect on the **process** (not the product) so the skill itself gets better over time. **If nothing slowed you down, write exactly one line: `Everything was smooth — no friction encountered.`** Otherwise, list each friction as a row and assign a source + severity.
|
||||
|
||||
```markdown
|
||||
## Retrospective
|
||||
|
||||
| # | Friction (what slowed you / what was wrong) | Source | Severity | Cost | Suggested fix |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | <concrete description — what you expected vs what happened> | <one source tag below> | <HIGH/MED/LOW> | <~min wasted · N attempts> | <the doc line / helper function / tool behavior to change> |
|
||||
```
|
||||
|
||||
**Source** — classify each friction into exactly one bucket so the right owner can fix it:
|
||||
|
||||
| Source tag | Meaning |
|
||||
|---|---|
|
||||
| `SKILL-UNCLEAR` | This skill's `SKILL.md` / `references/pre-flight.md` / module profile guidance was missing, ambiguous, or wrong. |
|
||||
| `WINAPP-TOOL-BUG` | The `winapp` CLI itself misbehaved (crash, wrong output, flag not honored) — a product defect in the tool. |
|
||||
| `WINAPP-DOC-UNCLEAR` | `references/winapp-ui-testing.md` was unclear/incorrect about how to use the tool (the tool worked; the docs misled you). |
|
||||
| `HELPER-FLAW` | A shipped `scripts/*.ps1` had a logic bug, bad default, or wrong assumption. Name the function. |
|
||||
| `PT-PRODUCT` | A PowerToys behavior/quirk made driving hard (distinct from a product **FAIL** — this is friction, not a checklist failure). |
|
||||
| `CHECKLIST` | The checklist item itself was wrong/stale/ambiguous (e.g. describes a renamed or removed control). Note: this usually *also* produces a `FAIL (cause: checklist-*)` verdict on the item; log it here too so the checklist owner sees it as a process-improvement signal. |
|
||||
| `ENVIRONMENT` | RDP/session/desktop/elevation friction not already covered by `references/environment-setup.md`. |
|
||||
|
||||
**Severity** — judge by *impact on future agents*, not just yourself:
|
||||
- **HIGH** — most agents will hit it; blocks progress or wastes >10 min, or you needed a non-obvious workaround.
|
||||
- **MED** — many agents may hit it; cost a few minutes or 2-3 retries; workaround exists once known.
|
||||
- **LOW** — edge case or cosmetic; <1 min; noted for completeness.
|
||||
|
||||
**Cost** — be concrete: approximate minutes wasted **and** number of attempts (e.g. `~8 min · 3 attempts`). This is the raw signal for prioritizing skill fixes.
|
||||
|
||||
**Suggested fix** — point at the specific artifact to change: a doc line/section, a helper function name, or a `winapp` behavior to file. Vague reflections ("docs could be clearer") are not actionable — cite the line.
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
## Retrospective
|
||||
|
||||
| # | Friction | Source | Severity | Cost | Suggested fix |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `winapp ui inspect --depth 7 -w $hwnd` threw "Cannot bind argument" until I moved `-w` after `--depth`. | `WINAPP-TOOL-BUG` | MED | ~6 min · 3 attempts | Already noted in pitfall #8, but the tool should parse flag order — file against winapp. |
|
||||
| 2 | SKILL.md §2.A says "wait 4s debounce" but PowerRename needed a full `Restart-PtRunner`; the module-owned-file note (pitfall #12) wasn't cross-linked from §2.A. | `SKILL-UNCLEAR` | HIGH | ~12 min · 4 attempts | Add an explicit "shell-ext modules → see pitfall #12" pointer inside §2.A. |
|
||||
```
|
||||
@@ -1,80 +0,0 @@
|
||||
# Scenario router
|
||||
|
||||
This skill runs in **two scenarios**. They share ~80% of the machinery (the `winapp ui` drive
|
||||
techniques in `../winapp-ui-testing.md`, the helper scripts, the per-module profiles in `../modules/`,
|
||||
the classification taxonomy, state hygiene, and the report format). They differ on only a couple of
|
||||
axes. **Pick the scenario first, read its doc, then run the shared engine in `SKILL.md`.**
|
||||
|
||||
| Axis | A — Module checklist | B — PR validation |
|
||||
|---|---|---|
|
||||
| **Checklist source** | **Supplied** file (`../release-checklist/<module>.md`) | **Derived** by the agent from each PR's description + diff |
|
||||
| **Scope** | 1 module, exhaustive (e.g. 88 items) | 1 PR (deep) — or a release/hotfix set, one folder per PR |
|
||||
| **Bits under test** | Installed shipped artifact (READ-ONLY) | **Depends on a sub-decision** — see below |
|
||||
| **Discipline** | Mutate only via user-facing UI; restore in `finally{}` | Installed-path: same as A · Build-path: build & sideload unreleased bits, then restore |
|
||||
|
||||
| If the task is… | Scenario | Read |
|
||||
|---|---|---|
|
||||
| "verify all `<Module>` checklist items", "sign off Color Picker", a supplied checklist file | **A** | [`module-checklist.md`](./module-checklist.md) |
|
||||
| "validate PR #N" (open **or** merged), "review this fix before merge", "build it and test the fix", "verify the PRs in this release / hotfix / milestone", "sign off 0.X.Y" | **B** | [`pr-validation.md`](./pr-validation.md) |
|
||||
|
||||
**PR validation is one scenario** whether the PR is open, merged, or part of a release set — the hard
|
||||
part (deriving drivable claims from the PR) is identical. What varies is only *which bits you run*,
|
||||
resolved by the sub-decision below.
|
||||
|
||||
---
|
||||
|
||||
## The one contract that differs — declare `BITS` first
|
||||
|
||||
The only real conflict is **what "the bits under test" are** and therefore what you're allowed to
|
||||
touch. Resolve it explicitly at the start of every run and **echo it in the report header** so a
|
||||
reviewer can trust the evidence chain.
|
||||
|
||||
**Scenario A** — always the installed artifact:
|
||||
```
|
||||
BITS: installed shipped artifact <version> (read-only)
|
||||
```
|
||||
|
||||
**Scenario B** — the sub-decision is **"is the PR's code already in the build under test?"**
|
||||
```
|
||||
BITS: installed shipped artifact <version> (read-only) # yes — merged AND shipped in the build
|
||||
BITS: local build of <Module> @ <sha/branch>, sideloaded # no — unmerged, or merged-but-unreleased
|
||||
```
|
||||
|
||||
> **"Merged" is not the deciding word — "in the build under test" is.** A PR merged into `main` but
|
||||
> not yet in the installed build is still the **build + sideload** case (e.g. #45242).
|
||||
|
||||
- **Installed / read-only (A, and B when the code is already shipped)** — the artifact is immutable.
|
||||
Forbidden: copying source-built files into the install or `%LOCALAPPDATA%\...\PowerToys\...`,
|
||||
pre-seeding caches the app/installer owns, editing module `settings.json` to bypass a Settings-UI
|
||||
step, registering/unregistering COM/MSIX, killing helper processes except as a documented user
|
||||
action. Allowed: anything a real user does through the shipped UI (toggles, hotkeys, set-value into
|
||||
Settings fields), read-only probes, screenshots — always capture pre-state and restore in
|
||||
`finally{}`. If the documented user flow does not produce the claimed outcome, that is **FAIL** —
|
||||
do not "rescue" it by editing install state.
|
||||
- **Build + sideload (B when the code isn't in the build)** — testing unreleased code is the point,
|
||||
so the immutability rule does **not** apply to *your* build. It still applies to *unrelated*
|
||||
installed bits you didn't build. Restore the machine to the shipped build when done (see
|
||||
`pr-validation.md` Step 4b).
|
||||
|
||||
> A run that mutates the wrong `BITS` (sideloads when the code is already installed, or drives the
|
||||
> stale installed binary when validating unreleased code) is **invalid regardless of the verdict.**
|
||||
> Set `BITS` before the first drive command.
|
||||
|
||||
---
|
||||
|
||||
## Verdict vocabulary (one taxonomy, two label sets)
|
||||
|
||||
The shared engine in `SKILL.md` Step 3 uses **PASS / FAIL(product|checklist) / BLOCKED(reason)**.
|
||||
Older PR-validation reports sometimes use **PASS / FAIL / Don't-know-what-to-test / Incapable**. They
|
||||
map 1:1 — use the `SKILL.md` set and treat the legacy labels as aliases:
|
||||
|
||||
| Engine (`SKILL.md` §3) | Legacy alias | Meaning |
|
||||
|---|---|---|
|
||||
| PASS | PASS | Drove the behavior; matches the claim. |
|
||||
| FAIL (cause=product) | FAIL | Shipped/built behavior contradicts the claim → file a bug. |
|
||||
| FAIL (cause=checklist) | Don't know what to test | The item/spec is too vague or stale to judge → fix the checklist, quote the ambiguity. |
|
||||
| BLOCKED (`BLK-*`) | Incapable of Testing | Couldn't run the check after ≥2 entry-paths → name the concrete obstacle. |
|
||||
|
||||
Whichever label set the caller asks for, keep the **evidence rules identical**: no "source verified /
|
||||
live deferred / pre-existing / probably / unlikely" weasel-words to justify a PASS — those flip the
|
||||
verdict to FAIL or BLOCKED-with-a-named-environmental-reason.
|
||||
@@ -1,42 +0,0 @@
|
||||
# Scenario A — Single-module release checklist
|
||||
|
||||
**Use when:** the task supplies (or points at) a module's checklist and asks you to verify every
|
||||
item — e.g. "verify all 18 Color Picker items", "sign off Command Palette's 88 items".
|
||||
|
||||
This is the skill's original scenario; `SKILL.md` Steps 1–7 are written for it, so this doc is
|
||||
short — it only nails down the inputs and the report shape.
|
||||
|
||||
## Bits under test
|
||||
|
||||
```
|
||||
BITS: installed shipped artifact <version> (read-only)
|
||||
```
|
||||
|
||||
Installed artifact is immutable; mutate only through the shipped UI and restore in `finally{}`
|
||||
(see `index.md` → "bits contract" and `../pre-flight.md` §Hard rules).
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Source |
|
||||
|---|---|
|
||||
| Checklist (the set of items) | `../release-checklist/<module>.md` — **this file IS the items to verify.** Each item carries `[ADMIN: …]` + `[CLARITY: …]` metadata. See `../release-checklist/index.md` for the full module list. |
|
||||
| Per-module recipes | `../modules/<module>.md` if it exists (entry-paths, item recipes, BLOCKED traps, fixtures). **Check this first.** If absent, fall back to the `SKILL.md` §2 drive-stack and author one afterwards (template in `../modules/README.md`). |
|
||||
|
||||
## Run order
|
||||
|
||||
1. `../pre-flight.md` — pre-flight + bootstrap (`SKILL.md` Step 1).
|
||||
2. For each checklist item: pick a bucket from the verb (`SKILL.md` §2.A/§2.B/§2.C), drive it, then
|
||||
classify (`SKILL.md` §3). One verdict + evidence per item.
|
||||
3. `../reporting-format.md` — per-item table + top-of-report summary + §G retrospective.
|
||||
4. `../pre-flight.md` §State hygiene + §Final wrap-up.
|
||||
5. `SKILL.md` Step 7 — archive the workspace to the sign-off folder.
|
||||
|
||||
## Scope rule
|
||||
|
||||
**One module per run.** Never chain multiple modules into one report. (For "verify a whole
|
||||
release", that's **Scenario B — PR validation**, which fans out across the release's PRs with per-PR folders.)
|
||||
|
||||
## Report
|
||||
|
||||
Use `../reporting-format.md` verbatim. Header must include the `BITS:` line and the module's total
|
||||
item count `<N>`.
|
||||
@@ -1,210 +0,0 @@
|
||||
# Scenario B — PR validation
|
||||
|
||||
**Use when:** you're asked to validate **one or more PRs** by deriving each PR's checklist from its
|
||||
own description + diff, then driving it. This one scenario covers all the PR shapes:
|
||||
|
||||
- a single **open / unmerged** PR ("validate PR #N", "review this fix before merge"),
|
||||
- a single **merged** PR ("check that #N actually works"),
|
||||
- a whole **release / hotfix set** of merged PRs ("sign off 0.100.1", "verify the 14 PRs in this draft release").
|
||||
|
||||
The *hard, interesting* part — turning a PR into concrete, drivable claims — is identical for all of
|
||||
them. They differ on only one thing: **what bits you run.**
|
||||
|
||||
---
|
||||
|
||||
## The bits sub-decision (the only real fork)
|
||||
|
||||
Ask one question up front and **echo the answer in the report header**:
|
||||
|
||||
> **Is the PR's code already in the build under test?**
|
||||
|
||||
| Answer | Bits under test | `BITS:` line |
|
||||
|---|---|---|
|
||||
| **Yes** — merged **and** present in the installed / shipped build you're testing | Drive the **installed** bits, **read-only** | `BITS: installed shipped artifact <version> (read-only)` |
|
||||
| **No** — unmerged/open, **or** merged-but-not-yet-released (not in any shipped build) | **Build** the affected module + **sideload** it | `BITS: local build of <Module> @ <sha/branch>, sideloaded` |
|
||||
|
||||
> **"Merged" is not the deciding word — "in the build under test" is.** A PR that merged into `main`
|
||||
> but hasn't shipped in the installed build is still the **build + sideload** case (e.g. validating
|
||||
> a fix ahead of the next release — PR #45242 was exactly this). Only drive the installed bits when
|
||||
> the code you're validating is genuinely in them.
|
||||
|
||||
- **Installed path — the artifact is immutable.** Forbidden: copying source-built files into the
|
||||
install or `%LOCALAPPDATA%\...\PowerToys\...`, editing module `settings.json` to bypass a
|
||||
Settings-UI step, registering/unregistering COM/MSIX, killing helpers except as a documented user
|
||||
action. Allowed: anything a real user does through the shipped UI, read-only probes, screenshots —
|
||||
capture pre-state and restore in `finally{}`. If the documented user flow doesn't produce the
|
||||
claimed outcome, that's **FAIL** — never "rescue" it by editing install state.
|
||||
- **Build + sideload path — building unreleased code is the whole point,** so the immutability rule
|
||||
does **not** apply to *your* build. It still applies to *unrelated* installed bits you didn't
|
||||
build. **Restore the machine to the shipped build when done** (Step 4b).
|
||||
|
||||
> A run that mutates the wrong `BITS` (sideloads when the code is already installed, or drives the
|
||||
> stale installed binary when validating unreleased code) is **invalid regardless of the verdict.**
|
||||
> Set `BITS` before the first drive command. Full forbidden/allowed list: `index.md` → bits contract
|
||||
> and `../pre-flight.md` §Hard rules.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Acquire the PR set (discovery model)
|
||||
|
||||
For a **single PR**, `N = 1` — skip straight to Step 1. For a **release / hotfix**, a full release
|
||||
can carry ~100 PRs and a hotfix ~10–25; blindly looping 100 is the wrong default. Resolve the set
|
||||
with the **smallest, most explicit source available**, then apply the size gate.
|
||||
|
||||
### Input modes (prefer the most explicit one the caller gave you)
|
||||
|
||||
| Mode | Source | How to enumerate |
|
||||
|---|---|---|
|
||||
| **1. Explicit set** *(preferred)* | PR numbers, a sign-off doc, or a draft/published release whose notes list the PRs | Parse the PR numbers directly. For a release: `gh api repos/microsoft/PowerToys/releases/<id>` → extract `#NNNNN` from `.body`. |
|
||||
| **2. Discoverable source** | A milestone, a release tag, or a commit/tag range | `gh pr list --repo microsoft/PowerToys --search "milestone:0.X" --state merged --limit 200 --json number,title,labels,files` · or for a range: `git log <tagA>..<tagB> --oneline` then extract `(#NNNNN)` merge refs. |
|
||||
| **3. Caller defers entirely** | "verify the latest release" with no list | Resolve to a concrete release/milestone first (mode 1/2). If you cannot, **ask** rather than guess. |
|
||||
|
||||
### Size gate (the guardrail)
|
||||
|
||||
Let `N` = enumerated candidate count, `MAX_AUTO = 25` (hotfix-sized; tune per request).
|
||||
|
||||
- **`N ≤ MAX_AUTO`** → proceed: derive + verify each PR (this is the single-PR and hotfix case).
|
||||
- **`N > MAX_AUTO`** → **STOP. Do not blind-loop.** Present the candidate list grouped by
|
||||
module/area with counts, and require the caller to **scope** before deep-verifying: by module, by
|
||||
label (e.g. `Needs-Verification`, priority), or an explicit subset. Confirm, then verify the scoped
|
||||
set. **Depth beats throughput** — deeply verifying 20 PRs and leaving a clear queue of the rest
|
||||
beats 100 shallow "source-verified" reports.
|
||||
|
||||
### Pre-filter non-runtime PRs
|
||||
|
||||
Before counting against the gate, set aside PRs with **no user-observable runtime surface** — list
|
||||
them as `exempt` (not BLOCKED, not PASS) with the reason, don't spend drive cycles on them:
|
||||
docs-only, CI/pipeline/build-only, dependency bumps, localization/translation-only,
|
||||
release-signing/installer-plumbing with nothing to click. Detect via labels and changed-file paths
|
||||
(`gh pr view N --json files,labels`). Everything else (anything a user opens/toggles/presses/types/
|
||||
previews) goes into the deep-verify set and is subject to the live-drive floor below.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Per PR: derive the checklist
|
||||
|
||||
For each PR in the (possibly scoped) set:
|
||||
|
||||
```powershell
|
||||
gh pr view <N> --repo microsoft/PowerToys --json title,body,files,labels,state,mergedAt
|
||||
gh pr diff <N> --repo microsoft/PowerToys # read the diff — it tells you what actually changed
|
||||
```
|
||||
|
||||
Turn the description + diff into **1–3 concrete checklist items** (the observable claim(s) the PR
|
||||
makes), then drive each with the `SKILL.md` §2 bucket selector and classify with `SKILL.md` §3. From
|
||||
`--json files`, also identify the **affected project(s)** (`.csproj` / `.vcxproj`) — you'll need
|
||||
them for the build path (Step 2b). If, after the description AND the diff, you still cannot tell what
|
||||
to verify → **FAIL (cause=checklist)** ("Don't know what to test"), quoting the ambiguity — do not
|
||||
guess.
|
||||
|
||||
You may `grep`/`view` a **read-only** local clone/worktree for source context (XAML AutomationIds,
|
||||
the `.cs`/`.cpp` the PR touched). On the **installed path** you must not run that code against the
|
||||
install; on the **build + sideload path**, building it is exactly the point (Step 2b).
|
||||
|
||||
### Live-drive floor (anti-shallow-verification)
|
||||
|
||||
If the PR has a verb a real user performs (open/toggle/press/drag/right-click/type/preview/paste/
|
||||
invoke/install/pin/search/record/scroll), the steps table MUST contain **≥4 `winapp ui …` rows** and
|
||||
**≥1 `winapp ui screenshot` of the post-state**. A PR with a user-visible surface and zero
|
||||
`winapp ui …` rows is **not validated**. "Source verified; live deferred" is a weasel-word that
|
||||
downgrades the verdict (see `index.md` → verdict vocabulary).
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Get the bits under test
|
||||
|
||||
Follow **2a or 2b** per the bits sub-decision above.
|
||||
|
||||
### 2a — Installed path (code already in the build under test)
|
||||
|
||||
Nothing to deploy: the shipped runner/modules are already installed. Confirm the module is present
|
||||
(`../pre-flight.md`), then drive the **installed** bits read-only. This is the path for a merged PR
|
||||
that has shipped, and for a release/hotfix sign-off where you're validating the released artifact.
|
||||
|
||||
### 2b — Build + sideload path (code not in the build under test)
|
||||
|
||||
Building & deploying unreleased code is the point. Build **only the affected project** (identified in
|
||||
Step 1) — do **not** build the whole solution just to populate every module.
|
||||
|
||||
```powershell
|
||||
cd <PT_REPO>
|
||||
gh pr checkout <N> # PR branch locally (open PRs); for a merged-unreleased PR, check out its merge commit / the branch
|
||||
tools\build\New-WorktreeFromBranch.ps1 -Branch <pr-branch> # isolated worktree
|
||||
git submodule update --init --recursive # once
|
||||
tools\build\build-essentials.cmd # first build / NuGet restore (runner + settings only)
|
||||
tools\build\build.ps1 -Platform x64 -Configuration Release # run from the changed .csproj/.vcxproj dir
|
||||
```
|
||||
|
||||
**Exit code 0 = success (absolute).** On non-zero, read `build.<config>.<platform>.errors.log` next
|
||||
to the project. If the toolchain is missing (VS 2022 17.4+/2026, Windows SDK) and the build can't
|
||||
complete → **BLOCKED (`BLK-ENV`)** — never PASS on an unbuilt PR.
|
||||
|
||||
> **Partial build ⇒ missing-module dialogs are EXPECTED.** A module-specific build produces only
|
||||
> *your* module, so the build-output runner pops a modal **"Failed to load PowerToys.<Module>ModuleInterface.dll"**
|
||||
> (`#32770`) for each un-built module. This is normal for a targeted build, **not** a build failure.
|
||||
|
||||
**Deploy — run the freshly built bits, don't overlay onto the install:**
|
||||
|
||||
- **Unpackaged module** (FancyZones, PT Run, ColorPicker, Peek, KBM, Advanced Paste, …): stop the
|
||||
installed runner, start the **build-output** runner, and dismiss the expected dialogs.
|
||||
```powershell
|
||||
Get-Process PowerToys -EA SilentlyContinue | ForEach-Object { Stop-Process -Id $_.Id -Force }
|
||||
Start-Process "<PT_REPO>\x64\Release\PowerToys.exe"
|
||||
(Get-Process PowerToys | Select-Object -First 1).Path # must point under the build output
|
||||
```
|
||||
The "Failed to load …" boxes are native `#32770` message boxes — `winapp ui invoke OK` is
|
||||
unreliable; `PostMessage`/`SendMessage` `WM_CLOSE (0x0010)` to each, or send Enter to the focused
|
||||
dialog, until none remain. (Only if dialogs keep racing your module → full solution build as a
|
||||
fallback.) **Never** overlay a partial build onto the installed layout — mixing `0.0.1` files with
|
||||
the shipped `0.100.x` install (esp. a WinUI module's `.dll` without its `.pri`) silently corrupts
|
||||
the test.
|
||||
- **Packaged module** (Command Palette / CmdPal — MSIX): enable Developer Mode, remove the shipped
|
||||
package, `Add-AppxPackage -Register "<build-output>\AppxManifest.xml"`, and restore on cleanup.
|
||||
|
||||
**Prove it's your bits** before driving: the running module's path is under the build output, and/or
|
||||
a dev version string (e.g. Settings shows `v0.0.1`, not `0.100.x`). A run that accidentally drove the
|
||||
installed binary is invalid.
|
||||
|
||||
Because you control the build, you can make a PASS decisive rather than "the fix is present" — e.g.
|
||||
exercise the **failing** state the PR fixes (revert-and-rebuild, or compare against the installed
|
||||
shipped build).
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Drive + classify
|
||||
|
||||
Same engine as everything else: per item pick the `SKILL.md` §2 bucket, drive, classify with
|
||||
`SKILL.md` §3. The live-drive floor (Step 1) applies.
|
||||
|
||||
## Step 4 — Artifacts, report, restore
|
||||
|
||||
**Artifacts + report.** One folder per PR: `{Module}-PR{Number}/` (e.g. `CmdPal-PR48689/`,
|
||||
`AdvancedPaste-PR45242/`). All screenshots and the PR's `report.md` go there; the verdict lives
|
||||
**inside** `report.md`, not in the folder name. Use the `../reporting-format.md` per-item table; the
|
||||
`winapp invoke` column is a hard contract — a literal `winapp ui …` command or `—` (never a
|
||||
`Select-String`/`gh`/`Test-Path` there). The header carries the `BITS:` line (incl. the sha/branch +
|
||||
proof-of-your-bits path on the sideload path) and, on 2b, a build summary (exit code, project built).
|
||||
|
||||
**Roll-up** (multi-PR sets): top-level summary table — PR · Module · verdict · one-line evidence —
|
||||
plus the `exempt` list and (if `N > MAX_AUTO`) the un-scoped **queue** of PRs not yet verified.
|
||||
Include a §G retrospective (run friction).
|
||||
|
||||
**Restore (Step 4b / sideload path only).** Stop your built runner and bring the shipped build back:
|
||||
```powershell
|
||||
Get-Process PowerToys -EA SilentlyContinue | ForEach-Object { Stop-Process -Id $_.Id -Force }
|
||||
Start-Process "$env:LOCALAPPDATA\PowerToys\PowerToys.exe" # or the Program Files install
|
||||
# Packaged: re-add the shipped package (Add-AppxPackage -Register the shipped AppxManifest.xml)
|
||||
```
|
||||
Restore all mutated state, confirm the runner is healthy, and disclose any residue in the report.
|
||||
|
||||
---
|
||||
|
||||
## Worked references
|
||||
|
||||
- **Single unmerged/unreleased PR, build + sideload:** PR #45242 (Advanced Paste "Show AI paste
|
||||
section") — derived 3 claims from the diff, built only the AdvancedPaste project, sideloaded the
|
||||
dev `v0.0.1` runner (dismissed the partial-build dialogs), drove Settings + the AP window + a GPO
|
||||
gate, then restored the shipped build. 3/3 PASS.
|
||||
- **Release/hotfix set, installed bits:** the 0.100.1 14-PR sign-off (8 PASS / 6 BLOCKED) — derived
|
||||
each PR's checklist from its release-notes line + diff, drove the installed bits, and blocked the
|
||||
hardware/visual-only items (DDC/CI, dual-GPU, audio, capture-excluded overlays) with named reasons.
|
||||
@@ -1,541 +0,0 @@
|
||||
# WinUI UI-testing mechanics (winapp ui)
|
||||
|
||||
> **Provenance:** Adapted from the `winui-ui-testing` skill in [microsoft/win-dev-skills](https://github.com/microsoft/win-dev-skills) (MIT, © Microsoft Corporation and Contributors), with PowerToys-specific edits. This is a **reference doc** for the `powertoys-verification` skill — it is intentionally not a standalone skill (no frontmatter), so it is not separately discovered.
|
||||
|
||||
Automated UI testing for WinUI 3 apps — generate a batch test script, run all tests in one pass, read results. Covers element assertions, interactions, value checking (TextBox, ComboBox, ToggleSwitch), file pickers, flyouts, dialogs, persistence, and accessibility audits.
|
||||
|
||||
### Approach
|
||||
|
||||
The goal of this skill is to validate UI and app functionality automatically, without manual interaction, by exercising the app's UI elements, verifying their state, and asserting that the app behaves as expected under test conditions.
|
||||
|
||||
There are two main approaches:
|
||||
1. Interactive exploration — manually run the app, use `winapp ui <command>` to explore the UI tree, find AutomationIds, verify element properties, and test functionality interactively. This is useful for discovery, but slow and expensive if repeated for every test iteration.
|
||||
2. Scripted batch testing — generate a `ui-tests.ps1` script that exercises all UI elements and asserts expected behavior in one pass. This allows you to run the tests automatically, capture results, and iterate quickly without manually interacting with the app each time.
|
||||
|
||||
Unless the user asked for interactive exploration, or you are unfamiliar with the code/app or need to explore the UI tree to discover AutomationIds for hidden or dynamically generated elements (flyouts, dialogs, lazy-loaded content), **prefer scripted batch testing** — it is faster, repeatable, and produces a record of pass/fail results that can be reviewed and acted on.
|
||||
|
||||
### `winapp ui` Verbs
|
||||
|
||||
`status`, `inspect`, `search`, `get-property`, `get-value`, `screenshot`, `invoke`, `click`, `set-value`, `focus`, `scroll`, `scroll-into-view`, `wait-for`, `list-windows`, `get-focused`. Run `winapp ui --cli-schema` for the complete command structure as JSON, or `winapp ui <verb> --help` for any single verb.
|
||||
|
||||
### Step 1: Use the Running App
|
||||
|
||||
If the app is already running, use its PID. **Do NOT relaunch** — use the PID already captured from the build step. If the app is not running, build and launch it using the guidance in the winui-dev-workflow skill.
|
||||
|
||||
### Step 2: Write the Test Script
|
||||
|
||||
**If you wrote the code:** Skip inspect — you already know all the AutomationIds and control structure from the XAML and code-behind. Write tests directly from that knowledge. Inspect misses popups, flyouts, dialogs, and lazy-loaded content anyway.
|
||||
|
||||
**If you're verifying code you didn't write:** Run inspect first to discover the UI:
|
||||
```powershell
|
||||
winapp ui inspect -a <PID> --interactive
|
||||
```
|
||||
Then read the XAML files to find AutomationIds that aren't currently visible (flyout items, dialog buttons, secondary pages).
|
||||
|
||||
Create a `ui-tests.ps1` file that tests all the app's requirements in one pass:
|
||||
|
||||
```powershell
|
||||
# ui-tests.ps1
|
||||
param([Parameter(Mandatory)][int]$AppPid)
|
||||
# NOTE: Do NOT name the parameter $Pid — it's read-only in PowerShell
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$pass = 0; $fail = 0; $results = @()
|
||||
|
||||
# Get main window HWND (avoids PopupHost interference with JSON parsing)
|
||||
$windows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json
|
||||
$hwnd = ($windows | Where-Object { $_.title -ne "PopupHost" } | Select-Object -First 1).hwnd
|
||||
|
||||
function Test-UI {
|
||||
param([string]$Name, [scriptblock]$Script)
|
||||
# IMPORTANT: Inside $Script, use 'throw' to signal failure — NOT 'exit 1'
|
||||
# (exit terminates the entire script, not just the test)
|
||||
try {
|
||||
$output = & $Script 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$script:pass++; $script:results += @{ name = $Name; status = "PASS" }
|
||||
} else {
|
||||
$script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$output" }
|
||||
}
|
||||
} catch {
|
||||
$script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$_" }
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Element Existence ───
|
||||
Test-UI "NavHome exists" { winapp ui wait-for "NavHome" -a $AppPid -t 3000 }
|
||||
Test-UI "NavSettings exists" { winapp ui wait-for "NavSettings" -a $AppPid -t 3000 }
|
||||
|
||||
# ─── Navigation ───
|
||||
Test-UI "Navigate to Settings" { winapp ui invoke "NavSettings" -a $AppPid }
|
||||
Test-UI "Settings page loaded" { winapp ui wait-for "TxtUserName" -a $AppPid -t 3000 }
|
||||
|
||||
# ─── Interactions ───
|
||||
Test-UI "Set username" { winapp ui set-value "TxtUserName" "TestUser" -a $AppPid }
|
||||
Test-UI "Click Save" { winapp ui invoke "BtnSave" -a $AppPid } # commits the TextBox binding
|
||||
Test-UI "Username value set" {
|
||||
winapp ui wait-for "TxtUserName" -a $AppPid --value "TestUser" -t 2000
|
||||
}
|
||||
|
||||
# ─── Value assertions for different control types ───
|
||||
Test-UI "Theme is System default" {
|
||||
winapp ui wait-for "CmbTheme" -a $AppPid --value "System default" -t 2000
|
||||
}
|
||||
Test-UI "Logging is off" {
|
||||
winapp ui wait-for "TglLogging" -a $AppPid --value "Off" -t 2000
|
||||
}
|
||||
|
||||
# ─── Accessibility Audit ───
|
||||
# Only audit controls in the app's main window (exclude OS picker/popup controls)
|
||||
$allElements = (winapp ui inspect -a $AppPid --interactive --json 2>$null | ConvertFrom-Json).elements
|
||||
$appElements = @($allElements | Where-Object {
|
||||
$_.type -match 'Button|TextBox|ComboBox|CheckBox|ToggleSwitch|TabItem|Edit' -and
|
||||
$_.name -notmatch 'Minimize|Maximize|Close|System' -and # window chrome
|
||||
$_.className -notmatch 'PickerHost|#32770|CabinetWClass' # OS dialogs
|
||||
})
|
||||
$missingId = @($appElements | Where-Object { -not $_.automationId })
|
||||
if ($missingId.Count -eq 0) {
|
||||
$pass++; $results += @{ name = "All app controls have AutomationId"; status = "PASS" }
|
||||
} else {
|
||||
$fail++
|
||||
$names = ($missingId | ForEach-Object { "$($_.type) '$($_.name)'" }) -join ", "
|
||||
$results += @{ name = "AutomationId coverage"; status = "FAIL"; detail = "Missing: $names" }
|
||||
}
|
||||
|
||||
# ─── State Screenshots (capture each meaningful state for visual review) ───
|
||||
New-Item -ItemType Directory -Force -Path "screenshots" | Out-Null
|
||||
winapp ui screenshot -a $AppPid -o "screenshots/01-initial.png" 2>$null
|
||||
# ...take more screenshots after key interactions above (mode switches, dialogs opened, etc.)
|
||||
|
||||
# ─── Final Screenshot ───
|
||||
winapp ui screenshot -a $AppPid -o "test-screenshot.png" 2>$null
|
||||
|
||||
# ─── Results ───
|
||||
Write-Host "`nPassed: $pass | Failed: $fail"
|
||||
$results | Where-Object { $_.status -eq "FAIL" } | ForEach-Object {
|
||||
Write-Host " FAIL: $($_.name) — $($_.detail)" -ForegroundColor Red
|
||||
}
|
||||
$results | ConvertTo-Json | Out-File "test-results.json"
|
||||
if ($fail -gt 0) { exit 1 } else { exit 0 }
|
||||
```
|
||||
|
||||
### What to Test
|
||||
|
||||
Write tests for **every requirement** from the user's prompt:
|
||||
|
||||
| Requirement type | Test approach |
|
||||
|---|---|
|
||||
| "Has a button that does X" | `search` to verify exists, `invoke` to click, `wait-for --value` to check result |
|
||||
| "Text field shows value" | `wait-for "TxtName" --value "expected"` — works for TextBox, TextBlock, labels |
|
||||
| "Status bar contains text" | `wait-for "StatusBar" --value "words" --contains` — substring match for dynamic content |
|
||||
| "Dropdown is set to X" | `wait-for "CmbTheme" --value "Dark"` — reads the selected item automatically |
|
||||
| "Toggle is on/off" | `wait-for "TglFeature" --value "On"` — reads the toggle state |
|
||||
| "Navigation between pages" | `invoke` nav item, `wait-for` a page-specific element to appear |
|
||||
| "Open file dialog" | `invoke` trigger, `list-windows` to find picker HWND, interact with `-w` |
|
||||
| "Save file dialog" | Same as open — find picker with `list-windows`, `set-value` filename, `invoke` Save |
|
||||
| "Right-click context menu" | `click --right` on element, `invoke` the flyout MenuItem |
|
||||
| "Confirmation dialog" | `invoke` trigger, `search` for dialog buttons, `invoke` Primary/Secondary/Close |
|
||||
| "Data persists" | Set values, `invoke` a button (to commit bindings), verify data file on disk (`Get-Content` + `ConvertFrom-Json`) |
|
||||
| "All controls accessible" | `inspect --interactive --json` + check all have AutomationId |
|
||||
|
||||
### Step 3: Run and Read Results
|
||||
|
||||
```powershell
|
||||
.\ui-tests.ps1 -AppPid <PID>
|
||||
```
|
||||
|
||||
Read `test-results.json` for structured pass/fail. Only fix code if tests fail.
|
||||
|
||||
### Step 3.5: Look at the Screenshots
|
||||
|
||||
UIA assertions don't see clipping, overlap, wrong theming, or controls bleeding past their container — UIA returns `PASS` while the app is visually broken. **Capture screenshots with `winapp ui screenshot` and view each PNG.**
|
||||
|
||||
Capture the initial state and any state after a major interaction (the State Screenshots block in the script template above handles this).
|
||||
|
||||
**Visual checklist — fail the run if any item is `no`:**
|
||||
- [ ] No unintended scrollbars
|
||||
- [ ] No text ending in `…` that shouldn't be
|
||||
- [ ] Hero elements fully visible (not sliced)
|
||||
- [ ] Right-edge controls fully visible
|
||||
- [ ] No overlapping rows
|
||||
- [ ] Content uses the available width — no asymmetric dead zones (e.g. content pinned to one edge leaving empty space on the other)
|
||||
- [ ] Spacing intentional — not cramped, not unintentionally vast
|
||||
- [ ] Theming matches the user's ask (Light/Dark/HighContrast if relevant)
|
||||
- [ ] Focus/hover/error states render if tested
|
||||
|
||||
If the checklist fails, it's a bug — fix before declaring done. Window too small → grow per `winui-design` Step 4.
|
||||
|
||||
### Step 4: Fix and Rerun (if the user asked for it)
|
||||
|
||||
If tests fail:
|
||||
1. Read the failure details from `test-results.json`
|
||||
2. Batch-fix all issues in one pass
|
||||
3. Rebuild with `.\BuildAndRun.ps1` (blocking mode — shows crash info if the fix broke something)
|
||||
4. Rerun `.\ui-tests.ps1 -AppPid <PID>` (parse PID from the `launched (PID: XXXXX)` output)
|
||||
|
||||
**Maximum 2 fix-and-rerun cycles.** If the same tests keep failing after 2 cycles, report them as known issues and move on — do not keep iterating.
|
||||
|
||||
### Assertion Reference
|
||||
|
||||
Use `wait-for --value` as the primary assertion — it uses a smart fallback chain that reads the right value for any control type:
|
||||
|
||||
| Control type | `--value` reads from | Example |
|
||||
|---|---|---|
|
||||
| TextBlock / Label | Name property | `wait-for "LblTitle" --value "Home"` |
|
||||
| TextBox / NumberBox | ValuePattern | `wait-for "TxtName" --value "John"` |
|
||||
| RichEditBox | TextPattern | `wait-for "Editor" --value "Hello"` |
|
||||
| ComboBox | Selected item (SelectionPattern) | `wait-for "CmbTheme" --value "Dark"` |
|
||||
| ToggleSwitch | Toggle state (On/Off) | `wait-for "TglDark" --value "On"` |
|
||||
| CheckBox | Toggle state (On/Off) | `wait-for "ChkAgree" --value "On"` |
|
||||
|
||||
**Full assertion commands:**
|
||||
|
||||
| Assertion | Command |
|
||||
|---|---|
|
||||
| Element exists | `winapp ui wait-for "Id" -a PID -t 3000` |
|
||||
| Element has exact value | `winapp ui wait-for "Id" -a PID --value "expected" -t 3000` |
|
||||
| Value contains text | `winapp ui wait-for "Id" -a PID --value "words" --contains -t 3000` |
|
||||
| Element gone | `winapp ui wait-for "Id" -a PID --gone -t 3000` |
|
||||
| Specific property | `winapp ui wait-for "Id" -a PID -p IsEnabled --value "True" -t 3000` |
|
||||
| Button clickable | `winapp ui invoke "Id" -a PID` (exit code 0) |
|
||||
| Set then verify | `winapp ui set-value "Id" "text" -a PID` then `wait-for --value` |
|
||||
| Screenshot | `winapp ui screenshot -a PID -o path.png` |
|
||||
| Dialog appeared | `winapp ui list-windows -a PID --json` (check window count) |
|
||||
| Right-click menu | `winapp ui click "Id" -a PID --right` then `wait-for` menu item |
|
||||
| Read raw property | `winapp ui get-property "Id" -a PID -p IsEnabled --json` |
|
||||
| Read current value (no wait) | `(winapp ui get-value "Id" -a PID --json \| ConvertFrom-Json).text` — always pass `--json` when capturing into a variable (plain stdout can include advisory text like "Auto-selected HWND … from N windows"); otherwise prefer `wait-for --value` |
|
||||
| Scroll item into view | `winapp ui scroll-into-view "Id" -a PID` — call before `wait-for` on virtualized ListView/repeater items below the fold |
|
||||
| Set keyboard focus | `winapp ui focus "Id" -a PID` — cleaner than clicking another control to trigger a TextBox `LostFocus` commit |
|
||||
|
||||
### Selecting a ComboBox / dropdown item
|
||||
|
||||
*Reading* a ComboBox's current value is one step (`wait-for "Cmb" --value "Dark"`); **changing** the selection takes three, because a collapsed ComboBox's options are not in the UIA tree until it is expanded:
|
||||
|
||||
1. **Expand** - `winapp ui invoke <cmb-id>` (fires ExpandCollapsePattern; the dropdown opens).
|
||||
2. **Inspect while open** - the options now appear as `ListItem`s in a popup with their **own ids** (e.g. `itm-<option>-<suffix>`), distinct from the combo's id. Resolve the target: `winapp ui search '<option text>' -w <hwnd> --json` and pick the match with `type -eq 'ListItem'`.
|
||||
3. **Invoke that ListItem id** - `winapp ui invoke <itm-id>` (fires SelectionItemPattern). Verify with `wait-for <cmb-id> --value "<option>"`.
|
||||
|
||||
**Trap:** after expanding, do **not** invoke the option by its caption text - the text match lands on the child `Text` label, not the selectable `ListItem`, and silently no-ops. Always resolve and invoke the `itm-...` id, not the caption and not the collapsed combo id.
|
||||
|
||||
### Testing File Pickers
|
||||
|
||||
File/folder pickers (FileOpenPicker, FileSavePicker, FolderPicker) run in a separate `PickerHost` process but are fully interactable. The picker appears as an owned dialog window.
|
||||
|
||||
```powershell
|
||||
# 1. Trigger the picker
|
||||
winapp ui invoke "BtnOpenFile" -a $AppPid
|
||||
|
||||
# 2. Find the picker window (it's a dialog owned by the app window)
|
||||
Start-Sleep 1
|
||||
$allWindows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json
|
||||
$picker = $allWindows | Where-Object { $_.title -match "Open|Save" }
|
||||
$pickerHwnd = $picker.hwnd
|
||||
|
||||
# 3. Interact with the picker using -w <HWND>
|
||||
# Type a filename:
|
||||
winapp ui set-value "FileNameControlHost" "test.txt" -w $pickerHwnd
|
||||
# Click Open/Save:
|
||||
winapp ui invoke "Open" -w $pickerHwnd # or "Save", "Cancel"
|
||||
# Or cancel:
|
||||
winapp ui invoke "Cancel" -w $pickerHwnd
|
||||
|
||||
# 4. Verify the app processed the file
|
||||
winapp ui wait-for "StatusBar" -a $AppPid -p Name --value "opened" -t 3000
|
||||
```
|
||||
|
||||
**Tip:** Use `winapp ui inspect -w <pickerHwnd> --interactive` to discover the picker's controls — they include the folder tree, file list, filename textbox, and Open/Cancel buttons.
|
||||
|
||||
### Testing Context Menus and Flyouts
|
||||
|
||||
MenuFlyouts and ContextFlyouts are fully testable. They appear in the UI automation tree when open.
|
||||
|
||||
```powershell
|
||||
# 1. Right-click to open a ContextFlyout
|
||||
winapp ui click "LstItems" -a $AppPid --right
|
||||
Start-Sleep 0.5
|
||||
|
||||
# 2. The flyout MenuItems appear in the tree immediately
|
||||
# Find them with inspect or search:
|
||||
winapp ui inspect -a $AppPid --interactive # shows MnuCopy, MnuDelete, etc.
|
||||
|
||||
# 3. Click a flyout item
|
||||
winapp ui invoke "MnuCopy" -a $AppPid
|
||||
|
||||
# 4. Verify the action
|
||||
winapp ui wait-for "StatusText" -a $AppPid -p Name --value "Copied" -t 2000
|
||||
```
|
||||
|
||||
**For MenuBar flyouts** (File, Edit, View menus):
|
||||
```powershell
|
||||
# Click the menu header to open
|
||||
winapp ui invoke "FileMenu" -a $AppPid
|
||||
Start-Sleep 0.5
|
||||
# Click the sub-item
|
||||
winapp ui invoke "MenuSaveAs" -a $AppPid
|
||||
```
|
||||
|
||||
### Testing ContentDialogs
|
||||
|
||||
ContentDialogs are in-app controls (same window) — they appear directly in the UI tree when shown.
|
||||
|
||||
```powershell
|
||||
# 1. Trigger the dialog
|
||||
winapp ui invoke "BtnDelete" -a $AppPid
|
||||
Start-Sleep 0.5
|
||||
|
||||
# 2. The dialog buttons appear in the tree
|
||||
# For a standard confirmation dialog:
|
||||
winapp ui search "Primary" -a $AppPid --json # finds the primary button
|
||||
winapp ui invoke "Primary" -a $AppPid # click "Yes"/"Delete"/"Save"
|
||||
# Or:
|
||||
winapp ui invoke "Secondary" -a $AppPid # click "No"/"Don't Save"
|
||||
winapp ui invoke "Close" -a $AppPid # click "Cancel"
|
||||
|
||||
# 3. Wait for dialog to dismiss
|
||||
winapp ui wait-for "Primary" -a $AppPid --gone -t 3000
|
||||
```
|
||||
|
||||
**Tip:** ContentDialog buttons often don't have custom AutomationIds — use `inspect` to find the actual selector (slug or text match).
|
||||
|
||||
### Key Gotchas
|
||||
|
||||
- **`set-value` does NOT commit default TextBox bindings** — WinUI 3 `x:Bind TwoWay` on TextBox.Text updates the ViewModel on `LostFocus` by default. UIA `set-value` changes the text but doesn't trigger focus events. **Fix:** apps should use `UpdateSourceTrigger=PropertyChanged` on TextBox bindings (see design skill). If the app doesn't, `invoke` a button or `click` another element after `set-value` to trigger `LostFocus`.
|
||||
- **Verify persistence via the data file, not UI relaunch** — killing and relaunching a packaged app from a test script is fragile (MSIX registration timing, PID issues). Instead, check the data file on disk: `Get-Content $dataFile | ConvertFrom-Json` and verify expected values.
|
||||
- **Use `$AppPid` not `$Pid`** — `$Pid` is a read-only automatic variable in PowerShell
|
||||
- **Use `--value` without `-p`** — it auto-detects the right UIA pattern (TextPattern → ValuePattern → TogglePattern → SelectionPattern → Name). Only use `-p PropertyName --value` when you need a specific property like `IsEnabled`
|
||||
- **File pickers need `-w <HWND>`** — they run in a separate PickerHost process, so `-a PID` won't find them. Use `list-windows` to discover the picker HWND first
|
||||
- **Flyouts need a short `Start-Sleep`** after triggering — the menu items appear in the tree asynchronously
|
||||
|
||||
### CRITICAL — `invoke` vs `click`: choose the right verb
|
||||
|
||||
**`winapp ui invoke <sel>`** dispatches through UIA's **`InvokePattern` via COM IPC**:
|
||||
- ✅ Bypasses Windows UIPI (User Interface Privilege Isolation)
|
||||
- ✅ Works even when your test runs elevated and the target is non-elevated AppX
|
||||
- ✅ Does NOT steal foreground / does NOT trigger focus-loss handlers
|
||||
- ✅ Works on Buttons, ListItems, ToggleSwitches, CheckBoxes — anything that exposes `InvokePattern` or `TogglePattern`
|
||||
- ❌ Does NOT work on elements without an UIA action pattern (plain Grid, Text, Pane) — error message says "does not support any invoke pattern"
|
||||
|
||||
**`winapp ui click <sel>`** uses Win32 **`SendInput`** under the hood:
|
||||
- ❌ **BLOCKED by UIPI** when source is elevated and target is non-elevated (or any AppX) — error: `SendInput failed — the target window may be elevated`
|
||||
- ❌ Triggers foreground change → can dismiss popups, dialogs, AppX windows that hide on deactivation
|
||||
- ✅ Only use when you genuinely need a synthetic mouse click (e.g. testing mouse hover/right-click flyouts where InvokePattern is unavailable)
|
||||
- ✅ Subject to your process having interactive desktop access
|
||||
|
||||
**Rule of thumb**: try `invoke` first; only fall back to `click` if the target lacks InvokePattern AND you have a non-elevated test runner.
|
||||
|
||||
### CRITICAL — DataTemplate AutomationId vs ListItem InvokePattern
|
||||
|
||||
When XAML binds `AutomationProperties.AutomationId="{x:Bind <DataProperty>}"` inside a `ListView.ItemTemplate`'s `<DataTemplate>`, the AutomationId lives on the **inner Grid (Group)** the template produces — NOT on the outer ListItem the ListView wraps around it. The outer ListItem is what carries `InvokePattern`.
|
||||
|
||||
Concrete example (CmdPal PR #48033 binds Command.Id this way):
|
||||
|
||||
```powershell
|
||||
# This FAILS with "does not support any invoke pattern":
|
||||
winapp ui invoke 'com.microsoft.cmdpal.calculator' -w $hwnd
|
||||
# Element grp-commicrosoftcmd-XXXX (Group) does not support any invoke pattern.
|
||||
# No invokable ancestor was found.
|
||||
|
||||
# This WORKS — find by Name (matches all 3 siblings), pick the ListItem child:
|
||||
$r = winapp ui search 'Calculator' -w $hwnd --json | ConvertFrom-Json
|
||||
$li = $r.matches | Where-Object type -eq 'ListItem' | Select-Object -First 1
|
||||
winapp ui invoke $li.selector -w $hwnd # selector like 'itm-calculator-7e3f'
|
||||
```
|
||||
|
||||
If you encounter "does not support any invoke pattern" while trying to use a data-bound AutomationId, this is almost always the cause. The fix is to search by Name and invoke the sibling ListItem.
|
||||
|
||||
### CRITICAL — Keystroke input that bypasses UIPI (PostMessage)
|
||||
|
||||
`winapp ui` has no `send-keys` verb. For keystroke input into elevated/AppX targets where SendInput fails, use **inline Win32 `PostMessage WM_KEYDOWN/WM_KEYUP`** which goes through the target's message queue without UIPI checks:
|
||||
|
||||
```powershell
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class K {
|
||||
[DllImport("user32.dll", CharSet=CharSet.Auto)]
|
||||
public static extern bool PostMessage(IntPtr h, uint msg, IntPtr wp, IntPtr lp);
|
||||
public const uint WM_KEYDOWN = 0x0100;
|
||||
public const uint WM_KEYUP = 0x0101;
|
||||
}
|
||||
"@
|
||||
|
||||
function Send-KeyToHwnd {
|
||||
param([IntPtr]$Hwnd, [byte]$Vk)
|
||||
[void][K]::PostMessage($Hwnd, [K]::WM_KEYDOWN, [IntPtr]$Vk, [IntPtr]0)
|
||||
Start-Sleep -Milliseconds 30
|
||||
[void][K]::PostMessage($Hwnd, [K]::WM_KEYUP, [IntPtr]$Vk, [IntPtr]0)
|
||||
}
|
||||
|
||||
# Common VK codes:
|
||||
# 0x08 Backspace 0x09 Tab 0x0D Enter 0x1B Escape
|
||||
# 0x25 Left 0x26 Up 0x27 Right 0x28 Down
|
||||
Send-KeyToHwnd -Hwnd $h -Vk 0x28 # Down arrow
|
||||
Send-KeyToHwnd -Hwnd $h -Vk 0x0D # Enter
|
||||
```
|
||||
|
||||
**Caveats**:
|
||||
- WinUI3 apps' raw-input hooks may NOT process some keys via WM_KEYDOWN — `Esc` in particular often goes ignored (use BackButton invoke instead). Arrow keys + Enter typically work for ListView navigation.
|
||||
- PostMessage returns immediately; allow 50-200 ms before reading state.
|
||||
- Repeat `Send-KeyToHwnd` calls work for multi-step navigation (Down × 5 to scroll, then Enter).
|
||||
|
||||
### CRITICAL — Global hotkeys / PowerToys activation chords (SendInput, verified working)
|
||||
|
||||
`PostMessage` above targets a specific window's queue. To fire a **global hotkey** (e.g. a PowerToys activation chord like `Win+Shift+C`) you must inject into the **system input stream** with `SendInput` so the low-level keyboard hook (`WH_KEYBOARD_LL`) sees it. This **works for Win+ chords** — the common belief that "Win+ chords can't be injected" is false; it's almost always a **marshaling bug** (`SendInput` returns `0`, `GetLastError()==87`) from building the `INPUT[]` array in PowerShell. Build the array in C#:
|
||||
|
||||
```powershell
|
||||
Add-Type @"
|
||||
using System; using System.Runtime.InteropServices; using System.Collections.Generic;
|
||||
public static class Inj {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct INPUT { public uint type; public KEYBDINPUT ki; public int p1; public int p2; } // p1/p2 pad the union -> cb=40 on x64
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
||||
[DllImport("user32.dll", SetLastError=true)] static extern uint SendInput(uint n, INPUT[] p, int cb);
|
||||
const uint KEYUP = 0x0002;
|
||||
static INPUT K(ushort vk, bool up){ INPUT i=new INPUT(); i.type=1; i.ki.wVk=vk; i.ki.dwFlags=up?KEYUP:0; return i; }
|
||||
public static uint Chord(ushort[] mods, ushort key){ // mods down -> key tap -> mods up (reverse)
|
||||
var l=new List<INPUT>();
|
||||
foreach(var m in mods) l.Add(K(m,false));
|
||||
l.Add(K(key,false)); l.Add(K(key,true));
|
||||
for(int i=mods.Length-1;i>=0;i--) l.Add(K(mods[i],true));
|
||||
var a=l.ToArray(); return SendInput((uint)a.Length,a,Marshal.SizeOf(typeof(INPUT)));
|
||||
}
|
||||
}
|
||||
"@
|
||||
# LWIN=0x5B CTRL=0x11 SHIFT=0x10 ALT=0x12 ; main key VK from the module's settings.json "code"
|
||||
$sent = [Inj]::Chord([uint16[]]@(0x5B,0x10), [uint16]0x43) # Win+Shift+C (Color Picker)
|
||||
if ($sent -eq 0) { throw "SendInput failed err=$([Runtime.InteropServices.Marshal]::GetLastWin32Error())" }
|
||||
```
|
||||
|
||||
**Caveats**:
|
||||
- The injector must run at the **same or higher integrity level** as the hook owner (PowerToys runner). Default per-user installs run the runner at Medium IL, so a normal shell works; if the runner is elevated, run the injector elevated too (otherwise UIPI silently drops the injection).
|
||||
- Must run in the interactive desktop session.
|
||||
- OS-reserved chords (Win+L, Win+Tab) are consumed by Windows before any hook and cannot be injected this way.
|
||||
- Verify the result via the runner trace log line `… hotkey is invoked from Centralized keyboard hook` (`%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs\runner-log_<date>.log`) and/or the module's observable side-effect (overlay window, spawned editor process).
|
||||
|
||||
### CRITICAL — Verify foreground BEFORE every SendInput targeting a specific window
|
||||
|
||||
`SendInput` injects into the **session-wide** input stream — it goes to whatever IS foreground at the moment. If your target window has lost foreground (very common with AppX windows), the keys silently land in another window (often your own terminal) with no error returned.
|
||||
|
||||
Always check the foreground state immediately before calling `SendInput`. For winapp ui's output, the literal substring `foreground` appears in the line for the foreground window:
|
||||
|
||||
```powershell
|
||||
function Test-AppForeground {
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
|
||||
return ($r -match 'foreground')
|
||||
}
|
||||
|
||||
# Force foreground (works ONCE per session reliably; subsequent attempts may be blocked by
|
||||
# Windows foreground-lock):
|
||||
function Force-AppForeground {
|
||||
param([Parameter(Mandatory)][IntPtr]$Hwnd, [int]$ProcessId)
|
||||
Add-Type -TypeDefinition @'
|
||||
using System; using System.Runtime.InteropServices;
|
||||
public static class Fg {
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
||||
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
||||
[DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int pid);
|
||||
}
|
||||
'@ -EA SilentlyContinue
|
||||
[Fg]::AllowSetForegroundWindow($ProcessId) | Out-Null
|
||||
[Fg]::ShowWindow($Hwnd, 9) | Out-Null # SW_RESTORE
|
||||
$fg = [Fg]::GetForegroundWindow(); $fgPid = 0
|
||||
$fgThread = [Fg]::GetWindowThreadProcessId($fg, [ref]$fgPid)
|
||||
$curThread = [Fg]::GetCurrentThreadId()
|
||||
if ($fgThread -ne 0 -and $fgThread -ne $curThread) { [Fg]::AttachThreadInput($curThread, $fgThread, $true) | Out-Null }
|
||||
[Fg]::BringWindowToTop($Hwnd) | Out-Null
|
||||
[Fg]::SetForegroundWindow($Hwnd) | Out-Null
|
||||
if ($fgThread -ne 0 -and $fgThread -ne $curThread) { [Fg]::AttachThreadInput($curThread, $fgThread, $false) | Out-Null }
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
|
||||
# Guard pattern: abort instead of silently sending keys to wrong window
|
||||
if (-not (Test-AppForeground -AppId 'Microsoft.CmdPal.UI')) {
|
||||
Force-AppForeground -Hwnd $h -ProcessId $pid
|
||||
if (-not (Test-AppForeground -AppId 'Microsoft.CmdPal.UI')) {
|
||||
throw 'Cannot force CmdPal foreground; aborting SendInput batch'
|
||||
}
|
||||
}
|
||||
# ... now safe to SendInput ...
|
||||
```
|
||||
|
||||
**Tip**: when foreground cannot be reliably maintained, prefer `winapp ui set-value` (UIA-IPC, no foreground required) or `winapp ui invoke` (UIA InvokePattern, no foreground required) instead of SendInput.
|
||||
|
||||
### CRITICAL — `set-value` bypasses TextChanged for some apps (CmdPal alias detection)
|
||||
|
||||
`winapp ui set-value` writes the value through UIA's ValuePattern, which fires a programmatic value-change event. **It does NOT raise the `TextBox.TextChanged` event** the way real keystrokes do. For apps whose logic listens to `TextChanged` rather than to property changes — most notably CmdPal's alias detection (typing `=`, `<`, `>`, `:`, `$`, `??`, `)` in MainSearchBox triggers navigation to a provider sub-page) — `set-value` will set the text but the alias will NOT activate.
|
||||
|
||||
Workarounds:
|
||||
- For plain queries: `winapp ui set-value` works fine (CmdPal still re-runs all providers on value change).
|
||||
- For alias-triggered navigation: use **real keystrokes** via Force-AppForeground + SendInput, typing one character at a time with ~60-100ms delay so the alias detector sees the TextChanged sequence.
|
||||
- Alternative: invoke the provider tile directly by its stable AutomationId (e.g. `winapp ui invoke 'com.microsoft.cmdpal.calculator' -w $hwnd`) when you only need the destination page, not the alias path.
|
||||
|
||||
### CRITICAL — Stunted UIA tree recovery
|
||||
|
||||
After ~30+ rapid `set-value` calls or after AppX has been interactive too long, an AppX window's UIA tree can degrade to a "stunted" state where `winapp ui inspect -w $h --depth 6` returns only ~5 elements (TitleBar / Close / Min / Max / RootPane) — even though the app looks fine visually.
|
||||
|
||||
Probe + recover pattern:
|
||||
|
||||
```powershell
|
||||
# Probe: any healthy ListView-based AppX has >50 UIA nodes at depth 6
|
||||
$probe = winapp ui inspect -w $h --depth 6 --json | ConvertFrom-Json
|
||||
$nodes = 0
|
||||
$stack = [System.Collections.Stack]::new()
|
||||
if ($probe.windows[0].elements) { foreach ($e in $probe.windows[0].elements) { $stack.Push($e) } }
|
||||
while ($stack.Count -gt 0) {
|
||||
$n = $stack.Pop(); $nodes++
|
||||
if ($n.PSObject.Properties['children']) { foreach ($c in $n.children) { $stack.Push($c) } }
|
||||
}
|
||||
|
||||
if ($nodes -lt 6) {
|
||||
Write-Warning "UIA tree stunted ($nodes nodes); restarting AppX"
|
||||
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | ForEach-Object {
|
||||
Stop-Process -Id $_.Id -Force
|
||||
Wait-Process -Id $_.Id -Timeout 5 -EA SilentlyContinue
|
||||
}
|
||||
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
|
||||
Start-Sleep 5
|
||||
# Re-resolve HWND with list-windows
|
||||
}
|
||||
```
|
||||
|
||||
### Settings.json mutation safety contract
|
||||
|
||||
When the only realistic way to reach a needed test state is editing the app's persistent settings (e.g. multi-select that the UI's `SelectionItemPattern.Select` clobbers), wrap mutations with **byte-identical backup + restore-on-exit**:
|
||||
|
||||
```powershell
|
||||
$settings = "$env:LOCALAPPDATA\Packages\Microsoft.CommandPalette_8wekyb3d8bbwe\LocalState\settings.json"
|
||||
$backup = "$env:TEMP\settings-backup-$(Get-Random).json"
|
||||
$origBytes = [System.IO.File]::ReadAllBytes($settings)
|
||||
[System.IO.File]::WriteAllBytes($backup, $origBytes)
|
||||
try {
|
||||
# 1. Stop the AppX so we can write the file (apps usually hold it open)
|
||||
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep 1
|
||||
# 2. Mutate
|
||||
$j = $origBytes | ForEach-Object { [char]$_ } | Join-String | ConvertFrom-Json
|
||||
$j.SomeKey = 'TestValue'
|
||||
[System.IO.File]::WriteAllBytes($settings, [System.Text.Encoding]::UTF8.GetBytes(($j | ConvertTo-Json -Depth 10)))
|
||||
# 3. Restart AppX so it re-reads the mutated settings
|
||||
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
|
||||
Start-Sleep 5
|
||||
# 4. ... run your test ...
|
||||
} finally {
|
||||
# ALWAYS restore — verify byte-identical via length + SHA256
|
||||
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue
|
||||
Start-Sleep 1
|
||||
[System.IO.File]::WriteAllBytes($settings, $origBytes)
|
||||
$check = [System.IO.File]::ReadAllBytes($settings)
|
||||
if ($check.Length -ne $origBytes.Length) { Write-Error "Restore length mismatch!" }
|
||||
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: this should be used ONLY when the UI route is unreachable. Any setting flippable through the AppX Settings UI should be flipped that way instead (it's the documented user flow and tests real binding code).
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# scripts/pt-admin-probe.ps1
|
||||
# Verify the current session is elevated AND that PT runner inherits the admin token.
|
||||
|
||||
if (-not ('PtTok' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class PtTok {
|
||||
[DllImport("advapi32.dll", SetLastError=true)]
|
||||
public static extern bool OpenProcessToken(IntPtr h, uint da, out IntPtr t);
|
||||
[DllImport("advapi32.dll", SetLastError=true)]
|
||||
public static extern bool GetTokenInformation(IntPtr t, uint c, IntPtr ti, uint l, out uint rl);
|
||||
[DllImport("kernel32.dll")] public static extern IntPtr GetCurrentProcess();
|
||||
[DllImport("kernel32.dll")] public static extern IntPtr OpenProcess(uint da, bool inh, int pid);
|
||||
[DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h);
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
function Test-PtAdmin {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether the current session is elevated by reading the process token's TokenElevation
|
||||
information class (20). Returns $true if elevated.
|
||||
#>
|
||||
[CmdletBinding()] param()
|
||||
$t = [IntPtr]::Zero
|
||||
[PtTok]::OpenProcessToken([PtTok]::GetCurrentProcess(), 8, [ref]$t) | Out-Null
|
||||
$ti = [Runtime.InteropServices.Marshal]::AllocHGlobal(4)
|
||||
$rl = 0
|
||||
try {
|
||||
[PtTok]::GetTokenInformation($t, 20, $ti, 4, [ref]$rl) | Out-Null
|
||||
return ([Runtime.InteropServices.Marshal]::ReadInt32($ti) -eq 1)
|
||||
} finally {
|
||||
[Runtime.InteropServices.Marshal]::FreeHGlobal($ti)
|
||||
[PtTok]::CloseHandle($t) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ProcessElevated {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether a specific PID is elevated (TokenElevation = 1).
|
||||
#>
|
||||
[CmdletBinding()] param([Parameter(Mandatory)][int]$ProcessId)
|
||||
$proc = [PtTok]::OpenProcess(0x1000, $false, $ProcessId) # PROCESS_QUERY_LIMITED_INFORMATION
|
||||
if ($proc -eq [IntPtr]::Zero) { return $null }
|
||||
try {
|
||||
$t = [IntPtr]::Zero
|
||||
if (-not [PtTok]::OpenProcessToken($proc, 8, [ref]$t)) { return $null }
|
||||
try {
|
||||
$ti = [Runtime.InteropServices.Marshal]::AllocHGlobal(4)
|
||||
$rl = 0
|
||||
try {
|
||||
[PtTok]::GetTokenInformation($t, 20, $ti, 4, [ref]$rl) | Out-Null
|
||||
return ([Runtime.InteropServices.Marshal]::ReadInt32($ti) -eq 1)
|
||||
} finally { [Runtime.InteropServices.Marshal]::FreeHGlobal($ti) }
|
||||
} finally { [PtTok]::CloseHandle($t) | Out-Null }
|
||||
} finally { [PtTok]::CloseHandle($proc) | Out-Null }
|
||||
}
|
||||
|
||||
function Test-PtRunnerAdmin {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether the PT runner (PowerToys.exe) is currently running elevated.
|
||||
.OUTPUTS
|
||||
PSCustomObject with .Found (bool), .Pid (int), .Elevated (bool|$null)
|
||||
#>
|
||||
$pt = Get-Process PowerToys -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $pt) { return [pscustomobject]@{ Found=$false; Pid=$null; Elevated=$null } }
|
||||
[pscustomobject]@{
|
||||
Found = $true
|
||||
Pid = $pt.Id
|
||||
Elevated = (Test-ProcessElevated -ProcessId $pt.Id)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
# scripts/pt-clipboard-diff.ps1
|
||||
# Multi-format clipboard inspection. Used to assert that AdvancedPaste plain-paste actually strips
|
||||
# rich formats while preserving UnicodeText (and similar before/after assertions).
|
||||
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
function Get-PtClipboardFormats {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the list of format names currently on the clipboard (e.g. UnicodeText, HTML Format,
|
||||
Rich Text Format, FileDrop, DeviceIndependentBitmap, etc.).
|
||||
#>
|
||||
$obj = [System.Windows.Forms.Clipboard]::GetDataObject()
|
||||
if (-not $obj) { return @() }
|
||||
return $obj.GetFormats()
|
||||
}
|
||||
|
||||
function Get-PtClipboardText {
|
||||
[System.Windows.Forms.Clipboard]::GetText()
|
||||
}
|
||||
|
||||
function Compare-PtClipboardFormatDiff {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Diff helper. Given a 'before' formats list (from Get-PtClipboardFormats), return:
|
||||
- Added: formats present in current clipboard but not in before
|
||||
- Removed: formats present in before but not in current
|
||||
- Common: formats present in both
|
||||
.EXAMPLE
|
||||
$before = Get-PtClipboardFormats # e.g. UnicodeText + HTML Format + RTF
|
||||
# ... user/script triggers AP plain-paste ...
|
||||
$diff = Compare-PtClipboardFormatDiff -Before $before
|
||||
# $diff.Removed should contain 'HTML Format' and 'Rich Text Format'
|
||||
# $diff.Common should still contain 'UnicodeText'
|
||||
#>
|
||||
param([Parameter(Mandatory)][string[]]$Before)
|
||||
$current = Get-PtClipboardFormats
|
||||
[pscustomobject]@{
|
||||
Before = $Before
|
||||
Current = $current
|
||||
Added = @($current | Where-Object { $_ -notin $Before })
|
||||
Removed = @($Before | Where-Object { $_ -notin $current })
|
||||
Common = @($current | Where-Object { $_ -in $Before })
|
||||
}
|
||||
}
|
||||
|
||||
function Set-PtClipboardRich {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Put HTML + UnicodeText on the clipboard so plain-paste detection has something to strip.
|
||||
Useful as test fixture before invoking AdvancedPaste.PasteAsPlainText.
|
||||
#>
|
||||
param(
|
||||
[string]$Text = 'Hello world',
|
||||
[string]$Html = '<html><body><b>Hello</b> <i>world</i></body></html>'
|
||||
)
|
||||
$obj = New-Object System.Windows.Forms.DataObject
|
||||
$obj.SetText($Text, [System.Windows.Forms.TextDataFormat]::UnicodeText)
|
||||
$obj.SetText($Html, [System.Windows.Forms.TextDataFormat]::Html)
|
||||
[System.Windows.Forms.Clipboard]::SetDataObject($obj, $true)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
# scripts/pt-cmdpal-recycle.ps1
|
||||
# Recover CmdPal AppX from "stuck" states (TextChanged-broken, sub-page hang, foreground-lock).
|
||||
# The helper Microsoft.CmdPal.Ext.PowerToys is kept alive so the CmdPal.Show event listener wiring
|
||||
# is not lost on recycle.
|
||||
|
||||
function Reset-CmdPalAppX {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Kill the Microsoft.CmdPal.UI process and relaunch the AppX. Returns the new HWND or 0 on failure.
|
||||
.NOTES
|
||||
Symptoms requiring this:
|
||||
- set-value MainSearchBox echoes the text but ZERO ListItems appear within 1.5s
|
||||
- winapp ui invoke <button> hangs subsequent inspect calls
|
||||
- Force-PtForeground returns false repeatedly
|
||||
#>
|
||||
$cp = Get-Process Microsoft.CmdPal.UI -ErrorAction SilentlyContinue
|
||||
if ($cp) {
|
||||
Stop-Process -Id $cp.Id -Force
|
||||
$deadline = (Get-Date).AddSeconds(5)
|
||||
while ((Get-Process -Id $cp.Id -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
}
|
||||
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
|
||||
$deadline = (Get-Date).AddSeconds(10)
|
||||
do {
|
||||
Start-Sleep -Milliseconds 300
|
||||
$r = winapp ui list-windows -a Microsoft.CmdPal.UI 2>$null | Out-String
|
||||
if ($r -match 'HWND (\d+):') { return [IntPtr][int64]$matches[1] }
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
return [IntPtr]::Zero
|
||||
}
|
||||
|
||||
function Reset-CmdPalToHome {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Navigate CmdPal back to the home page from any sub-page by invoking BackButton via UIA.
|
||||
CmdPal's Esc handler is unreachable via SendInput from elevated sessions (UIPI), and Esc-via-
|
||||
PostMessage is filtered by the WinUI 3 raw-input hook. BackButton invoke via UIA InvokePattern
|
||||
works regardless.
|
||||
#>
|
||||
$homePlaceholder = 'Search for apps, files and commands'
|
||||
for ($i = 0; $i -lt 6; $i++) {
|
||||
$cur = winapp ui get-value 'MainSearchBox' -a Microsoft.CmdPal.UI 2>$null
|
||||
if ($cur -and ($cur -match [regex]::Escape($homePlaceholder))) { break }
|
||||
winapp ui invoke 'BackButton' -a Microsoft.CmdPal.UI 2>$null | Out-Null
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
# Re-signal Show in case BackButton dismissed the window
|
||||
if (Get-Command Invoke-PtSharedEvent -ErrorAction SilentlyContinue) {
|
||||
try { Invoke-PtSharedEvent -Name 'CmdPal.Show' | Out-Null } catch {}
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
function Test-CmdPalDegraded {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Probe the AppX with a known-good query ('notepad') and verify >=1 ListItem appears within
|
||||
1500ms. Returns $true if degraded (TextChanged-broken).
|
||||
#>
|
||||
Reset-CmdPalToHome
|
||||
winapp ui set-value 'MainSearchBox' 'notepad' -a Microsoft.CmdPal.UI 2>$null | Out-Null
|
||||
$deadline = (Get-Date).AddMilliseconds(1500)
|
||||
do {
|
||||
$insLines = (winapp ui inspect -a Microsoft.CmdPal.UI --depth 7 -i 2>$null) -split "`n"
|
||||
$items = $insLines | Where-Object { $_ -match 'itm-' -and $_ -match 'ListItem' }
|
||||
if ($items.Count -gt 0) {
|
||||
winapp ui set-value 'MainSearchBox' '' -a Microsoft.CmdPal.UI 2>$null | Out-Null
|
||||
return $false
|
||||
}
|
||||
Start-Sleep -Milliseconds 150
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
return $true
|
||||
}
|
||||
|
||||
function Invoke-CmdPalQuery {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Type a query into MainSearchBox after returning to home. Auto-recovers if AppX is degraded.
|
||||
Returns the result items as an array of strings (text lines starting with itm-).
|
||||
.EXAMPLE
|
||||
$items = Invoke-CmdPalQuery -Query 'notepad'
|
||||
if ($items | Where-Object { $_ -match 'Notepad' }) { 'PASS' } else { 'FAIL' }
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$Query, [int]$WaitMs = 800)
|
||||
Reset-CmdPalToHome
|
||||
winapp ui set-value 'MainSearchBox' $Query -a Microsoft.CmdPal.UI 2>$null | Out-Null
|
||||
Start-Sleep -Milliseconds $WaitMs
|
||||
$out = winapp ui inspect -a Microsoft.CmdPal.UI --depth 7 -i 2>$null | Out-String
|
||||
$items = ($out -split "`r?`n" | Where-Object { $_ -match 'itm-' -and $_ -match 'ListItem' })
|
||||
if ($items.Count -eq 0) {
|
||||
if (Test-CmdPalDegraded) {
|
||||
Reset-CmdPalAppX | Out-Null
|
||||
return (Invoke-CmdPalQuery -Query $Query -WaitMs $WaitMs)
|
||||
}
|
||||
}
|
||||
return $items
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
# scripts/pt-explorer-com.ps1
|
||||
# Drive Explorer windows via Shell.Application COM to set up file selections, then trigger
|
||||
# PT modules that read IShellItemArray from the foreground Explorer window (Peek, Image Resizer,
|
||||
# PowerRename, File Locksmith, Workspaces).
|
||||
#
|
||||
# This bypasses needing a real mouse / interactive selection - Shell COM does the selection
|
||||
# programmatically, then the PT hotkey (e.g. Ctrl+Space for Peek) fires the centralized hook
|
||||
# which reads Explorer's selection at the moment of activation.
|
||||
#
|
||||
# Requires an interactive desktop session. If GetForegroundWindow() returns 0 or no Explorer
|
||||
# windows are open, the functions return $null/$false instead of throwing - callers should
|
||||
# treat that as a BLK-ENV signal (an environment block, not a product FAIL).
|
||||
|
||||
function Get-PtExplorerWindows {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return all open Explorer windows as Shell COM objects (with .LocationName, .Document.Folder, etc.).
|
||||
Returns @() if no Explorer windows are open.
|
||||
#>
|
||||
try {
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
return @($shell.Windows() | Where-Object { $_.Name -eq 'File Explorer' -or $_.FullName -match 'explorer\.exe$' })
|
||||
} catch { return @() }
|
||||
}
|
||||
|
||||
function Open-PtExplorerAtPath {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Open a fresh Explorer window at the given path. Returns the Shell COM window object.
|
||||
Useful when no Explorer is open yet.
|
||||
#>
|
||||
[CmdletBinding()] param([Parameter(Mandatory)][string]$Path)
|
||||
if (-not (Test-Path $Path)) { throw "Path not found: $Path" }
|
||||
Start-Process explorer.exe -ArgumentList $Path
|
||||
Start-Sleep -Milliseconds 1500
|
||||
$wins = Get-PtExplorerWindows
|
||||
# Note: the -replace must be wrapped in its own parens, otherwise the ',' in -replace '\\','/'
|
||||
# is parsed as a second argument to [regex]::Escape() (overload error: "argument count: 2").
|
||||
$needle = [regex]::Escape(((Resolve-Path $Path).Path -replace '\\','/'))
|
||||
return ($wins | Where-Object { $_.LocationURL -match $needle } | Select-Object -First 1)
|
||||
}
|
||||
|
||||
function Select-PtExplorerFiles {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Select 1+ files in an open Explorer window via Shell COM. The window comes to foreground.
|
||||
.DESCRIPTION
|
||||
Uses Shell.Application's SelectItem(item, flags) API. Flags:
|
||||
0x01 = SVSI_SELECT
|
||||
0x04 = SVSI_DESELECTOTHERS (apply to the first item only when selecting multiple)
|
||||
0x08 = SVSI_ENSUREVISIBLE
|
||||
0x20 = SVSI_FOCUSED
|
||||
Returns $true on success, $false if any file wasn't found in the folder.
|
||||
.EXAMPLE
|
||||
$win = Get-PtExplorerWindows | Select-Object -First 1
|
||||
Select-PtExplorerFiles -ExplorerWindow $win -FileNames 'test-markdown.md','test-html.html','test-source.cs'
|
||||
Send-PtChord -Mods 0x11 -Key 0x20 # Ctrl+Space -> Peek opens on 3 selected files
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]$ExplorerWindow,
|
||||
[Parameter(Mandatory)][string[]]$FileNames
|
||||
)
|
||||
if (-not $ExplorerWindow.Document) { return $false }
|
||||
$folder = $ExplorerWindow.Document.Folder
|
||||
$first = $true
|
||||
foreach ($name in $FileNames) {
|
||||
$item = $folder.ParseName($name)
|
||||
if (-not $item) { Write-Warning "File not found in folder: $name"; return $false }
|
||||
# First item: SELECT + DESELECTOTHERS + ENSUREVISIBLE + FOCUSED = 0x2D
|
||||
# Subsequent items: SELECT + ENSUREVISIBLE = 0x09
|
||||
$flags = if ($first) { 0x2D } else { 0x09 }
|
||||
$ExplorerWindow.Document.SelectItem($item, $flags)
|
||||
$first = $false
|
||||
}
|
||||
Start-Sleep -Milliseconds 300
|
||||
return $true
|
||||
}
|
||||
|
||||
function Invoke-PtPeekWithExplorerSelection {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Set up an Explorer multi-file selection and trigger Peek via Ctrl+Space.
|
||||
Returns the new Peek window HWND, or $null on failure.
|
||||
.EXAMPLE
|
||||
$h = Invoke-PtPeekWithExplorerSelection -FolderPath D:\fixtures -FileNames 'a.png','b.md','c.cs'
|
||||
winapp ui invoke PinButton -w $h
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$FolderPath,
|
||||
[Parameter(Mandatory)][string[]]$FileNames
|
||||
)
|
||||
$win = Get-PtExplorerWindows | Where-Object { $_.LocationURL -match [regex]::Escape(($FolderPath -replace '\\','/')) } | Select-Object -First 1
|
||||
if (-not $win) { $win = Open-PtExplorerAtPath -Path $FolderPath }
|
||||
if (-not $win) { return $null }
|
||||
if (-not (Select-PtExplorerFiles -ExplorerWindow $win -FileNames $FileNames)) { return $null }
|
||||
|
||||
# Capture pre-state Peek HWND list to detect the new window
|
||||
$beforeHwnds = @(Get-Process PowerToys.Peek.UI -EA SilentlyContinue | ForEach-Object MainWindowHandle)
|
||||
|
||||
# Fire Ctrl+Space (Peek default). Requires pt-sendinput-chord.ps1 to be dot-sourced first.
|
||||
if (-not (Get-Command Send-PtChord -EA SilentlyContinue)) {
|
||||
throw "Send-PtChord not loaded. Dot-source scripts/pt-sendinput-chord.ps1 first."
|
||||
}
|
||||
Send-PtChord -Mods 0x11 -Key 0x20 | Out-Null # Ctrl+Space
|
||||
Start-Sleep -Milliseconds 1200
|
||||
|
||||
# Find the new Peek window HWND
|
||||
$afterHwnds = @(Get-Process PowerToys.Peek.UI -EA SilentlyContinue | ForEach-Object MainWindowHandle)
|
||||
$new = $afterHwnds | Where-Object { $_ -ne 0 -and $_ -notin $beforeHwnds } | Select-Object -First 1
|
||||
if (-not $new) { $new = $afterHwnds | Where-Object { $_ -ne 0 } | Select-Object -First 1 }
|
||||
return $new
|
||||
}
|
||||
|
||||
function Test-PtInteractiveDesktop {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Probe whether the current session is interactive (foreground + Shell COM both working).
|
||||
Returns a PSCustomObject with .ForegroundOk and .ShellComOk.
|
||||
.EXAMPLE
|
||||
$env = Test-PtInteractiveDesktop
|
||||
if (-not $env.ForegroundOk -or -not $env.ShellComOk) {
|
||||
Write-Warning "Non-interactive session - Explorer-driven techniques will fail."
|
||||
}
|
||||
#>
|
||||
Add-Type 'using System; using System.Runtime.InteropServices; public class FG3 { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }' -EA SilentlyContinue
|
||||
$hasFg = $false
|
||||
for ($i = 0; $i -lt 5; $i++) {
|
||||
if ([FG3]::GetForegroundWindow() -ne [IntPtr]::Zero) { $hasFg = $true; break }
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
$shellOk = $false
|
||||
try { @((New-Object -ComObject Shell.Application).Windows()).Count | Out-Null; $shellOk = $true } catch {}
|
||||
[pscustomobject]@{ ForegroundOk = $hasFg; ShellComOk = $shellOk }
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
# pt-explorer-contextmenu.ps1 - drive any Explorer (Win11) context-menu PowerToys module
|
||||
# end-to-end the way a real user does: open Explorer, select file(s), synthetic right-click
|
||||
# to OPEN the menu, then UIA-invoke the module's menu item by NAME (robust - no coordinate
|
||||
# click). Used by File Locksmith, Image Resizer, PowerRename, New+, etc.
|
||||
#
|
||||
# See explorer-context-menu-flow.md for the full write-up, stability notes, and per-module captions.
|
||||
#
|
||||
# Requires an UNLOCKED interactive desktop (synthetic right-click needs foreground). Check first:
|
||||
# if ([PtFg]::GetForegroundWindow() -eq [IntPtr]::Zero) -> desktop locked -> BLK-ENV.
|
||||
|
||||
Add-Type -TypeDefinition @'
|
||||
using System; using System.Runtime.InteropServices;
|
||||
public static class PtCtx {
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int c);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
||||
[DllImport("user32.dll",CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string cls, string win);
|
||||
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
||||
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] public static extern void mouse_event(uint f, uint dx, uint dy, uint d, IntPtr e);
|
||||
[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte s, uint f, IntPtr e);
|
||||
public const uint RIGHTDOWN=0x0008, RIGHTUP=0x0010, LEFTDOWN=0x0002, LEFTUP=0x0004, KEYUP=0x0002;
|
||||
// Shift+F10 = "open context menu for the current selection/focus". With NOTHING selected it opens the
|
||||
// folder-BACKGROUND menu - coordinate-free, so it can't miss a row or land in the preview pane.
|
||||
public static void ShiftF10() {
|
||||
keybd_event(0x10,0,0,IntPtr.Zero); System.Threading.Thread.Sleep(40);
|
||||
keybd_event(0x79,0,0,IntPtr.Zero); System.Threading.Thread.Sleep(60); keybd_event(0x79,0,KEYUP,IntPtr.Zero);
|
||||
System.Threading.Thread.Sleep(40); keybd_event(0x10,0,KEYUP,IntPtr.Zero);
|
||||
}
|
||||
public static void EscKey() { keybd_event(0x1B,0,0,IntPtr.Zero); System.Threading.Thread.Sleep(40); keybd_event(0x1B,0,KEYUP,IntPtr.Zero); }
|
||||
public static void ForceForeground(IntPtr h) {
|
||||
IntPtr fg = GetForegroundWindow(); uint fp;
|
||||
uint ft = GetWindowThreadProcessId(fg, out fp); uint ct = GetCurrentThreadId();
|
||||
ShowWindow(h, 9);
|
||||
if (ft != 0 && ft != ct) AttachThreadInput(ct, ft, true);
|
||||
BringWindowToTop(h); SetForegroundWindow(h);
|
||||
if (ft != 0 && ft != ct) AttachThreadInput(ct, ft, false);
|
||||
}
|
||||
public static void RightClick(int x, int y) {
|
||||
SetCursorPos(x, y); System.Threading.Thread.Sleep(250);
|
||||
mouse_event(RIGHTDOWN,0,0,0,IntPtr.Zero); System.Threading.Thread.Sleep(70); mouse_event(RIGHTUP,0,0,0,IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
'@ -ErrorAction SilentlyContinue
|
||||
|
||||
function Test-PtDesktopInteractive {
|
||||
# Polls up to $TimeoutSec for a foreground window. A momentary 0 is common for a few seconds
|
||||
# right after Restart-PtRunner / Explorer restart - without the poll that blip is misclassified
|
||||
# as a locked desktop (false BLK-ENV). A genuinely locked/non-interactive desktop stays 0 for
|
||||
# the whole window and still returns $false.
|
||||
param([int]$TimeoutSec = 5)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
if ([PtCtx]::GetForegroundWindow() -ne [IntPtr]::Zero) { return $true }
|
||||
Start-Sleep -Milliseconds 250
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
return $false
|
||||
}
|
||||
|
||||
# Opens the Win11 context menu for a file in an already-open Explorer window and returns the
|
||||
# menu popup HWND. $ExplorerHwnd = the CabinetWClass window; $FileName = item to right-click.
|
||||
function Open-PtExplorerContextMenu {
|
||||
param([Parameter(Mandatory)][int]$ExplorerHwnd, [Parameter(Mandatory)][string]$FileName, [int]$MaxTries = 3)
|
||||
if (-not (Test-PtDesktopInteractive)) { throw 'BLK-ENV: desktop is locked / no foreground (GetForegroundWindow()=0). Unlock and retry.' }
|
||||
# Turn off the preview pane (best-effort) so a fallback coordinate right-click can't land in the
|
||||
# preview surface -> folder-BACKGROUND menu (rejected by the honesty guard below).
|
||||
try { Disable-PtPreviewPane -ExplorerHwnd $ExplorerHwnd | Out-Null } catch { }
|
||||
$fgEverOk = $false
|
||||
for ($try = 1; $try -le $MaxTries; $try++) {
|
||||
# Bring Explorer to the foreground AND confirm it actually stuck. Shift+F10 and the coordinate
|
||||
# fallback both need the target window to OWN the foreground + keyboard focus. If the input
|
||||
# desktop is detached (RDP minimized/disconnected, workstation locked -> GetForegroundWindow()=0)
|
||||
# or another process holds the foreground-lock, ForceForeground silently no-ops, the keystrokes
|
||||
# go nowhere, and the menu never opens. Track whether foreground ever stuck so the throw below
|
||||
# can distinguish this environment problem from a genuine wrong-menu open.
|
||||
$fgThisTry = $false
|
||||
for ($f = 0; $f -lt 3; $f++) {
|
||||
[PtCtx]::ForceForeground([IntPtr]$ExplorerHwnd); Start-Sleep -Milliseconds 400
|
||||
if ([PtCtx]::GetForegroundWindow() -eq [IntPtr]$ExplorerHwnd) { $fgThisTry = $true; $fgEverOk = $true; break }
|
||||
}
|
||||
# HARD GATE: Explorer must actually OWN the foreground before we synthesize input. If it never
|
||||
# stuck (locked/secure desktop, or another window holds the foreground-lock), the Shift+F10 /
|
||||
# coordinate-click below would go nowhere - so skip them and retry rather than waste keystrokes.
|
||||
# If it never sticks across all $MaxTries, $fgEverOk stays $false and we throw BLK-ENV (below).
|
||||
if (-not $fgThisTry) { Start-Sleep -Milliseconds 500; continue }
|
||||
# Select the target via Shell COM first (reliable, coordinate-free, scrolls it into view) so the
|
||||
# right-click has a guaranteed-visible selected row - never opens the file / never renames it.
|
||||
try {
|
||||
$sh = New-Object -ComObject Shell.Application
|
||||
$w = $sh.Windows() | Where-Object { $_.HWND -eq $ExplorerHwnd } | Select-Object -First 1
|
||||
if ($w -and $w.Document) {
|
||||
$it = $w.Document.Folder.Items() | Where-Object { $_.Name -eq $FileName -or $_.Name -like "$FileName*" } | Select-Object -First 1
|
||||
if ($it) { $w.Document.SelectItem($it, 0x2D); Start-Sleep -Milliseconds 300 }
|
||||
}
|
||||
} catch { }
|
||||
$item = (winapp ui search $FileName -w $ExplorerHwnd --json 2>$null | ConvertFrom-Json).matches |
|
||||
Where-Object { $_.type -eq 'ListItem' } | Select-Object -First 1
|
||||
if (-not $item) { Start-Sleep -Milliseconds 500; continue } # transient (mid-populate) - retry
|
||||
# OPEN the menu COORDINATE-FREE (preferred): Shift+F10 acts on the COM-selected item - no pixel
|
||||
# involved, so a preview pane / DPI / wide Details row / screen-edge row can't misplace the click
|
||||
# (measured 10/10 on fresh windows). First restore KEYBOARD focus to the list item so Shift+F10
|
||||
# targets the file and not the nav tree / address bar (verified: focus-in-tree -> wrong menu).
|
||||
winapp ui focus $item.selector -w $ExplorerHwnd 2>$null | Out-Null; Start-Sleep -Milliseconds 200
|
||||
[PtCtx]::ShiftF10()
|
||||
# Poll for the Shift+F10 menu (the PopupHost can take >1.2s to render under RDP / slow foreground)
|
||||
# before degrading to the fragile coordinate fallback below. ~3.6s budget (12 x 300ms).
|
||||
$menu = $null
|
||||
for ($poll = 0; $poll -lt 12 -and -not $menu; $poll++) {
|
||||
Start-Sleep -Milliseconds 300
|
||||
$menu = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.className -match 'PopupWindowSiteBridge' } | Sort-Object height -Descending | Select-Object -First 1
|
||||
}
|
||||
if (-not $menu) { # Shift+F10 produced nothing - fall back to the coordinate right-click by selector
|
||||
winapp ui click --right $item.selector -w $ExplorerHwnd 2>$null | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
$menu = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.className -match 'PopupWindowSiteBridge' } | Sort-Object height -Descending | Select-Object -First 1
|
||||
}
|
||||
if (-not $menu) { # still nothing - last-resort coordinate right-click on the row point
|
||||
[PtCtx]::RightClick([int]($item.x + [Math]::Min(80, $item.width/2)), [int]($item.y + $item.height/2))
|
||||
Start-Sleep -Seconds 2
|
||||
$menu = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.className -match 'PopupWindowSiteBridge' } | Sort-Object height -Descending | Select-Object -First 1
|
||||
}
|
||||
if ($menu) {
|
||||
# HONESTY GUARD: confirm we actually opened the FILE item's menu, not the folder background
|
||||
# (winapp's click point can land in the preview pane / empty canvas -> background menu). A file
|
||||
# menu has Open/Cut/Copy/Delete; the background menu has View/Sort by/Group by. Never return
|
||||
# the background menu here and pretend it's the file's - retry, then fail loudly.
|
||||
$names = Get-PtContextMenuItems -MenuHwnd $menu.hwnd
|
||||
if ($names -match '^(Open|Cut|Copy|Delete|Rename)$') { return $menu.hwnd }
|
||||
[PtCtx]::EscKey(); Start-Sleep -Milliseconds 400 # wrong (background) menu - close and retry
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
if (-not $fgEverOk) {
|
||||
throw "BLK-ENV: could not bring Explorer (HWND $ExplorerHwnd) to the foreground after $MaxTries attempts - GetForegroundWindow() never matched it. This is a detached/locked input desktop (RDP minimized or disconnected, workstation locked, screensaver) or another window holds the foreground-lock; Shift+F10 keystrokes are dropped in this state. Restore an attached interactive desktop and retry. See references/environment-setup.md and SKILL.md pitfall #7."
|
||||
}
|
||||
throw "Could not open the FILE context menu for '$FileName' after $MaxTries attempts (got background/none) even though Explorer had the foreground. If the module's entry appears on the folder menu (PowerRename, New+), use Open-PtBackgroundContextMenu instead."
|
||||
}
|
||||
|
||||
# Opens the folder-BACKGROUND context menu (no file needed) via Shift+F10 with nothing selected -
|
||||
# coordinate-free, so it can't miss a row or land in the preview pane. Use this for modules whose entry
|
||||
# appears on the folder menu (PowerRename, New+). NOT valid for File Locksmith / Image Resizer, whose
|
||||
# entries only appear on a selected file (Image Resizer: an image) - use Open-PtExplorerContextMenu +
|
||||
# Select-PtExplorerFiles for those.
|
||||
function Open-PtBackgroundContextMenu {
|
||||
param([Parameter(Mandatory)][int]$ExplorerHwnd, [int]$MaxTries = 3)
|
||||
if (-not (Test-PtDesktopInteractive)) { throw 'BLK-ENV: desktop is locked / no foreground (GetForegroundWindow()=0). Unlock and retry.' }
|
||||
for ($try = 1; $try -le $MaxTries; $try++) {
|
||||
[PtCtx]::ForceForeground([IntPtr]$ExplorerHwnd); Start-Sleep -Milliseconds 400
|
||||
[PtCtx]::EscKey(); Start-Sleep -Milliseconds 200 # clear any selection so Shift+F10 targets the folder background
|
||||
# Put KEYBOARD focus in the FILE-LIST (not the nav tree / address bar) so Shift+F10 opens the
|
||||
# folder-background menu, not a tree node's menu (verified: focus-in-tree -> wrong menu, module entry
|
||||
# absent). Do this LAST before Shift+F10. Coordinate-free via UIA SetFocus.
|
||||
$listSel = (winapp ui search 'Items View' -w $ExplorerHwnd --json 2>$null | ConvertFrom-Json).matches |
|
||||
Where-Object { $_.type -eq 'List' } | Select-Object -First 1 -ExpandProperty selector
|
||||
if ($listSel) { winapp ui focus $listSel -w $ExplorerHwnd 2>$null | Out-Null; Start-Sleep -Milliseconds 200 }
|
||||
[PtCtx]::ShiftF10()
|
||||
# Poll for the background menu (PopupHost can take >2s to render under RDP / slow foreground). ~3.6s budget.
|
||||
$menu = $null
|
||||
for ($poll = 0; $poll -lt 12 -and -not $menu; $poll++) {
|
||||
Start-Sleep -Milliseconds 300
|
||||
$menu = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.className -match 'PopupWindowSiteBridge' } | Sort-Object height -Descending | Select-Object -First 1
|
||||
}
|
||||
if ($menu) {
|
||||
# Confirm it's the BACKGROUND menu (View/Sort by/Group by), not an item menu.
|
||||
$names = Get-PtContextMenuItems -MenuHwnd $menu.hwnd
|
||||
if ($names -match '^(View|Sort by|Group by)$') { return $menu.hwnd }
|
||||
[PtCtx]::EscKey(); Start-Sleep -Milliseconds 400
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
throw "Could not open the folder BACKGROUND context menu after $MaxTries attempts."
|
||||
}
|
||||
|
||||
# Invokes a context-menu item by its visible NAME (robust - UIA InvokePattern, no coord click).
|
||||
# Returns $true if invoked. Match the module caption, e.g.:
|
||||
# File Locksmith -> 'Unlock with File Locksmith' PowerRename -> 'Rename with PowerRename'
|
||||
# Image Resizer -> 'Resize images' (verify by enumerating) New+ -> 'New+'
|
||||
function Invoke-PtContextMenuItem {
|
||||
param([Parameter(Mandatory)][int]$MenuHwnd, [Parameter(Mandatory)][string]$ItemName)
|
||||
$m = (winapp ui search $ItemName -w $MenuHwnd --json 2>$null | ConvertFrom-Json).matches |
|
||||
Where-Object { $_.type -eq 'MenuItem' } | Select-Object -First 1
|
||||
if (-not $m) { return $false } # caller can treat $false as "entry absent" (e.g. module disabled)
|
||||
winapp ui invoke $m.selector -w $MenuHwnd 2>$null | Out-Null
|
||||
return $true
|
||||
}
|
||||
|
||||
# Lists all context-menu item names - works on ANY menu HWND: the modern Win11 menu (from
|
||||
# Open-PtBackgroundContextMenu / Open-PtExplorerContextMenu) OR the legacy #32768 menu (from
|
||||
# Open-PtShowMoreOptionsMenu). Use for present/absent assertions and to discover a module's caption.
|
||||
function Get-PtContextMenuItems {
|
||||
param([Parameter(Mandatory)][int]$MenuHwnd)
|
||||
winapp ui inspect -w $MenuHwnd --depth 8 2>$null | Out-String |
|
||||
Select-String 'MenuItem "([^"]+)"' -AllMatches | ForEach-Object { $_.Matches } | ForEach-Object { $_.Groups[1].Value }
|
||||
}
|
||||
|
||||
# Open an Explorer window on $Path and return its CabinetWClass HWND (int) - the handle the context-menu
|
||||
# openers need. Polls until the window appears (or throws). Use at the start of a synthetic-menu flow.
|
||||
# Detect whether the Explorer preview (reading) pane is currently shown. Heuristic: when the preview
|
||||
# pane is open it occupies the right portion of the window, so the "Shell Folder View" list pane's
|
||||
# right edge falls well short of the window's right edge (>200px gap). Returns $true/$false; best-effort
|
||||
# $false on any error. Uses UIA only (no foreground / keystrokes needed).
|
||||
function Get-PtPreviewPaneOn {
|
||||
param([Parameter(Mandatory)][int]$ExplorerHwnd)
|
||||
try {
|
||||
$win = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.hwnd -eq $ExplorerHwnd } | Select-Object -First 1
|
||||
if (-not $win) { return $false }
|
||||
$line = (winapp ui inspect -w $ExplorerHwnd --depth 10 2>$null | Out-String) -split "`r?`n" |
|
||||
Where-Object { $_ -match 'Shell Folder View' } | Select-Object -First 1
|
||||
if ($line -match '\((\d+),(\d+)\s+(\d+)x(\d+)\)') {
|
||||
$right = [int]$matches[1] + [int]$matches[3]
|
||||
return (($win.width - $right) -gt 200)
|
||||
}
|
||||
} catch { }
|
||||
return $false
|
||||
}
|
||||
|
||||
# Turn the Explorer preview (reading) pane OFF if it's on. An open preview pane is the top cause of the
|
||||
# coordinate right-click FALLBACK (in Open-PtExplorerContextMenu, used when Shift+F10 doesn't produce a
|
||||
# menu) landing in the preview surface -> the folder-BACKGROUND menu, which the honesty guard rejects
|
||||
# (looks like "got background/none"). Toggles via the command-bar "PreviewPaneToggleButton" using UIA
|
||||
# InvokePattern (no foreground needed, unlike the Alt+P keystroke). Best-effort: never throws, so it
|
||||
# can't break the menu flow. Returns $true if the pane ends up OFF. Note: on a very narrow window the
|
||||
# button can collapse into the "..." More menu and the invoke may miss - the width heuristic then simply
|
||||
# reports the unchanged state and the caller proceeds (Shift+F10 is unaffected either way).
|
||||
function Disable-PtPreviewPane {
|
||||
param([Parameter(Mandatory)][int]$ExplorerHwnd)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
if (-not (Get-PtPreviewPaneOn -ExplorerHwnd $ExplorerHwnd)) { return $true }
|
||||
winapp ui invoke PreviewPaneToggleButton -w $ExplorerHwnd 2>$null | Out-Null
|
||||
Start-Sleep -Milliseconds 900
|
||||
}
|
||||
return (-not (Get-PtPreviewPaneOn -ExplorerHwnd $ExplorerHwnd))
|
||||
}
|
||||
|
||||
function Open-PtExplorerWindow {
|
||||
param([Parameter(Mandatory)][string]$Path, [int]$TimeoutSec = 10)
|
||||
if (-not (Test-Path $Path)) { throw "Path not found: $Path" }
|
||||
Start-Process explorer.exe $Path
|
||||
$leaf = Split-Path $Path -Leaf
|
||||
$h = $null; $deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
Start-Sleep -Milliseconds 800
|
||||
$h = (winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.className -eq 'CabinetWClass' -and $_.title -match [regex]::Escape($leaf) } |
|
||||
Select-Object -First 1).hwnd
|
||||
} while (-not $h -and (Get-Date) -lt $deadline)
|
||||
if (-not $h) { throw "Explorer window for '$Path' did not appear within $TimeoutSec s." }
|
||||
# Turn off the preview pane so the coordinate right-click fallback in Open-PtExplorerContextMenu
|
||||
# can't land in the preview surface (-> folder-background menu). Best-effort; ignore failures.
|
||||
try { Disable-PtPreviewPane -ExplorerHwnd ([int]$h) | Out-Null } catch { }
|
||||
[int]$h
|
||||
}
|
||||
|
||||
# From an already-open MODERN menu (HWND from Open-PtBackgroundContextMenu / Open-PtExplorerContextMenu),
|
||||
# expand "Show more options" and return the legacy classic #32768 menu's HWND (int). The #32768 window is
|
||||
# NOT enumerable via `winapp ui list-windows`, so its handle is resolved with Win32 FindWindow; the returned
|
||||
# HWND is a normal menu HWND - read it with Get-PtContextMenuItems / act with Invoke-PtContextMenuItem, exactly
|
||||
# like the modern menu. See explorer-context-menu-flow.md for the writeup.
|
||||
function Open-PtShowMoreOptionsMenu {
|
||||
param([Parameter(Mandatory)][int]$MenuHwnd, [int]$TimeoutSec = 6)
|
||||
if (-not (Invoke-PtContextMenuItem -MenuHwnd $MenuHwnd -ItemName 'Show more options')) {
|
||||
throw "No 'Show more options' entry on the modern menu (HWND $MenuHwnd) - is this a Win11 packaged menu?"
|
||||
}
|
||||
$classic = [IntPtr]::Zero; $deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do { Start-Sleep -Milliseconds 500; $classic = [PtCtx]::FindWindow('#32768', $null) }
|
||||
while ($classic -eq [IntPtr]::Zero -and (Get-Date) -lt $deadline)
|
||||
if ($classic -eq [IntPtr]::Zero) { throw 'Classic #32768 menu did not appear after invoking "Show more options".' }
|
||||
[int64]$classic
|
||||
}
|
||||
|
||||
# From an already-open MODERN menu (HWND from Open-PtBackgroundContextMenu / Open-PtExplorerContextMenu),
|
||||
# expand a submenu-parent item (e.g. New+'s "New+") and return the child submenu's HWND (int). This is the
|
||||
# MODERN-menu sibling of Open-PtShowMoreOptionsMenu: same "invoke a parent item -> wait -> return the child
|
||||
# menu's HWND" shape, but the child is a WinUI PopupWindowSiteBridge popup (NOT the legacy #32768 window),
|
||||
# so it's resolved by enumerating windows rather than FindWindow('#32768'). It reuses Invoke-PtContextMenuItem
|
||||
# for the click and Get-PtContextMenuItems to disambiguate the popup; the returned HWND is a normal menu HWND
|
||||
# you read/act on with Get-PtContextMenuItems / Invoke-PtContextMenuItem, exactly like the parent menu.
|
||||
# -MenuHwnd the parent modern menu's HWND
|
||||
# -ItemName the submenu-parent MenuItem to invoke (e.g. 'New+')
|
||||
# -ContainsItem an item name known to live in the CHILD submenu (e.g. New+'s always-present 'Open templates'),
|
||||
# used to pick the right popup when several PopupWindowSiteBridge windows are open (parent + child).
|
||||
# Throws if the parent item is absent, or if no child popup containing -ContainsItem appears within TimeoutSec.
|
||||
function Expand-PtModernSubmenu {
|
||||
param(
|
||||
[Parameter(Mandatory)][int]$MenuHwnd,
|
||||
[Parameter(Mandatory)][string]$ItemName,
|
||||
[string]$ContainsItem,
|
||||
[int]$TimeoutSec = 6
|
||||
)
|
||||
# Record the popups already open so we can distinguish the NEW child popup from the parent menu.
|
||||
$before = @(winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.className -match 'PopupWindowSiteBridge' } | ForEach-Object { [int]$_.hwnd })
|
||||
# Poll for the parent item before invoking: Win11 menus populate asynchronously and 3rd-party
|
||||
# packaged entries (New+, PowerRename, ...) land a beat AFTER the base verbs, so a too-early invoke
|
||||
# on a freshly-opened menu can miss the item (a transient race, esp. right after enabling the module).
|
||||
$invoked = $false; $itemDeadline = (Get-Date).AddSeconds([Math]::Max(2, [Math]::Floor($TimeoutSec / 2)))
|
||||
do {
|
||||
if (Invoke-PtContextMenuItem -MenuHwnd $MenuHwnd -ItemName $ItemName) { $invoked = $true; break }
|
||||
Start-Sleep -Milliseconds 400
|
||||
} while ((Get-Date) -lt $itemDeadline)
|
||||
if (-not $invoked) {
|
||||
throw "No '$ItemName' entry on the modern menu (HWND $MenuHwnd) to expand (still absent after waiting for late-populating packaged entries)."
|
||||
}
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
Start-Sleep -Milliseconds 300
|
||||
$popups = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.className -match 'PopupWindowSiteBridge' }
|
||||
# Prefer a popup that actually contains the known child item (unambiguous). Otherwise, take a
|
||||
# newly-appeared popup (one not open before the invoke) as the child submenu.
|
||||
if ($ContainsItem) {
|
||||
foreach ($p in $popups) { if ((Get-PtContextMenuItems -MenuHwnd $p.hwnd) -contains $ContainsItem) { return [int]$p.hwnd } }
|
||||
} else {
|
||||
$new = $popups | Where-Object { [int]$_.hwnd -notin $before } | Sort-Object height -Descending | Select-Object -First 1
|
||||
if ($new) { return [int]$new.hwnd }
|
||||
}
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
throw "Submenu for '$ItemName' did not appear within $TimeoutSec s (looked for a PopupWindowSiteBridge$(if($ContainsItem){" containing '$ContainsItem'"}))."
|
||||
}
|
||||
|
||||
# Open the EXTENDED (CMF_EXTENDEDVERBS) legacy #32768 menu for a file via a GENUINE Shift+right-click, and
|
||||
# return its HWND (int). Use this - NOT Open-PtShowMoreOptionsMenu - whenever the entry under test is registered
|
||||
# "extended-verbs only" (e.g. PowerRename "Extended context menu only"): the non-Shift "Show more options" path
|
||||
# does NOT pass CMF_EXTENDEDVERBS, so such entries are (correctly) absent there and only appear on Shift+right-click
|
||||
# (src PowerRenameExt.cpp:84 returns E_FAIL unless CMF_EXTENDEDVERBS). Mechanics: COM-select the file (verifiable),
|
||||
# hold VK_SHIFT around winapp's element-resolved right-click, release Shift, then FindWindow('#32768'). Retries
|
||||
# because the first attempt right after Explorer opens can miss (foreground not settled). Read/act on the returned
|
||||
# HWND with Get-PtContextMenuItems / Invoke-PtContextMenuItem like any other menu.
|
||||
function Open-PtShiftRightClickMenu {
|
||||
param([Parameter(Mandatory)][int]$ExplorerHwnd, [Parameter(Mandatory)][string]$FileName, [int]$MaxTries = 3)
|
||||
if (-not (Test-PtDesktopInteractive)) { throw 'BLK-ENV: desktop is locked / no foreground (GetForegroundWindow()=0). Unlock and retry.' }
|
||||
for ($try = 1; $try -le $MaxTries; $try++) {
|
||||
[PtCtx]::ForceForeground([IntPtr]$ExplorerHwnd); Start-Sleep -Milliseconds 500
|
||||
# COM-select the target first (deterministic, verifiable, scrolls it into view; never opens/renames it).
|
||||
try {
|
||||
$sh = New-Object -ComObject Shell.Application
|
||||
$w = $sh.Windows() | Where-Object { $_.HWND -eq $ExplorerHwnd } | Select-Object -First 1
|
||||
if ($w -and $w.Document) {
|
||||
$it = $w.Document.Folder.Items() | Where-Object { $_.Name -eq $FileName -or $_.Name -like "$FileName*" } | Select-Object -First 1
|
||||
if ($it) { $w.Document.SelectItem($it, 0x1D); Start-Sleep -Milliseconds 300 }
|
||||
}
|
||||
} catch { }
|
||||
$item = (winapp ui search $FileName -w $ExplorerHwnd --json 2>$null | ConvertFrom-Json).matches |
|
||||
Where-Object { $_.type -eq 'ListItem' } | Select-Object -First 1
|
||||
if (-not $item) { Start-Sleep -Milliseconds 500; continue } # transient (mid-populate) - retry
|
||||
# Hold Shift around winapp's element-resolved right-click (Shift => CMF_EXTENDEDVERBS). Always release Shift.
|
||||
try {
|
||||
[PtCtx]::keybd_event(0x10, 0, 0, [IntPtr]::Zero); Start-Sleep -Milliseconds 60 # SHIFT down
|
||||
winapp ui click --right $item.selector -w $ExplorerHwnd 2>$null | Out-Null
|
||||
Start-Sleep -Milliseconds 80
|
||||
} finally { [PtCtx]::keybd_event(0x10, 0, 0x2, [IntPtr]::Zero) } # SHIFT up
|
||||
Start-Sleep -Milliseconds 800
|
||||
# Shift+right-click on Win11 opens the classic #32768 DIRECTLY (bypasses the modern PopupWindowSiteBridge).
|
||||
$classic = [int64][PtCtx]::FindWindow('#32768', $null)
|
||||
if ($classic -ne 0) { return [int]$classic }
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
throw "Could not open the extended (Shift+right-click) #32768 menu for '$FileName' after $MaxTries attempts."
|
||||
}
|
||||
|
||||
# Enable/disable a whole PowerToys MODULE through the real Settings-UI toggle switch - the faithful
|
||||
# user flow for "check enable/disable of the module works" items, and the sanctioned way to do it.
|
||||
# The UI toggle exercises the Settings->runner IPC enable/disable path and takes effect on the live
|
||||
# context menu WITHOUT a runner restart; for New+ it also runs the enable-time CopyTemplateExamples
|
||||
# that seeds templates. If the desktop is locked so this can't be driven, mark the item BLK-ENV.
|
||||
# Verified for PowerRename / File Locksmith / Image Resizer / New+ (2026-07-01). Always restore the
|
||||
# original state in a finally{} (call again with the old value).
|
||||
#
|
||||
# $r = Set-PtModuleEnabledViaSettingsUI -PageTag PowerRename -Enabled $false -EnabledKey PowerRename
|
||||
# # ...assert the context-menu entry is gone...
|
||||
# Set-PtModuleEnabledViaSettingsUI -PageTag PowerRename -Enabled $true -EnabledKey PowerRename # restore
|
||||
#
|
||||
# -PageTag : the `--open-settings=<tag>` moniker / page id. Known: PowerRename, FileLocksmith,
|
||||
# ImageResizer, NewPlus (no spaces).
|
||||
# -Enabled : desired state ($true = on, $false = off). No-ops if already in that state.
|
||||
# -EnabledKey : (optional) master settings.json `enabled.<key>` to cross-check. Note the key uses the
|
||||
# DISPLAY name with spaces: 'PowerRename','File Locksmith','Image Resizer','NewPlus'.
|
||||
# Returns: [pscustomobject] @{ SettingsHwnd; Selector; State('on'|'off'); EnabledFlag; Matched }.
|
||||
# NOTE: leaves the Settings window OPEN (caller closes it, e.g. `winapp ui invoke btn-close-<id> -w <hwnd>`).
|
||||
function Set-PtModuleEnabledViaSettingsUI {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$PageTag,
|
||||
[Parameter(Mandatory)][bool]$Enabled,
|
||||
[string]$EnabledKey,
|
||||
[int]$TimeoutSec = 20
|
||||
)
|
||||
if (-not (Test-PtDesktopInteractive)) { throw 'BLK-ENV: desktop locked / no foreground; cannot drive the Settings UI toggle.' }
|
||||
|
||||
# 1) Open Settings straight on the module page (the runner honours --open-settings=<tag>; single-instance,
|
||||
# so if Settings is already open it just navigates+focuses that window).
|
||||
Start-Process "$env:LOCALAPPDATA\PowerToys\PowerToys.exe" -ArgumentList "--open-settings=$PageTag" -EA SilentlyContinue
|
||||
$h = $null; $deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
Start-Sleep 1
|
||||
$w = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.title -match 'PowerToys Settings' } | Select-Object -First 1
|
||||
if ($w) { $h = [int]$w.hwnd }
|
||||
} while (-not $h -and (Get-Date) -lt $deadline)
|
||||
if (-not $h) { throw "Could not find the PowerToys Settings window after --open-settings=$PageTag." }
|
||||
Start-Sleep 2 # let the page render
|
||||
|
||||
# 2) The module's master enable toggle is ALWAYS the top-most ToggleSwitch on its page, i.e. the FIRST
|
||||
# element whose UIA state renders as [on]/[off]. AutomationIds carry a per-session suffix
|
||||
# (btn-powerrename-XXXX), so discover by state, never by a hard-coded id.
|
||||
$sel = $null; $state = $null; $deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
$dump = (winapp ui inspect -w $h --depth 12 2>$null | Out-String) -split "`r?`n"
|
||||
$line = $dump | Where-Object { $_ -match '\[(on|off)\]' } | Select-Object -First 1
|
||||
if ($line -and $line -match '^\s*(\S+)\b.*\[(on|off)\]') { $sel = $matches[1]; $state = $matches[2]; break }
|
||||
Start-Sleep 1
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
if (-not $sel) { throw "Could not locate the enable ToggleSwitch on the '$PageTag' Settings page." }
|
||||
|
||||
# 3) Flip only if needed
|
||||
$want = if ($Enabled) { 'on' } else { 'off' }
|
||||
if ($state -ne $want) {
|
||||
winapp ui invoke $sel -w $h 2>$null | Out-Null
|
||||
Start-Sleep 3
|
||||
$dump = (winapp ui inspect -w $h --depth 12 2>$null | Out-String) -split "`r?`n"
|
||||
$line = $dump | Where-Object { $_ -match [regex]::Escape($sel) -and $_ -match '\[(on|off)\]' } | Select-Object -First 1
|
||||
if ($line -and $line -match '\[(on|off)\]') { $state = $matches[1] }
|
||||
}
|
||||
|
||||
# 4) Optional cross-check against the master settings.json enabled flag (the UI writes it for you)
|
||||
$flag = $null
|
||||
if ($EnabledKey) {
|
||||
Start-Sleep 1
|
||||
try { $flag = (Get-Content "$env:LOCALAPPDATA\Microsoft\PowerToys\settings.json" -Raw | ConvertFrom-Json).enabled.$EnabledKey } catch {}
|
||||
}
|
||||
[pscustomobject]@{ SettingsHwnd = $h; Selector = $sel; State = $state; EnabledFlag = $flag; Matched = ($state -eq $want) }
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
# scripts/pt-foreground-guard.ps1
|
||||
# Verify and force a window to foreground BEFORE sending SendInput.
|
||||
# Without this guard, SendInput keys silently leak to the caller's terminal when
|
||||
# the target window has lost foreground (common with CmdPal AppX where Windows
|
||||
# foreground-lock blocks SetForegroundWindow after the first attempt).
|
||||
#
|
||||
# Use winapp ui set-value for UIA-friendly inputs (no foreground required).
|
||||
# Use this guard ONLY when you need real keystrokes (e.g. CmdPal alias detection).
|
||||
|
||||
if (-not ('PtFg' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class PtFg {
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
||||
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
||||
[DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int pid);
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
function Test-PtForeground {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether the target AppX is currently foreground by parsing winapp ui list-windows output
|
||||
for the literal substring 'foreground'.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
|
||||
return ($r -match 'foreground')
|
||||
}
|
||||
|
||||
function Get-PtHwnd {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the first HWND for the given AppX/exe. Returns [IntPtr]::Zero if none.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
|
||||
if ($r -match 'HWND (\d+):') { return [IntPtr][int64]$matches[1] }
|
||||
return [IntPtr]::Zero
|
||||
}
|
||||
|
||||
function Force-PtForeground {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Force the target AppX window to foreground using the AttachThreadInput + AllowSetForegroundWindow
|
||||
trick. Returns $true if window is foreground after this attempt; $false otherwise.
|
||||
.NOTES
|
||||
Windows foreground-lock will block subsequent SetForegroundWindow calls in the same session if
|
||||
a real interactive event hasn't fired recently. If this returns $false repeatedly, the only
|
||||
reliable recovery is to recycle the AppX (kill + relaunch via shell:AppsFolder URI).
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
$h = Get-PtHwnd -AppId $AppId
|
||||
if ($h -eq [IntPtr]::Zero) { return $false }
|
||||
|
||||
# Permission grant
|
||||
$proc = Get-Process | Where-Object { $_.MainWindowHandle -eq $h } | Select-Object -First 1
|
||||
if ($proc) { [PtFg]::AllowSetForegroundWindow($proc.Id) | Out-Null }
|
||||
|
||||
[PtFg]::ShowWindow($h, 9) | Out-Null # SW_RESTORE
|
||||
Start-Sleep -Milliseconds 150
|
||||
|
||||
# AttachThreadInput trick
|
||||
$fg = [PtFg]::GetForegroundWindow()
|
||||
$fgPid = 0
|
||||
$fgThread = [PtFg]::GetWindowThreadProcessId($fg, [ref]$fgPid)
|
||||
$curThread = [PtFg]::GetCurrentThreadId()
|
||||
if ($fgThread -ne 0 -and $fgThread -ne $curThread) {
|
||||
[PtFg]::AttachThreadInput($curThread, $fgThread, $true) | Out-Null
|
||||
}
|
||||
[PtFg]::BringWindowToTop($h) | Out-Null
|
||||
[PtFg]::SetForegroundWindow($h) | Out-Null
|
||||
[PtFg]::ShowWindow($h, 5) | Out-Null # SW_SHOW
|
||||
if ($fgThread -ne 0 -and $fgThread -ne $curThread) {
|
||||
[PtFg]::AttachThreadInput($curThread, $fgThread, $false) | Out-Null
|
||||
}
|
||||
Start-Sleep -Milliseconds 400
|
||||
return (Test-PtForeground -AppId $AppId)
|
||||
}
|
||||
|
||||
function Assert-PtForegroundOrAbort {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Guard helper. Throws if the target AppX is NOT foreground. Use this immediately before any
|
||||
SendInput call to ensure keys don't leak to the wrong window.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
if (-not (Test-PtForeground -AppId $AppId)) {
|
||||
if (-not (Force-PtForeground -AppId $AppId)) {
|
||||
throw "ABORT: $AppId is not foreground and cannot be forced foreground. SendInput would leak to wrong window."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
# pt-nonelevated.ps1 - launch a process at MEDIUM integrity (non-elevated) from an
|
||||
# already-elevated agent shell. Needed for tests that assert elevation-dependent
|
||||
# visibility (e.g. File Locksmith L649/L650: a non-elevated module must NOT see
|
||||
# higher-integrity processes; an elevated one must).
|
||||
#
|
||||
# The drive-stack in SKILL.md only covers gaining MORE privilege. De-elevation is the
|
||||
# opposite problem: from a High-IL shell you cannot simply CreateProcess a Medium-IL
|
||||
# child. The robust, dependency-free way is a one-shot Scheduled Task registered with
|
||||
# RunLevel=Limited + LogonType=Interactive, which lands on the logged-on user's desktop
|
||||
# at their filtered (medium) token. (The classic explorer-shell-injection trick is more
|
||||
# fragile across sessions.)
|
||||
#
|
||||
# Functions:
|
||||
# Start-PtNonElevated -Exe <path> [-Arguments <str>] -> launches a GUI/console exe non-elevated, returns the spawned PID(s)
|
||||
# Invoke-PtNonElevatedCapture -Exe <path> -Arguments <str> -OutFile <path> -> runs a console exe non-elevated, redirects stdout/err to a file, waits, returns the file path
|
||||
#
|
||||
# Verify elevation of the result with Test-ProcessElevated (scripts/pt-admin-probe.ps1).
|
||||
|
||||
function Start-PtNonElevated {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Exe,
|
||||
[string]$Arguments = '',
|
||||
[int]$WaitSeconds = 5,
|
||||
[string]$MatchProcessName # optional: base name to enumerate spawned PIDs (e.g. 'PowerToys.FileLocksmithUI')
|
||||
)
|
||||
if (-not (Test-Path $Exe)) { throw "Exe not found: $Exe" }
|
||||
$taskName = "PtNonElev_$([guid]::NewGuid().ToString('N').Substring(0,8))"
|
||||
$before = @()
|
||||
if ($MatchProcessName) { $before = @(Get-Process -Name $MatchProcessName -EA SilentlyContinue | Select-Object -Expand Id) }
|
||||
try {
|
||||
$action = New-ScheduledTaskAction -Execute $Exe -Argument $Arguments
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -RunLevel Limited -LogonType Interactive
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Start-Sleep -Seconds $WaitSeconds
|
||||
if ($MatchProcessName) {
|
||||
$after = @(Get-Process -Name $MatchProcessName -EA SilentlyContinue | Select-Object -Expand Id)
|
||||
return ($after | Where-Object { $_ -notin $before })
|
||||
}
|
||||
return $null
|
||||
}
|
||||
finally {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -EA SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PtNonElevatedCapture {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Exe,
|
||||
[string]$Arguments = '',
|
||||
[Parameter(Mandatory)][string]$OutFile,
|
||||
[int]$TimeoutSeconds = 30
|
||||
)
|
||||
if (-not (Test-Path $Exe)) { throw "Exe not found: $Exe" }
|
||||
Remove-Item $OutFile -EA SilentlyContinue
|
||||
$wrap = [IO.Path]::ChangeExtension($OutFile, '.cmd')
|
||||
"`"$Exe`" $Arguments > `"$OutFile`" 2>&1" | Set-Content -Encoding ascii $wrap
|
||||
$taskName = "PtNonElev_$([guid]::NewGuid().ToString('N').Substring(0,8))"
|
||||
try {
|
||||
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/c `"$wrap`""
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -RunLevel Limited -LogonType Interactive
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
do { Start-Sleep 1; $info = Get-ScheduledTaskInfo -TaskName $taskName }
|
||||
while ($info.LastTaskResult -eq 267009 -and (Get-Date) -lt $deadline) # 267009 = task still running
|
||||
Start-Sleep 1
|
||||
return $OutFile
|
||||
}
|
||||
finally {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -EA SilentlyContinue
|
||||
Remove-Item $wrap -EA SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
# scripts/pt-sendinput-chord.ps1
|
||||
# Inject a global hotkey chord (e.g. Win+Shift+/) into the system input stream.
|
||||
# Critical: INPUT struct MUST be cb=40 on x64 (with padding for the MOUSEINPUT union member).
|
||||
# The common bug "Win+ hotkeys can't be injected" is a marshaling error producing 32-byte struct
|
||||
# and SendInput returns 0 with GetLastError()==87 (ERROR_INVALID_PARAMETER).
|
||||
#
|
||||
# This SHOULD be a last resort. Prefer Named Events (Invoke-PtSharedEvent) when the module exposes one.
|
||||
# Use this only for: (a) explicit hotkey-trigger verification tests, (b) modules without Named Events,
|
||||
# (c) UI keystrokes inside an already-foreground window (use Send-KeyToHwnd via PostMessage instead
|
||||
# for elevated -> non-elevated AppX, see references/winapp-ui-testing.md).
|
||||
|
||||
if (-not ('PtChord' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Collections.Generic;
|
||||
public static class PtChord {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct INPUT { public uint type; public KEYBDINPUT ki; public int pad1; public int pad2; } // pad to 40 bytes
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
||||
[DllImport("user32.dll", SetLastError=true)]
|
||||
static extern uint SendInput(uint n, INPUT[] p, int cb);
|
||||
const uint KEYUP = 0x0002;
|
||||
static INPUT K(ushort vk, bool up) { INPUT i=new INPUT(); i.type=1; i.ki.wVk=vk; i.ki.dwFlags=up?KEYUP:0; return i; }
|
||||
public static uint Chord(ushort[] mods, ushort key) {
|
||||
var l=new List<INPUT>();
|
||||
foreach(var m in mods) l.Add(K(m,false));
|
||||
l.Add(K(key,false)); l.Add(K(key,true));
|
||||
for(int i=mods.Length-1;i>=0;i--) l.Add(K(mods[i],true));
|
||||
var a=l.ToArray();
|
||||
return SendInput((uint)a.Length, a, Marshal.SizeOf(typeof(INPUT)));
|
||||
}
|
||||
public static uint Tap(ushort key) { return Chord(new ushort[0], key); }
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
# Common VK codes for chord mods:
|
||||
# LWIN=0x5B RWIN=0x5C CTRL=0x11 SHIFT=0x10 ALT=0x12
|
||||
# Main key VKs:
|
||||
# 0x08 Backspace 0x09 Tab 0x0D Enter 0x1B Escape 0x20 Space
|
||||
# 0x25 Left 0x26 Up 0x27 Right 0x28 Down
|
||||
# 0x30..0x39 0..9 0x41..0x5A A..Z
|
||||
|
||||
function Send-PtChord {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Inject a hotkey chord. Returns number of inputs Windows accepted (0 = failed; check GetLastError).
|
||||
.EXAMPLE
|
||||
Send-PtChord -Mods 0x5B,0x10 -Key 0x43 # Win+Shift+C (Color Picker)
|
||||
Send-PtChord -Mods 0x5B,0x11 -Key 0x52 # Win+Ctrl+R (PowerOcr)
|
||||
Send-PtChord -Mods 0x5B,0xA4 -Key 0x20 # Win+Alt+Space (CmdPal default)
|
||||
Send-PtChord -Key 0x0D # plain Enter (no mods)
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[uint16[]]$Mods = @(),
|
||||
[Parameter(Mandatory)][uint16]$Key
|
||||
)
|
||||
$sent = [PtChord]::Chord($Mods, $Key)
|
||||
if ($sent -eq 0) {
|
||||
$err = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
||||
throw "SendInput failed (returned 0, GetLastError=$err). Likely caller is at lower integrity than PT runner, or chord is OS-reserved (Win+L, Win+Tab)."
|
||||
}
|
||||
return $sent
|
||||
}
|
||||
|
||||
function Wait-PtHotkeyAccepted {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
After Send-PtChord, verify the PT runner saw it by tailing its log for the centralized-hook line.
|
||||
Returns the matching log line (if any) within $TimeoutSec.
|
||||
.EXAMPLE
|
||||
Send-PtChord -Mods 0x5B,0x10 -Key 0x43
|
||||
$line = Wait-PtHotkeyAccepted -ModuleHint 'Color' -TimeoutSec 3
|
||||
if (-not $line) { throw "Runner did not log hotkey invocation" }
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param([string]$ModuleHint = '', [int]$TimeoutSec = 3)
|
||||
$log = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\PowerToys\RunnerLogs" -Filter 'runner-log_*.log' -EA SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $log) { return $null }
|
||||
$start = (Get-Date).AddSeconds(-2)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
$line = Get-Content $log.FullName -Tail 50 -EA SilentlyContinue |
|
||||
Where-Object { $_ -match 'hotkey is invoked from Centralized keyboard hook' -and ($ModuleHint -eq '' -or $_ -match $ModuleHint) } |
|
||||
Select-Object -Last 1
|
||||
if ($line) { return $line }
|
||||
Start-Sleep -Milliseconds 200
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
return $null
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
# pt-session-diagnose.ps1
|
||||
# Diagnose whether the current shell can drive interactive PowerToys tests.
|
||||
# Tells you in one go: am I on the active console session, can I see foreground windows,
|
||||
# and can I use Shell COM. If not, prints the exact psexec mitigation command.
|
||||
|
||||
# Guard Add-Type so this script can be dot-sourced / re-run in the same session without
|
||||
# throwing "type already exists" on the second invocation.
|
||||
if (-not ('WTS' -as [type])) {
|
||||
Add-Type 'using System; using System.Runtime.InteropServices;
|
||||
public class WTS { [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); }
|
||||
public class FG { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }'
|
||||
}
|
||||
|
||||
Write-Host "--- Logged-on users + sessions ---" -ForegroundColor Cyan
|
||||
quser 2>&1
|
||||
|
||||
Write-Host "`n--- This shell's session ---" -ForegroundColor Cyan
|
||||
$me = [Diagnostics.Process]::GetCurrentProcess()
|
||||
" PID: $($me.Id)"
|
||||
" Session: $($me.SessionId)"
|
||||
|
||||
Write-Host "`n--- Console Explorer session(s) ---" -ForegroundColor Cyan
|
||||
$exps = Get-Process explorer -ErrorAction SilentlyContinue
|
||||
if ($exps) {
|
||||
$exps | Select-Object Id, SessionId, @{N='StartTime';E={$_.StartTime}} | Format-Table -AutoSize
|
||||
} else {
|
||||
Write-Host " (no explorer.exe running)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "`n--- Windows active console + foreground + Shell COM ---" -ForegroundColor Cyan
|
||||
$activeConsole = [WTS]::WTSGetActiveConsoleSessionId()
|
||||
$fg = [FG]::GetForegroundWindow()
|
||||
$shellOk = $false
|
||||
try { @((New-Object -ComObject Shell.Application).Windows()).Count | Out-Null; $shellOk = $true } catch {}
|
||||
" WTSGetActiveConsoleSessionId() = $activeConsole"
|
||||
" GetForegroundWindow() = $fg"
|
||||
" Shell.Application available = $shellOk"
|
||||
|
||||
Write-Host "`n--- Verdict ---" -ForegroundColor Cyan
|
||||
$consoleSession = ($exps | Select-Object -First 1).SessionId
|
||||
if ($me.SessionId -eq $consoleSession -and $fg -ne 0 -and $shellOk) {
|
||||
Write-Host " PASS - this shell can drive interactive PowerToys tests." -ForegroundColor Green
|
||||
} elseif ($me.SessionId -eq $consoleSession -and $fg -eq 0) {
|
||||
Write-Host " WARN - same session as explorer but no foreground (workstation locked?). Unlock and re-run." -ForegroundColor Yellow
|
||||
} elseif (-not $shellOk) {
|
||||
Write-Host " FAIL - Shell COM unavailable (likely Session 0 / service context). Very few tests possible." -ForegroundColor Red
|
||||
} else {
|
||||
Write-Host " FAIL - shell in Session $($me.SessionId), console explorer in Session $consoleSession. Input injection denied." -ForegroundColor Red
|
||||
Write-Host " Mitigation: relaunch in the console session with:" -ForegroundColor Yellow
|
||||
Write-Host " psexec -accepteula -h -i $consoleSession -s pwsh.exe" -ForegroundColor Yellow
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
# scripts/pt-shared-events.ps1
|
||||
# Signal PowerToys modules via Win32 named events.
|
||||
# Catalog source: PowerToys repo src/common/interop/shared_constants.h
|
||||
# (Friendly-name mapping was originally surfaced by community frameworks; the values themselves
|
||||
# are stable PT public IPC names. This file is self-contained - no external repo required.)
|
||||
# Reason: instead of pressing a hotkey (which is racey, foreground-sensitive, and UIPI-fragile),
|
||||
# directly SetEvent on the kernel event the module is waiting on. Same code path as the hotkey.
|
||||
|
||||
if (-not ('PtEv' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class PtEv {
|
||||
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
||||
private static extern IntPtr OpenEventW(uint dwAccess, bool bInherit, string lpName);
|
||||
[DllImport("kernel32.dll", SetLastError=true)]
|
||||
private static extern bool SetEvent(IntPtr h);
|
||||
[DllImport("kernel32.dll", SetLastError=true)]
|
||||
private static extern bool CloseHandle(IntPtr h);
|
||||
private const uint EVENT_MODIFY_STATE = 0x0002;
|
||||
private const uint SYNCHRONIZE = 0x00100000;
|
||||
|
||||
public static bool Signal(string fullName) {
|
||||
IntPtr h = OpenEventW(EVENT_MODIFY_STATE | SYNCHRONIZE, false, fullName);
|
||||
if (h == IntPtr.Zero) {
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
throw new System.ComponentModel.Win32Exception(err,
|
||||
"OpenEvent failed for '" + fullName + "' (err=" + err + "). Owning module process may not be running.");
|
||||
}
|
||||
try { return SetEvent(h); } finally { CloseHandle(h); }
|
||||
}
|
||||
|
||||
public static bool Exists(string fullName) {
|
||||
IntPtr h = OpenEventW(SYNCHRONIZE, false, fullName);
|
||||
if (h == IntPtr.Zero) return false;
|
||||
CloseHandle(h); return true;
|
||||
}
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
# Friendly-name -> full event name map (per Local\ namespace).
|
||||
# Source: <PT-repo>\src\common\interop\shared_constants.h
|
||||
$script:PtSharedEvents = @{
|
||||
# -- Hotkey-activated module triggers --
|
||||
'AOT.Pin' = 'Local\AlwaysOnTopPinEvent-892e0aa2-cfa8-4cc4-b196-ddeb32314ce8'
|
||||
'AOT.IncreaseOpacity' = 'Local\AlwaysOnTopIncreaseOpacityEvent-a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
'AOT.DecreaseOpacity' = 'Local\AlwaysOnTopDecreaseOpacityEvent-b2c3d4e5-f6a7-8901-bcde-f12345678901'
|
||||
'AdvancedPaste.ShowUI' = 'Local\PowerToys_AdvancedPaste_ShowUI'
|
||||
'CmdPal.Show' = 'Local\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a'
|
||||
'ColorPicker.Show' = 'Local\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525'
|
||||
'CropAndLock.Reparent' = 'Local\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36'
|
||||
'CropAndLock.Thumbnail' = 'Local\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434'
|
||||
'CursorWrap.Trigger' = 'Local\CursorWrapTriggerEvent-1f8452b5-4e6e-45b3-8b09-13f14a5900c9'
|
||||
'EnvVars.Show' = 'Local\PowerToysEnvironmentVariables-ShowEnvironmentVariablesEvent-1021f616-e951-4d64-b231-a8f972159978'
|
||||
'EnvVars.ShowAdmin' = 'Local\PowerToysEnvironmentVariables-EnvironmentVariablesAdminEvent-8c95d2ad-047c-49a2-9e8b-b4656326cfb2'
|
||||
'FancyZones.ToggleEditor' = 'Local\FancyZones-ToggleEditorEvent-1e174338-06a3-472b-874d-073b21c62f14'
|
||||
'FindMyMouse.Trigger' = 'Local\FindMyMouseTriggerEvent-5a9dc5f4-1c74-4f2f-a66f-1b9b6a2f9b23'
|
||||
'Hosts.Show' = 'Local\Hosts-ShowHostsEvent-5a0c0aae-5ff5-40f5-95c2-20e37ed671f0'
|
||||
'Hosts.ShowAdmin' = 'Local\Hosts-ShowHostsAdminEvent-60ff44e2-efd3-43bf-928a-f4d269f98bec'
|
||||
'LightSwitch.Toggle' = 'Local\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a'
|
||||
'LightSwitch.Light' = 'Local\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca'
|
||||
'LightSwitch.Dark' = 'Local\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368'
|
||||
'MeasureTool.Trigger' = 'Local\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199'
|
||||
'MouseCrosshairs.Trigger' = 'Local\MouseCrosshairsTriggerEvent-0d4c7f92-0a5c-4f5c-b64b-8a2a2f7e0b21'
|
||||
'MouseHighlighter.Trigger' = 'Local\MouseHighlighterTriggerEvent-1e3c9c3d-3fdf-4f9a-9a52-31c9b3c3a8f4'
|
||||
'MouseJump.Show' = 'Local\MouseJumpEvent-aa0be051-3396-4976-b7ba-1a9cc7d236a5'
|
||||
'NewKeyboardManager.Open' = 'Local\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f'
|
||||
'Peek.Show' = 'Local\ShowPeekEvent'
|
||||
'PowerDisplay.Toggle' = 'Local\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c'
|
||||
'PowerLauncher.Invoke' = 'Local\PowerToysRunInvokeEvent-30f26ad7-d36d-4c0e-ab02-68bb5ff3c4ab'
|
||||
'PowerOcr.Show' = 'Local\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a'
|
||||
'RegistryPreview.Trigger' = 'Local\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687'
|
||||
'ShortcutGuide.Trigger' = 'Local\ShortcutGuide-TriggerEvent-d4275ad3-2531-4d19-9252-c0becbd9b496'
|
||||
'TextExtractor.Show' = 'Local\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a'
|
||||
'Workspaces.Hotkey' = 'Local\PowerToys-Workspaces-HotkeyEvent-2625C3C8-BAC9-4DB3-BCD6-3B4391A26FD0'
|
||||
'Workspaces.LaunchEditor' = 'Local\Workspaces-LaunchEditorEvent-a55ff427-cf62-4994-a2cd-9f72139296bf'
|
||||
'ZoomIt.Zoom' = 'Local\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393'
|
||||
'ZoomIt.Draw' = 'Local\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975'
|
||||
'ZoomIt.Break' = 'Local\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b'
|
||||
'ZoomIt.LiveZoom' = 'Local\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d'
|
||||
'ZoomIt.Snip' = 'Local\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30'
|
||||
'ZoomIt.SnipOcr' = 'Local\PowerToysZoomIt-SnipOcrEvent-a7c3b1d2-9e4f-4a6b-8d5c-1f2e3a4b5c6d'
|
||||
'ZoomIt.Record' = 'Local\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512'
|
||||
|
||||
# -- Termination triggers (clean shutdown without process kill) --
|
||||
'AOT.Terminate' = 'Local\AlwaysOnTopTerminateEvent-cfdf1eae-791f-4953-8021-2f18f3837eae'
|
||||
'Awake.Exit' = 'Local\PowerToysAwakeExitEvent-c0d5e305-35fc-4fb5-83ec-f6070cfaf7fe'
|
||||
'CmdPal.Exit' = 'Local\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd'
|
||||
'ColorPicker.Terminate' = 'Local\TerminateColorPickerEvent-3d676258-c4d5-424e-a87a-4be22020e813'
|
||||
'CropAndLock.Exit' = 'Local\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a'
|
||||
'FZE.Exit' = 'Local\PowerToys-FZE-ExitEvent-ca8c73de-a52c-4274-b691-46e9592d3b43'
|
||||
'Hosts.Terminate' = 'Local\Hosts-TerminateHostsEvent-d5410d5e-45a6-4d11-bbf0-a4ec2d064888'
|
||||
'KBM.Terminate' = 'Local\TerminateKBMSharedEvent-a787c967-55b6-47de-94d9-56f39fed839e'
|
||||
'MouseJump.Terminate' = 'Local\TerminateMouseJumpEvent-252fa337-317f-4c37-a61f-99464c3f9728'
|
||||
'Peek.Terminate' = 'Local\TerminatePeekEvent-267149fe-7ed2-427d-a3ad-9e18203c037c'
|
||||
'PowerAccent.Exit' = 'Local\PowerToysPowerAccentExitEvent-53e93389-d19a-4fbb-9b36-1981c8965e17'
|
||||
'PowerOcr.Terminate' = 'Local\TerminatePowerOCREvent-08e5de9d-15df-4ea8-8840-487c13435a67'
|
||||
'PowerDisplay.Terminate' = 'Local\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a'
|
||||
'Run.Exit' = 'Local\PowerToysRunExitEvent-3e38e49d-a762-4ef1-88f2-fd4bc7481516'
|
||||
'ShortcutGuide.Exit' = 'Local\ShortcutGuide-ExitEvent-35697cdd-a3d2-47d6-a246-34efcc73eac0'
|
||||
'Settings.Terminate' = 'Local\PowerToysRunnerTerminateSettingsEvent-c34cb661-2e69-4613-a1f8-4e39c25d7ef6'
|
||||
'ZoomIt.Exit' = 'Local\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220'
|
||||
'GrabAndMove.Exit' = 'Local\PowerToysGrabAndMove-ExitEvent-b8c4d2e3-5f6a-7b8c-9d0e-1f2a3b4c5d6e'
|
||||
}
|
||||
|
||||
function Invoke-PtSharedEvent {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Signal a PowerToys named kernel event by friendly name (e.g. 'CmdPal.Show')
|
||||
or by full event path (e.g. 'Local\PowerToys_AdvancedPaste_ShowUI').
|
||||
Returns $true on success; throws if event doesn't exist or owner not running.
|
||||
.EXAMPLE
|
||||
Invoke-PtSharedEvent -Name 'CmdPal.Show'
|
||||
Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke'
|
||||
Invoke-PtSharedEvent -Name 'AOT.Pin'
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory)][string]$Name)
|
||||
$eventName = if ($script:PtSharedEvents.ContainsKey($Name)) { $script:PtSharedEvents[$Name] } else { $Name }
|
||||
return [PtEv]::Signal($eventName)
|
||||
}
|
||||
|
||||
function Test-PtSharedEvent {
|
||||
[CmdletBinding()] param([Parameter(Mandatory)][string]$Name)
|
||||
$eventName = if ($script:PtSharedEvents.ContainsKey($Name)) { $script:PtSharedEvents[$Name] } else { $Name }
|
||||
return [PtEv]::Exists($eventName)
|
||||
}
|
||||
|
||||
function Get-PtSharedEventCatalog {
|
||||
$script:PtSharedEvents.GetEnumerator() | Sort-Object Name |
|
||||
ForEach-Object { [pscustomobject]@{ Name = $_.Key; Event = $_.Value } }
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
# scripts/pt-shell-verbs.ps1
|
||||
# Enumerate Windows classic shell verbs (HKCR-registered) via Shell.Application COM.
|
||||
#
|
||||
# SCOPE WARNING: this does NOT find PowerToys context-menu items on Win11. PT registers
|
||||
# PowerRename, Image Resizer, File Locksmith, New+ etc. via IExplorerCommand (Tier-1 modern
|
||||
# menu), which is invisible to Shell.Application.Verbs(). For PT-context-menu drives, use
|
||||
# `pt-explorer-contextmenu.ps1` (synthetic right-click + UIA invoke). See
|
||||
# `explorer-context-menu-flow.md` for the canonical pattern.
|
||||
#
|
||||
# Useful for: enumerating non-PT classic verbs (Open, Edit, Send-to, third-party shell extensions),
|
||||
# and as a negative check that PT verbs are NOT classic-shadowed.
|
||||
|
||||
function Get-PtShellVerbs {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Enumerate classic HKCR shell verbs on a file or folder. Returns Name + the underlying Verb COM object.
|
||||
.EXAMPLE
|
||||
Get-PtShellVerbs -Path 'D:\fixtures\image.png' | Format-Table Name
|
||||
#>
|
||||
[CmdletBinding()] param([Parameter(Mandatory)][string]$Path)
|
||||
if (-not (Test-Path $Path)) { throw "Path not found: $Path" }
|
||||
$abs = (Resolve-Path $Path).Path
|
||||
$folder = Split-Path -Parent $abs
|
||||
$leaf = Split-Path -Leaf $abs
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$ns = $shell.NameSpace($folder)
|
||||
if (-not $ns) { throw "Cannot open folder namespace: $folder" }
|
||||
$item = $ns.ParseName($leaf)
|
||||
if (-not $item) { throw "File not in folder: $leaf" }
|
||||
return @($item.Verbs()) | ForEach-Object {
|
||||
[pscustomobject]@{ Name = $_.Name; Verb = $_ }
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PtShellVerb {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Invoke a classic shell verb on a file by name-regex match. Returns $true on success.
|
||||
Does NOT work for PT Win11 modern-menu items - see SCOPE WARNING at top.
|
||||
.EXAMPLE
|
||||
Invoke-PtShellVerb -Path 'D:\fixtures\img.png' -NamePattern '^Edit$'
|
||||
#>
|
||||
[CmdletBinding()] param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$NamePattern
|
||||
)
|
||||
$verb = Get-PtShellVerbs -Path $Path | Where-Object { $_.Name -match $NamePattern } | Select-Object -First 1
|
||||
if (-not $verb) {
|
||||
Write-Warning "No classic shell verb matching '$NamePattern' on '$Path'. (Win11 PT modern-menu items are NOT visible here - use pt-explorer-contextmenu.ps1 instead.)"
|
||||
return $false
|
||||
}
|
||||
$verb.Verb.DoIt()
|
||||
return $true
|
||||
}
|
||||
|
||||
function Reset-PtShellComCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Release current Shell.Application COM instance + force a fresh one on next call.
|
||||
Use when you've installed/registered a shell handler mid-test and the cached verb list
|
||||
still reflects the old state.
|
||||
#>
|
||||
[System.Runtime.InteropServices.Marshal]::CleanupUnusedObjectsInCurrentContext()
|
||||
[System.GC]::Collect()
|
||||
[System.GC]::WaitForPendingFinalizers()
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
# scripts/pt-state.ps1
|
||||
# Common state-verification helpers: settings.json diff, runner log grep, GPO log check,
|
||||
# process spawn detection, AppX probe.
|
||||
|
||||
function Get-PtSettings {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read the master PT settings.json (enabled.<Module> flags + run_elevated + theme + language).
|
||||
#>
|
||||
$f = "$env:LOCALAPPDATA\Microsoft\PowerToys\settings.json"
|
||||
if (-not (Test-Path $f)) { return $null }
|
||||
Get-Content $f -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-PtModuleSettings {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read a single module's settings.json (e.g. AdvancedPaste, FancyZones, etc.).
|
||||
These ARE auto-reloaded by the per-module file watcher (~3s debounce).
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ModuleDir)
|
||||
$f = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
|
||||
if (-not (Test-Path $f)) { return $null }
|
||||
Get-Content $f -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-CmdPalSettings {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read CmdPal AppX settings.json (sandboxed path). Contains 19 ProviderSettings, DockSettings,
|
||||
GalleryFeedUrl, EscapeKeyBehaviorSetting, AutoGoHomeInterval, Hotkey, Aliases, etc.
|
||||
#>
|
||||
$f = "$env:LOCALAPPDATA\Packages\Microsoft.CommandPalette_8wekyb3d8bbwe\LocalState\settings.json"
|
||||
if (-not (Test-Path $f)) { return $null }
|
||||
Get-Content $f -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-PtRunnerLogTail {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tail the latest runner-log_<date>.log file for matching lines.
|
||||
.EXAMPLE
|
||||
Get-PtRunnerLogTail -Pattern 'hotkey is invoked' -TailLines 100
|
||||
Get-PtRunnerLogTail -Pattern 'GPO sets' -TailLines 50
|
||||
#>
|
||||
param([string]$Pattern = '.*', [int]$TailLines = 50)
|
||||
$log = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\PowerToys\RunnerLogs" -Filter 'runner-log_*.log' -EA SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $log) { return @() }
|
||||
Get-Content $log.FullName -Tail $TailLines -EA SilentlyContinue | Where-Object { $_ -match $Pattern }
|
||||
}
|
||||
|
||||
function Test-PtModuleEnabled {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether a specific module is enabled in master settings.json.
|
||||
Note: PT Run uses the key "PowerToys Run" (with space).
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ModuleKey)
|
||||
$s = Get-PtSettings
|
||||
if (-not $s) { return $false }
|
||||
return [bool]$s.enabled.$ModuleKey
|
||||
}
|
||||
|
||||
function Test-PtModuleProcess {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the process(es) for a module exe name (e.g. 'PowerToys.AdvancedPaste').
|
||||
Returns empty array if not running.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ExeName)
|
||||
@(Get-Process $ExeName -EA SilentlyContinue)
|
||||
}
|
||||
|
||||
function Restart-PtRunner {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Kill the runner and relaunch to force fresh load of master settings.json.
|
||||
The runner does NOT auto-pickup edits to the top-level enabled.<Module> flags.
|
||||
#>
|
||||
$pt = Get-Process PowerToys -EA SilentlyContinue | Select-Object -First 1
|
||||
if ($pt) { Stop-Process -Id $pt.Id -Force; Start-Sleep -Milliseconds 800 }
|
||||
Start-Process "$env:LOCALAPPDATA\PowerToys\PowerToys.exe"
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
|
||||
function Backup-PtModuleSettings {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Snapshot a module's settings.json to TEMP for restore-on-exit. Returns the backup path.
|
||||
.EXAMPLE
|
||||
$bk = Backup-PtModuleSettings -ModuleDir AdvancedPaste
|
||||
try { ... mutate ... } finally { Restore-PtModuleSettings -ModuleDir AdvancedPaste -BackupPath $bk }
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ModuleDir)
|
||||
$src = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
|
||||
if (-not (Test-Path $src)) { return $null }
|
||||
$bk = Join-Path $env:TEMP ("ptbk-$ModuleDir-$(Get-Random -Maximum 9999).json")
|
||||
Copy-Item -Path $src -Destination $bk -Force
|
||||
return $bk
|
||||
}
|
||||
|
||||
function Restore-PtModuleSettings {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ModuleDir,
|
||||
[Parameter(Mandatory)][string]$BackupPath
|
||||
)
|
||||
$dst = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
|
||||
Copy-Item -Path $BackupPath -Destination $dst -Force
|
||||
Remove-Item $BackupPath -Force -EA SilentlyContinue
|
||||
}
|
||||
201
.github/skills/release-note-generation/LICENSE.txt
vendored
201
.github/skills/release-note-generation/LICENSE.txt
vendored
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
140
.github/skills/release-note-generation/SKILL.md
vendored
140
.github/skills/release-note-generation/SKILL.md
vendored
@@ -1,140 +0,0 @@
|
||||
---
|
||||
name: release-note-generation
|
||||
description: Toolkit for generating PowerToys release notes from GitHub milestone PRs or commit ranges. Use when asked to create release notes, summarize milestone PRs, generate changelog, prepare release documentation, generate PR review summaries locally for release notes, update README for a new release, manage PR milestones, collect PRs between commits/tags, or prepare release assets (download installers and compute installer hashes).
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Release Note Generation Skill
|
||||
|
||||
Generate professional release notes for PowerToys milestones by collecting merged PRs, summarizing each PR with the local CLI agent, grouping by label, and producing user-facing summaries.
|
||||
|
||||
## Output Directory
|
||||
|
||||
All generated artifacts are placed under `Generated Files/ReleaseNotes/` at the repository root (gitignored).
|
||||
|
||||
```
|
||||
Generated Files/ReleaseNotes/
|
||||
├── milestone_prs.json # Raw PR data from GitHub
|
||||
├── sorted_prs.csv # Sorted PR list with Copilot summaries
|
||||
├── prs_with_milestone.csv # Milestone assignment tracking
|
||||
├── grouped_csv/ # PRs grouped by label (one CSV per label)
|
||||
├── grouped_md/ # Generated markdown summaries per label
|
||||
└── v{VERSION}-release-notes.md # Final consolidated release notes
|
||||
```
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Generate release notes for a milestone
|
||||
- Summarize PRs merged in a release
|
||||
- Generate per-PR review summaries locally for release-notes copy
|
||||
- Assign milestones to PRs missing them
|
||||
- Collect PRs between two commits/tags
|
||||
- Update README.md for a new version
|
||||
- Prepare GitHub release assets (download installers/symbols + compute hashes)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **GitHub CLI (`gh`) installed and authenticated** — The collection script uses `gh pr view` and `gh api graphql` to fetch PR metadata and co-author information. Run `gh auth status` to verify; if not logged in, run `gh auth login` first. See [Step 1.0.0](./references/step1-collection.md) for details.
|
||||
- MCP Server: github-mcp-server installed (used to fetch PR diffs/files for the local-agent review step)
|
||||
- For [prepare-release-assets.ps1](./scripts/prepare-release-assets.ps1) only: **Azure CLI** authenticated against the Microsoft tenant (`az login`) with the `azure-devops` extension; access to the `microsoft/Dart` ADO project
|
||||
|
||||
## Required Variables
|
||||
|
||||
⚠️ **Before starting**, confirm `{{ReleaseVersion}}` with the user. If not provided, **ASK**: "What release version are we generating notes for? (e.g., 0.98)"
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{ReleaseVersion}}` | Target release version | `0.98` |
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
```
|
||||
┌────────────────────────────────┐
|
||||
│ 1.0 Verify gh auth + MemberList │
|
||||
└────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────┐
|
||||
│ 1.1 Collect PRs (stable range) │
|
||||
└────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────┐
|
||||
│ 1.2 Assign Milestones │
|
||||
└────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────┐
|
||||
│ 2.1–2.4 Label PRs (auto+human) │
|
||||
└────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────┐
|
||||
│ 3.1 Local-agent PR summaries │
|
||||
│ (writes CopilotSummary) │
|
||||
└────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────┐
|
||||
│ 3.2 (Optional) Refresh PR data │
|
||||
└────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────┐
|
||||
│ 3.3 Group by label │
|
||||
│ (grouped_csv) │
|
||||
└────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────┐
|
||||
│ 4.1 Summarize (grouped_md) │
|
||||
└────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────┐
|
||||
│ 4.2 Final notes (v{VERSION}.md) │
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
|
||||
| Step | Action | Details |
|
||||
|------|--------|---------|
|
||||
| 1.0 | Verify prerequisites | `gh auth status` must pass; generate MemberList.md |
|
||||
| 1.1 | Collect PRs | From previous release tag on `stable` branch → `sorted_prs.csv` |
|
||||
| 1.2 | Assign Milestones | Ensure all PRs have correct milestone |
|
||||
| 2.1–2.4 | Label PRs | Auto-suggest + human label low-confidence |
|
||||
| 3.1–3.3 | Reviews & Grouping | Local agent summarizes each PR diff into `CopilotSummary` → (optional refresh) → group by label |
|
||||
| 4.1–4.2 | Summaries & Final | Generate grouped summaries, then consolidate |
|
||||
|
||||
## Detailed workflow docs
|
||||
|
||||
Do not read all steps at once—only read the step you are executing.
|
||||
|
||||
- [Step 1: Collection & Milestones](./references/step1-collection.md)
|
||||
- [Step 2: Labeling PRs](./references/step2-labeling.md)
|
||||
- [Step 3: Reviews & Grouping](./references/step3-review-grouping.md)
|
||||
- [Step 4: Summarization](./references/step4-summarization.md)
|
||||
|
||||
|
||||
## Available Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| [dump-prs-since-commit.ps1](./scripts/dump-prs-since-commit.ps1) | Fetch PRs between commits/tags |
|
||||
| [group-prs-by-label.ps1](./scripts/group-prs-by-label.ps1) | Group PRs into CSVs |
|
||||
| [collect-or-apply-milestones.ps1](./scripts/collect-or-apply-milestones.ps1) | Assign milestones |
|
||||
| [diff_prs.ps1](./scripts/diff_prs.ps1) | Incremental PR diff |
|
||||
| [prepare-release-assets.ps1](./scripts/prepare-release-assets.ps1) | Download installers + symbols from an ADO build, compute SHA256, emit the "Installer Hashes" markdown table for the GitHub release page |
|
||||
|
||||
## References
|
||||
|
||||
- [Sample Output](./references/SampleOutput.md) - Example summary formatting
|
||||
- [Detailed Instructions](./references/Instruction.md) - Legacy full documentation
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Terminal usage**: Disabled by default; only run scripts when user explicitly requests
|
||||
- **Batch generation**: Generate ALL grouped_md files in one pass, then human reviews
|
||||
- **PR order**: Preserve order from `sorted_prs.csv` in all outputs
|
||||
- **Label filtering**: Keeps `Product-*`, `Area-*`, `GitHub*`, `*Plugin`, `Issue-*`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| `gh` command not found | Install GitHub CLI and add to PATH |
|
||||
| No PRs returned | Verify milestone title matches exactly |
|
||||
| Empty `CopilotSummary` for many PRs | Run Step 3.1 (local-agent summaries). Do **not** use `mcp_github_request_copilot_review` from a CLI/coding agent — the GitHub API rejects bot-initiated review requests, so the column will stay empty. |
|
||||
| Many unlabeled PRs | Return to labeling step before grouping |
|
||||
| `prepare-release-assets.ps1` fails with "Failed to acquire ADO access token" | Run `az login` and ensure you have access to the `microsoft/Dart` ADO project |
|
||||
@@ -1,9 +0,0 @@
|
||||
- Added mouse button actions so you can choose what left, right, or middle click does in [#1234](https://github.com/microsoft/PowerToys/pull/1234) by [@PesBandi](https://github.com/PesBandi)
|
||||
|
||||
- Aligned window styling with current Windows theme for a cleaner look in [#1235](https://github.com/microsoft/PowerToys/pull/1235) by [@sadirano](https://github.com/sadirano)
|
||||
|
||||
- Ensured screen readers are notified when the selected item in the list changes for better accessibility in [#1236](https://github.com/microsoft/PowerToys/pull/1236)
|
||||
|
||||
- Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours in [#1237](https://github.com/microsoft/PowerToys/pull/1237)
|
||||
|
||||
- Fixed Alt+Left Arrow navigation not working when search box contains text in [#1238](https://github.com/microsoft/PowerToys/pull/1238) by [@jiripolasek](https://github.com/jiripolasek)
|
||||
@@ -1,174 +0,0 @@
|
||||
# Step 1: Collection and Milestones
|
||||
|
||||
## 1.0 To-do
|
||||
- 1.0.0 Verify GitHub CLI authentication (REQUIRED)
|
||||
- 1.0.1 Generate MemberList.md (REQUIRED)
|
||||
- 1.1 Collect PRs
|
||||
- 1.2 Assign Milestones (REQUIRED)
|
||||
|
||||
## Required Variables
|
||||
|
||||
⚠️ **Before starting**, confirm these values with the user:
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{{ReleaseVersion}}` | Target release version | `0.97` |
|
||||
| `{{PreviousReleaseTag}}` | Previous release tag from releases page | `v0.96.1` |
|
||||
|
||||
**If user hasn't specified `{{ReleaseVersion}}`, ASK:** "What release version are we generating notes for? (e.g., 0.97)"
|
||||
|
||||
**`{{PreviousReleaseTag}}` is derived from the releases page, not user input.** Use the latest published release tag (top of the page). You will use its tag name and tag commit SHA in Step 1.
|
||||
|
||||
---
|
||||
|
||||
## 1.0.0 Verify GitHub CLI Authentication (REQUIRED)
|
||||
|
||||
⚠️ **BLOCKING:** The collection script requires an authenticated `gh` CLI to fetch PR metadata and co-author information via GitHub's GraphQL API. Without authentication, PR data and `NeedThanks` attribution will be incomplete.
|
||||
|
||||
### Check authentication status
|
||||
|
||||
```powershell
|
||||
gh auth status
|
||||
```
|
||||
|
||||
**If authenticated:** You'll see `Logged in to github.com account <username>`. Proceed to 1.0.1.
|
||||
|
||||
**If NOT authenticated:** Run the login flow before continuing:
|
||||
|
||||
```powershell
|
||||
# Interactive login (opens browser for OAuth)
|
||||
gh auth login --hostname github.com --web
|
||||
|
||||
# Or use a personal access token
|
||||
gh auth login --with-token <<< "YOUR_GITHUB_TOKEN"
|
||||
```
|
||||
|
||||
**Required scopes:** `repo` (for reading PR data and assigning milestones)
|
||||
|
||||
After login, verify again with `gh auth status` and confirm exit code 0.
|
||||
|
||||
---
|
||||
|
||||
## 1.0.1 Generate MemberList.md (REQUIRED)
|
||||
|
||||
Create `Generated Files/ReleaseNotes/MemberList.md` from the **PowerToys core team** section in [COMMUNITY.md](../../../COMMUNITY.md).
|
||||
|
||||
Rules:
|
||||
- One GitHub username per line, **no** `@` prefix.
|
||||
- Use the usernames exactly as listed in the core team section.
|
||||
- Do not include former team members or other sections.
|
||||
|
||||
Example (format only):
|
||||
```
|
||||
example-user
|
||||
another-user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1.1 Collect PRs
|
||||
|
||||
### 1.1.1 Get the previous release commit
|
||||
|
||||
1. Open the [PowerToys releases page](https://github.com/microsoft/PowerToys/releases/)
|
||||
2. Find the latest release (e.g., v0.96.1, which should be at the top)
|
||||
3. Set `{{PreviousReleaseTag}}` to that tag name (e.g., `v0.96.1`)
|
||||
4. Copy the full tag commit SHA as `{{SHALastRelease}}`
|
||||
|
||||
|
||||
**If the release SHA is not in your branch history:** Use the helper script to find an equivalent commit on the target branch by matching the commit title:
|
||||
|
||||
```powershell
|
||||
pwsh ./.github/skills/release-note-generation/scripts/find-commit-by-title.ps1 `
|
||||
-Commit '{{SHALastRelease}}' `
|
||||
-Branch 'stable'
|
||||
```
|
||||
|
||||
### 1.1.2 Run collection script against stable branch
|
||||
|
||||
```powershell
|
||||
# Collect PRs from previous release to current HEAD of stable branch
|
||||
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
|
||||
-StartCommit '{{SHALastRelease}}' `
|
||||
-Branch 'stable' `
|
||||
-OutputDir 'Generated Files/ReleaseNotes'
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `-StartCommit` - Previous release tag or commit SHA (exclusive)
|
||||
- `-Branch` - Always use `stable` branch, not `main` (script uses `origin/stable` as the end ref)
|
||||
- `-EndCommit` - Optional override if you need a custom end ref
|
||||
- `-OutputDir` - Output directory for generated files
|
||||
|
||||
**Reliability check:** If the script reports “No commits found”, the stable branch has not moved since the last release. In that case, either:
|
||||
- Confirm this is expected and stop (no new release notes), or
|
||||
- Re-run against `main` to gather pending changes for the next release cycle.
|
||||
|
||||
The script detects both merge commits (`Merge pull request #12345`) and squash commits (`Feature (#12345)`).
|
||||
|
||||
**Output** (in `Generated Files/ReleaseNotes/`):
|
||||
- `milestone_prs.json` - raw PR data
|
||||
- `sorted_prs.csv` - sorted PR list with columns: Id, Title, Labels, Author, Url, Body, CopilotSummary, NeedThanks
|
||||
- `Author`: Comma-separated list of all contributors (PR opener + co-authors from commit trailers)
|
||||
- `NeedThanks`: Comma-separated list of external contributors to thank (excludes core team members from MemberList.md). Empty string means no thanks needed.
|
||||
|
||||
---
|
||||
|
||||
## 1.2 Assign Milestones (REQUIRED)
|
||||
|
||||
**Before generating release notes**, ensure all collected PRs have the correct milestone assigned.
|
||||
|
||||
⚠️ **CRITICAL:** Do NOT proceed to labeling until all PRs have milestones assigned.
|
||||
|
||||
### 1.2.1 Check current milestone status (dry run)
|
||||
|
||||
```powershell
|
||||
# Dry run first to see what would be changed:
|
||||
pwsh ./.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1 `
|
||||
-InputCsv 'Generated Files/ReleaseNotes/sorted_prs.csv' `
|
||||
-OutputCsv 'Generated Files/ReleaseNotes/prs_with_milestone.csv' `
|
||||
-DefaultMilestone 'PowerToys {{ReleaseVersion}}' `
|
||||
-ApplyMissing -WhatIf
|
||||
```
|
||||
|
||||
This queries GitHub for each PR's current milestone and shows which PRs would be updated.
|
||||
|
||||
### 1.2.2 Apply milestones to PRs missing them
|
||||
|
||||
```powershell
|
||||
# Apply for real:
|
||||
pwsh ./.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1 `
|
||||
-InputCsv 'Generated Files/ReleaseNotes/sorted_prs.csv' `
|
||||
-OutputCsv 'Generated Files/ReleaseNotes/prs_with_milestone.csv' `
|
||||
-DefaultMilestone 'PowerToys {{ReleaseVersion}}' `
|
||||
-ApplyMissing
|
||||
```
|
||||
|
||||
**Script Behavior:**
|
||||
- Queries each PR's current milestone from GitHub
|
||||
- PRs that already have a milestone are **skipped** (not overwritten)
|
||||
- PRs missing a milestone get the default milestone applied
|
||||
- Outputs `prs_with_milestone.csv` with (Id, Milestone) columns
|
||||
- Produces summary: `Updated=X Skipped=Y Failed=Z`
|
||||
|
||||
**Validation:** After assignment, all PRs in `prs_with_milestone.csv` should have the target milestone.
|
||||
|
||||
---
|
||||
|
||||
## Additional Commands
|
||||
|
||||
### Collect milestones only (no changes to GitHub)
|
||||
```powershell
|
||||
pwsh ./.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1 `
|
||||
-InputCsv 'Generated Files/ReleaseNotes/sorted_prs.csv' `
|
||||
-OutputCsv 'Generated Files/ReleaseNotes/prs_with_milestone.csv'
|
||||
```
|
||||
|
||||
### Local assignment only (fill blanks in CSV, no GitHub changes)
|
||||
```powershell
|
||||
pwsh ./.github/skills/release-note-generation/scripts/collect-or-apply-milestones.ps1 `
|
||||
-InputCsv 'Generated Files/ReleaseNotes/sorted_prs.csv' `
|
||||
-OutputCsv 'Generated Files/ReleaseNotes/prs_with_milestone.csv' `
|
||||
-DefaultMilestone 'PowerToys {{ReleaseVersion}}' `
|
||||
-LocalAssign
|
||||
```
|
||||
@@ -1,131 +0,0 @@
|
||||
# Step 2: Label Unlabeled PRs
|
||||
|
||||
## 2.0 To-do
|
||||
- 2.1 Identify unlabeled PRs (Agent Mode)
|
||||
- 2.2 Suggest labels (Agent Mode)
|
||||
- 2.3 Human label low-confidence PRs
|
||||
- 2.4 Recheck labels, delete Unlabeled.csv, and re-collect
|
||||
|
||||
**Before grouping**, ensure all PRs have appropriate labels for categorization.
|
||||
|
||||
⚠️ **CRITICAL:** Do NOT proceed to grouping until all PRs have labels assigned. PRs without labels will end up in `Unlabeled.csv` and won't appear in the correct release note sections.
|
||||
|
||||
## 2.1 Identify unlabeled PRs (Agent Mode)
|
||||
|
||||
Read `sorted_prs.csv` and identify PRs with empty or missing `Labels` column.
|
||||
|
||||
For each unlabeled PR, analyze:
|
||||
- **Title** - Often contains module name or feature
|
||||
- **Body** - PR description with context
|
||||
- **CopilotSummary** - AI-generated summary of changes
|
||||
|
||||
## 2.2 Suggest labels (Agent Mode)
|
||||
|
||||
For each unlabeled PR, suggest an appropriate label based on the content analysis.
|
||||
|
||||
**Output:** Create `Generated Files/ReleaseNotes/prs_label_review.md` with the following format:
|
||||
|
||||
```markdown
|
||||
# PR Label Review
|
||||
|
||||
Generated: YYYY-MM-DD HH:mm:ss
|
||||
|
||||
## Summary
|
||||
- Total unlabeled PRs: X
|
||||
- High confidence: X
|
||||
- Medium confidence: X
|
||||
- Low confidence: X
|
||||
|
||||
---
|
||||
|
||||
## PRs Needing Review (sorted by confidence, low first)
|
||||
|
||||
| PR | Title | Suggested Label | Confidence | Reason |
|
||||
|----|-------|-----------------|------------|--------|
|
||||
| [#12347](url) | Some generic fix | ??? | Low | Unclear from content |
|
||||
| [#12346](url) | Update dependencies | `Area-Build` | Medium | Body mentions NuGet packages |
|
||||
```
|
||||
|
||||
Sort by confidence (low first) so human reviews uncertain ones first.
|
||||
|
||||
After writing `prs_label_review.md`, **generate `prs_to_label.csv`, apply labels, and re-run collection** so the CSV/labels stay in sync:
|
||||
|
||||
```powershell
|
||||
# Generate CSV from suggestions (agent)
|
||||
# Apply labels
|
||||
pwsh ./.github/skills/release-note-generation/scripts/apply-labels.ps1 `
|
||||
-InputCsv 'Generated Files/ReleaseNotes/prs_to_label.csv'
|
||||
|
||||
# Refresh collection
|
||||
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
|
||||
-StartCommit '{{PreviousReleaseTag}}' -Branch 'stable' `
|
||||
-OutputDir 'Generated Files/ReleaseNotes'
|
||||
```
|
||||
|
||||
## 2.3 Human label low-confidence PRs
|
||||
|
||||
Ask the human to label **low-confidence** PRs directly (in GitHub). Skip any they decide not to label.
|
||||
|
||||
## 2.4 Recheck labels, delete Unlabeled.csv, and re-collect
|
||||
|
||||
Recheck that all PRs now have labels. Delete `Unlabeled.csv` (if present), then re-run the collection script to update `sorted_prs.csv`:
|
||||
|
||||
```powershell
|
||||
# Remove stale unlabeled output if it exists
|
||||
Remove-Item 'Generated Files/ReleaseNotes/Unlabeled.csv' -ErrorAction SilentlyContinue
|
||||
```
|
||||
|
||||
```powershell
|
||||
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
|
||||
-StartCommit '{{PreviousReleaseTag}}' -Branch 'stable' `
|
||||
-OutputDir 'Generated Files/ReleaseNotes'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Label Mappings
|
||||
|
||||
| Keywords/Patterns | Suggested Label |
|
||||
| ----------------- | --------------- |
|
||||
| Advanced Paste, AP, clipboard, paste | `Product-Advanced Paste` |
|
||||
| CmdPal, Command Palette, cmdpal | `Product-Command Palette` |
|
||||
| FancyZones, zones, layout | `Product-FancyZones` |
|
||||
| ZoomIt, zoom, screen annotation | `Product-ZoomIt` |
|
||||
| Settings, settings-ui, Quick Access, flyout | `Product-Settings` |
|
||||
| Installer, setup, MSI, MSIX, WiX | `Area-Setup/Install` |
|
||||
| Build, pipeline, CI/CD, msbuild | `Area-Build` |
|
||||
| Test, unit test, UI test, fuzz | `Area-Tests` |
|
||||
| Localization, loc, translation, resw | `Area-Localization` |
|
||||
| Foundry, AI, LLM | `Product-Advanced Paste` (AI features) |
|
||||
| Mouse Without Borders, MWB | `Product-Mouse Without Borders` |
|
||||
| PowerRename, rename, regex | `Product-PowerRename` |
|
||||
| Peek, preview, file preview | `Product-Peek` |
|
||||
| Image Resizer, resize | `Product-Image Resizer` |
|
||||
| LightSwitch, theme, dark mode | `Product-LightSwitch` |
|
||||
| Quick Accent, accent, diacritics | `Product-Quick Accent` |
|
||||
| Awake, keep awake, caffeine | `Product-Awake` |
|
||||
| ColorPicker, color picker, eyedropper | `Product-ColorPicker` |
|
||||
| Hosts, hosts file | `Product-Hosts` |
|
||||
| Keyboard Manager, remap | `Product-Keyboard Manager` |
|
||||
| Mouse Highlighter | `Product-Mouse Highlighter` |
|
||||
| Mouse Jump | `Product-Mouse Jump` |
|
||||
| Find My Mouse | `Product-Find My Mouse` |
|
||||
| Mouse Pointer Crosshairs | `Product-Mouse Pointer Crosshairs` |
|
||||
| Shortcut Guide | `Product-Shortcut Guide` |
|
||||
| Text Extractor, OCR, PowerOCR | `Product-Text Extractor` |
|
||||
| Workspaces | `Product-Workspaces` |
|
||||
| File Locksmith | `Product-File Locksmith` |
|
||||
| Crop And Lock | `Product-CropAndLock` |
|
||||
| Environment Variables | `Product-Environment Variables` |
|
||||
| New+ | `Product-New+` |
|
||||
|
||||
## Label Filtering Rules
|
||||
|
||||
The grouping script keeps labels matching these patterns:
|
||||
- `Product-*`
|
||||
- `Area-*`
|
||||
- `GitHub*`
|
||||
- `*Plugin`
|
||||
- `Issue-*`
|
||||
|
||||
Other labels are ignored for grouping purposes.
|
||||
@@ -1,58 +0,0 @@
|
||||
# Step 3: Local Agent Reviews and Grouping
|
||||
|
||||
## 3.0 To-do
|
||||
- 3.1 Generate PR Summaries with the Local Agent
|
||||
- 3.2 (Optional) Refresh PR Data
|
||||
- 3.3 Group PRs by Label
|
||||
|
||||
## 3.1 Generate PR Summaries with the Local Agent
|
||||
|
||||
> ⚠️ **Do not use `mcp_github_request_copilot_review` (or any "request Copilot review" tool that calls the GitHub API).**
|
||||
> When this skill is driven from a CLI / coding agent, the request is made from a bot identity and the GitHub API rejects it ("Bot reviewers cannot be requested"). The PR ends up with no Copilot review and `CopilotSummary` stays empty.
|
||||
>
|
||||
> Instead, **the local agent that is running this skill performs the review itself** and writes the summary directly into `sorted_prs.csv`.
|
||||
|
||||
For every PR listed in `Generated Files/ReleaseNotes/sorted_prs.csv` whose `CopilotSummary` is empty:
|
||||
|
||||
1. Fetch the PR diff using a tool that does **not** post anything back to GitHub. Any of these works:
|
||||
- `mcp_github_pull_request_read` with `method: get_diff`
|
||||
- `mcp_github_pull_request_read` with `method: get_files` (when the diff is large)
|
||||
- `gh pr diff <PR_NUMBER> --repo microsoft/PowerToys`
|
||||
2. Read the PR title, body, and diff. Produce a 1–3 sentence, user-facing summary in the same style as a Copilot PR review (focus on observable behavior change, not implementation details).
|
||||
3. Write the summary into the `CopilotSummary` column for that PR row in `Generated Files/ReleaseNotes/sorted_prs.csv`. Preserve all other columns and the existing row order.
|
||||
|
||||
**Batching guidance**
|
||||
|
||||
- Process PRs in the order they appear in `sorted_prs.csv`.
|
||||
- Generate summaries for **all** PRs in one pass before continuing to Step 3.3, so the human reviewer can validate them together.
|
||||
- For very large diffs, summarize from `get_files` (filenames + per-file patches) rather than the full diff.
|
||||
- Skip PRs that already have a non-empty `CopilotSummary` (e.g. PRs where a human reviewer already pasted one). Do not overwrite existing summaries.
|
||||
|
||||
**Why not post the summary back to the PR?** Posting a comment from the agent's identity would not be picked up by `dump-prs-since-commit.ps1` (which only matches Copilot bot authors), and it adds noise to the PR. Writing straight into the CSV keeps the artifact self-contained.
|
||||
|
||||
---
|
||||
|
||||
## 3.2 (Optional) Refresh PR Data
|
||||
|
||||
Only re-run the collection script if PR metadata on GitHub has changed (new labels, retitled PRs, etc.) since Step 1.1. **Skip this step if you only want to preserve the locally generated `CopilotSummary` values from Step 3.1**, because re-running the dump will overwrite the CSV.
|
||||
|
||||
```powershell
|
||||
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
|
||||
-StartCommit '{{PreviousReleaseTag}}' -Branch 'stable' `
|
||||
-OutputDir 'Generated Files/ReleaseNotes'
|
||||
```
|
||||
|
||||
If you do refresh, redo Step 3.1 afterwards to repopulate `CopilotSummary`.
|
||||
|
||||
---
|
||||
|
||||
## 3.3 Group PRs by Label
|
||||
|
||||
```powershell
|
||||
pwsh ./.github/skills/release-note-generation/scripts/group-prs-by-label.ps1 -CsvPath 'Generated Files/ReleaseNotes/sorted_prs.csv' -OutDir 'Generated Files/ReleaseNotes/grouped_csv'
|
||||
```
|
||||
|
||||
Creates `Generated Files/ReleaseNotes/grouped_csv/` with one CSV per label combination.
|
||||
|
||||
**Validation:** The `Unlabeled.csv` file should be minimal (ideally empty). If many PRs remain unlabeled, return to Step 2 (see [step2-labeling.md](./step2-labeling.md)).
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Step 4: Summaries and Final Release Notes
|
||||
|
||||
## 4.0 To-do
|
||||
- 4.1 Generate Summary Markdown (Agent Mode)
|
||||
- 4.2 Produce Final Release Notes File
|
||||
|
||||
## 4.1 Generate Summary Markdown (Agent Mode)
|
||||
|
||||
For each CSV in `Generated Files/ReleaseNotes/grouped_csv/`, create a markdown file in `Generated Files/ReleaseNotes/grouped_md/`.
|
||||
|
||||
⚠️ **IMPORTANT:** Generate **ALL** markdown files first. Do NOT pause between files or ask for feedback during generation. Complete the entire batch, then human reviews afterwards.
|
||||
|
||||
### Structure per file
|
||||
|
||||
**1. Bullet list** - one concise, user-facing line per PR:
|
||||
- Use the “Verb-ed + Scenario + Impact” sentence structure—make readers think, “That’s exactly what I need” or “Yes, that’s an awesome fix.”; The "impact" can be end-user focused (written to convey user excitement) or technical (performance/stability) when user-facing impact is minimal.
|
||||
- If nothing special on impact or unclear impact, mark as needing human summary
|
||||
- Source from Title, Body, and CopilotSummary (prefer CopilotSummary when available)
|
||||
- The `NeedThanks` column contains a comma-separated list of external contributor usernames who should be credited (empty = no attribution needed, all authors are core team). For each non-empty `NeedThanks` value, append a `by` attribution that lists **every** contributor, matching GitHub's standard contributor-attribution style: `by [@user1](https://github.com/user1)` for a single contributor, `by [@user1](https://github.com/user1) and [@user2](https://github.com/user2)` for two, or `by [@user1](https://github.com/user1), [@user2](https://github.com/user2), and [@user3](https://github.com/user3)` for three or more. In the final consolidated release notes (Step 4.2), the attribution follows the PR link, e.g. `…in [#1234](url) by [@user](url)`. Do not use "Thanks @user!" phrasing.
|
||||
- Do NOT include PR numbers in bullet lines
|
||||
- Do NOT mention “security” or “privacy” issues, since these are not known and could be leveraged by attackers in earlier versions. Instead, describe the user-facing scenario, usage, or impact.
|
||||
- If confidence < 70%, write: `Human Summary Needed: <PR full link>`
|
||||
|
||||
**See [SampleOutput.md](./SampleOutput.md) for examples of well-written bullet summaries.**
|
||||
|
||||
**2. Three-column table** (same PR order):
|
||||
- Column 1: Concise summary (same as bullet)
|
||||
- Column 2: PR link `[#ID](URL)`
|
||||
- Column 3: Confidence level (High/Medium/Low)
|
||||
|
||||
### Review Process (AFTER all files generated)
|
||||
|
||||
- Human reviews each `grouped_md/*.md` file and requests rewrites as needed
|
||||
- Human may say "rewrite Product-X" or "combine these bullets"—apply changes to that specific file
|
||||
- Do NOT interrupt generation to ask for feedback
|
||||
|
||||
---
|
||||
|
||||
## 4.2 Produce Final Release Notes File
|
||||
|
||||
Once all `grouped_md/*.md` files are reviewed and approved, consolidate into a single release notes file.
|
||||
|
||||
**Output:** `Generated Files/ReleaseNotes/v{{ReleaseVersion}}-release-notes.md`
|
||||
|
||||
### Structure
|
||||
|
||||
**1. Highlights section** (top):
|
||||
- 8-12 bullets covering the most user-visible features and impactful fixes
|
||||
- Pattern: `**Module**: brief description`
|
||||
- Avoid internal refactors; focus on what users will notice
|
||||
|
||||
**2. Module sections** (alphabetical order):
|
||||
- One section per product (Advanced Paste, Awake, Command Palette, etc.)
|
||||
- Migrate bullet summaries from the approved `grouped_md/Product-*.md` files
|
||||
- One section 'Development' for all the rest summaries from the approved `grouped_md/Area-*.md` files
|
||||
- Re-review E2E, group release improvements by section, and move the most important items to the top of each section.
|
||||
Some items in the Development section may overlap and should be moved to the Module section where more applicable.
|
||||
|
||||
### Example Final Structure
|
||||
|
||||
```markdown
|
||||
# PowerToys v{{ReleaseVersion}} Release Notes
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Command Palette**: Added theme customization and drag-and-drop support
|
||||
- **Advanced Paste**: Image input for AI, color detection in clipboard history
|
||||
- **FancyZones**: New CLI tool for command-line layout management
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
## Advanced Paste
|
||||
|
||||
- Wrapped paste option lists in a single ScrollViewer in [#5678](https://github.com/microsoft/PowerToys/pull/5678)
|
||||
- Added image input handling for AI-powered transformations in [#5679](https://github.com/microsoft/PowerToys/pull/5679)
|
||||
...
|
||||
|
||||
## Awake
|
||||
|
||||
- Fixed timed mode expiration in [#5680](https://github.com/microsoft/PowerToys/pull/5680) by [@daverayment](https://github.com/daverayment)
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
...
|
||||
```
|
||||
@@ -1,90 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Apply labels to PRs from a CSV file.
|
||||
|
||||
.DESCRIPTION
|
||||
Reads a CSV with Id and Label columns and applies the specified label to each PR via GitHub CLI.
|
||||
Supports dry-run mode to preview changes before applying.
|
||||
|
||||
.PARAMETER InputCsv
|
||||
CSV file with Id and Label columns. Default: prs_to_label.csv
|
||||
|
||||
.PARAMETER Repo
|
||||
GitHub repository (owner/name). Default: microsoft/PowerToys
|
||||
|
||||
.PARAMETER WhatIf
|
||||
Dry run - show what would be applied without making changes.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh ./apply-labels.ps1 -InputCsv 'Generated Files/ReleaseNotes/prs_to_label.csv'
|
||||
|
||||
.EXAMPLE
|
||||
pwsh ./apply-labels.ps1 -InputCsv 'Generated Files/ReleaseNotes/prs_to_label.csv' -WhatIf
|
||||
|
||||
.NOTES
|
||||
Requires: gh CLI authenticated with repo write access.
|
||||
|
||||
Input CSV format:
|
||||
Id,Label
|
||||
12345,Product-Advanced Paste
|
||||
12346,Product-Settings
|
||||
#>
|
||||
[CmdletBinding()] param(
|
||||
[Parameter(Mandatory=$false)][string]$InputCsv = 'prs_to_label.csv',
|
||||
[Parameter(Mandatory=$false)][string]$Repo = 'microsoft/PowerToys',
|
||||
[switch]$WhatIf
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-Info($m){ Write-Host "[info] $m" -ForegroundColor Cyan }
|
||||
function Write-Warn($m){ Write-Host "[warn] $m" -ForegroundColor Yellow }
|
||||
function Write-Err($m){ Write-Host "[error] $m" -ForegroundColor Red }
|
||||
function Write-OK($m){ Write-Host "[ok] $m" -ForegroundColor Green }
|
||||
|
||||
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { Write-Err "GitHub CLI 'gh' not found in PATH"; exit 1 }
|
||||
if (-not (Test-Path -LiteralPath $InputCsv)) { Write-Err "Input CSV not found: $InputCsv"; exit 1 }
|
||||
|
||||
$rows = Import-Csv -LiteralPath $InputCsv
|
||||
if (-not $rows) { Write-Info "No rows in CSV."; exit 0 }
|
||||
|
||||
$firstCols = $rows[0].PSObject.Properties.Name
|
||||
if (-not ($firstCols -contains 'Id' -and $firstCols -contains 'Label')) {
|
||||
Write-Err "CSV must contain 'Id' and 'Label' columns"; exit 1
|
||||
}
|
||||
|
||||
Write-Info "Processing $($rows.Count) label assignments..."
|
||||
if ($WhatIf) { Write-Warn "DRY RUN - no changes will be made" }
|
||||
|
||||
$applied = 0
|
||||
$skipped = 0
|
||||
$failed = 0
|
||||
|
||||
foreach ($row in $rows) {
|
||||
$id = $row.Id
|
||||
$label = $row.Label
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($id) -or [string]::IsNullOrWhiteSpace($label)) {
|
||||
Write-Warn "Skipping row with empty Id or Label"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
if ($WhatIf) {
|
||||
Write-Info "Would apply label '$label' to PR #$id"
|
||||
$applied++
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
gh pr edit $id --repo $Repo --add-label $label 2>&1 | Out-Null
|
||||
Write-OK "Applied '$label' to PR #$id"
|
||||
$applied++
|
||||
} catch {
|
||||
Write-Warn "Failed to apply label to PR #${id}: $_"
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
|
||||
Write-Info ""
|
||||
Write-Info "Summary: Applied=$applied Skipped=$skipped Failed=$failed"
|
||||
@@ -1,172 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Collect existing PR milestones or (optionally) assign/apply a milestone to missing PRs in one script.
|
||||
|
||||
.DESCRIPTION
|
||||
This unified script merges the behaviors of the previous add-milestone-column (collector) and
|
||||
set-milestones-missing (remote updater) scripts.
|
||||
|
||||
Modes (controlled by switches):
|
||||
1. Collect (default) – For each PR Id in the input CSV, queries GitHub for the current milestone and
|
||||
outputs a two-column CSV (Id,Milestone) leaving blanks where none are set.
|
||||
2. LocalAssign – Same as Collect, but for rows that end up blank assigns the value of -DefaultMilestone
|
||||
in memory (does NOT touch GitHub). Useful for quickly preparing a fully populated CSV.
|
||||
3. ApplyMissing – After determining which PRs have no milestone, call GitHub API to set their milestone
|
||||
to -DefaultMilestone. Requires milestone to already exist (open). Network + write.
|
||||
|
||||
You can combine LocalAssign and ApplyMissing: the remote update uses the existing live state; LocalAssign only
|
||||
affects the output CSV/pipeline objects.
|
||||
|
||||
.PARAMETER InputCsv
|
||||
Source CSV with at least an Id column. Default: sorted_prs.csv
|
||||
|
||||
.PARAMETER OutputCsv
|
||||
Destination CSV for collected (and optionally locally assigned) milestones. Default: prs_with_milestone.csv
|
||||
|
||||
.PARAMETER Repo
|
||||
GitHub repository (owner/name). Default: microsoft/PowerToys
|
||||
|
||||
.PARAMETER DefaultMilestone
|
||||
Milestone title used when -LocalAssign or -ApplyMissing is specified. Default: 'PowerToys 0.97'
|
||||
|
||||
.PARAMETER Offline
|
||||
Skip ALL GitHub lookups / updates. Implies Collect-only with all Milestone cells blank (unless LocalAssign).
|
||||
|
||||
.PARAMETER LocalAssign
|
||||
Populate empty Milestone cells in the output with -DefaultMilestone (does not modify GitHub).
|
||||
|
||||
.PARAMETER ApplyMissing
|
||||
For PRs which currently have no milestone (live on GitHub), set them to -DefaultMilestone via Issues API.
|
||||
|
||||
.PARAMETER WhatIf
|
||||
Dry run for ApplyMissing: show intended remote changes without performing PATCH requests.
|
||||
|
||||
.EXAMPLE
|
||||
# Collect only
|
||||
pwsh ./collect-or-apply-milestones.ps1
|
||||
|
||||
.EXAMPLE
|
||||
# Collect and fill blanks locally in the output only
|
||||
pwsh ./collect-or-apply-milestones.ps1 -LocalAssign
|
||||
|
||||
.EXAMPLE
|
||||
# Collect and remotely apply milestone to missing PRs
|
||||
pwsh ./collect-or-apply-milestones.ps1 -ApplyMissing
|
||||
|
||||
.EXAMPLE
|
||||
# Dry run remote application
|
||||
pwsh ./collect-or-apply-milestones.ps1 -ApplyMissing -WhatIf
|
||||
|
||||
.EXAMPLE
|
||||
# Offline local assignment
|
||||
pwsh ./collect-or-apply-milestones.ps1 -Offline -LocalAssign -DefaultMilestone 'PowerToys 0.96'
|
||||
|
||||
.NOTES
|
||||
Requires gh CLI unless -Offline AND -ApplyMissing not specified.
|
||||
Remote apply path queries milestones to resolve numeric ID.
|
||||
#>
|
||||
[CmdletBinding()] param(
|
||||
[Parameter(Mandatory=$false)][string]$InputCsv = 'sorted_prs.csv',
|
||||
[Parameter(Mandatory=$false)][string]$OutputCsv = 'prs_with_milestone.csv',
|
||||
[Parameter(Mandatory=$false)][string]$Repo = 'microsoft/PowerToys',
|
||||
[Parameter(Mandatory=$false)][string]$DefaultMilestone = 'PowerToys 0.97',
|
||||
[switch]$Offline,
|
||||
[switch]$LocalAssign,
|
||||
[switch]$ApplyMissing,
|
||||
[switch]$WhatIf
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
function Write-Info($m){ Write-Host "[info] $m" -ForegroundColor Cyan }
|
||||
function Write-Warn($m){ Write-Host "[warn] $m" -ForegroundColor Yellow }
|
||||
function Write-Err($m){ Write-Host "[error] $m" -ForegroundColor Red }
|
||||
|
||||
if (-not (Test-Path -LiteralPath $InputCsv)) { Write-Err "Input CSV not found: $InputCsv"; exit 1 }
|
||||
$rows = Import-Csv -LiteralPath $InputCsv
|
||||
if (-not $rows) { Write-Warn "Input CSV has no rows."; @() | Export-Csv -NoTypeInformation -LiteralPath $OutputCsv; exit 0 }
|
||||
if (-not ($rows[0].PSObject.Properties.Name -contains 'Id')) { Write-Err "Input CSV missing 'Id' column."; exit 1 }
|
||||
|
||||
$needGh = (-not $Offline) -and ($ApplyMissing -or -not $Offline)
|
||||
if ($needGh -and -not (Get-Command gh -ErrorAction SilentlyContinue)) { Write-Err "GitHub CLI 'gh' not found. Use -Offline or install gh."; exit 1 }
|
||||
|
||||
# Step 1: Collect current milestone titles
|
||||
$milestoneCache = @{}
|
||||
$collected = New-Object System.Collections.Generic.List[object]
|
||||
$idx = 0
|
||||
foreach ($row in $rows) {
|
||||
$idx++
|
||||
$id = $row.Id
|
||||
if (-not $id) { Write-Warn "Row $idx missing Id; skipping"; continue }
|
||||
$ms = ''
|
||||
if (-not $Offline) {
|
||||
if ($milestoneCache.ContainsKey($id)) { $ms = $milestoneCache[$id] }
|
||||
else {
|
||||
try {
|
||||
$json = gh pr view $id --repo $Repo --json milestone 2>$null | ConvertFrom-Json
|
||||
if ($json -and $json.milestone -and $json.milestone.title) { $ms = $json.milestone.title }
|
||||
} catch {
|
||||
Write-Warn "Failed to fetch PR #$id milestone: $_"
|
||||
}
|
||||
$milestoneCache[$id] = $ms
|
||||
}
|
||||
}
|
||||
$collected.Add([PSCustomObject]@{ Id = $id; Milestone = $ms }) | Out-Null
|
||||
}
|
||||
|
||||
# Step 2: Remote apply (if requested)
|
||||
$applySummary = @()
|
||||
if ($ApplyMissing) {
|
||||
if ($Offline) { Write-Err "Cannot use -ApplyMissing with -Offline."; exit 1 }
|
||||
Write-Info "Resolving milestone id for '$DefaultMilestone' ..."
|
||||
$milestonesRaw = gh api repos/$Repo/milestones --paginate --jq '.[] | {number,title,state}'
|
||||
$msObj = $milestonesRaw | ConvertFrom-Json | Where-Object { $_.title -eq $DefaultMilestone -and $_.state -eq 'open' } | Select-Object -First 1
|
||||
if (-not $msObj) { Write-Err "Milestone '$DefaultMilestone' not found/open."; exit 1 }
|
||||
$msNumber = $msObj.number
|
||||
$targets = $collected | Where-Object { [string]::IsNullOrWhiteSpace($_.Milestone) }
|
||||
Write-Info ("ApplyMissing: {0} PR(s) without milestone." -f $targets.Count)
|
||||
foreach ($t in $targets) {
|
||||
$id = $t.Id
|
||||
try {
|
||||
# Verify still missing live
|
||||
$current = gh pr view $id --repo $Repo --json milestone --jq '.milestone.title // ""'
|
||||
if ($current) {
|
||||
$applySummary += [PSCustomObject]@{ Id=$id; Action='Skip (already has)'; Milestone=$current; Status='OK' }
|
||||
continue
|
||||
}
|
||||
if ($WhatIf) {
|
||||
$applySummary += [PSCustomObject]@{ Id=$id; Action='Would set'; Milestone=$DefaultMilestone; Status='DRY RUN' }
|
||||
continue
|
||||
}
|
||||
gh api -X PATCH -H 'Accept: application/vnd.github+json' repos/$Repo/issues/$id -f milestone=$msNumber | Out-Null
|
||||
$applySummary += [PSCustomObject]@{ Id=$id; Action='Set'; Milestone=$DefaultMilestone; Status='OK' }
|
||||
# Reflect in collected object for CSV output if not LocalAssign already doing so
|
||||
$t.Milestone = $DefaultMilestone
|
||||
} catch {
|
||||
$errText = $_ | Out-String
|
||||
$applySummary += [PSCustomObject]@{ Id=$id; Action='Failed'; Milestone=$DefaultMilestone; Status=$errText.Trim() }
|
||||
Write-Warn ("Failed to set milestone for PR #{0}: {1}" -f $id, ($errText.Trim()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Step 3: Local assignment (purely for output) AFTER remote so remote actual result not overwritten accidentally
|
||||
if ($LocalAssign) {
|
||||
foreach ($item in $collected) {
|
||||
if ([string]::IsNullOrWhiteSpace($item.Milestone)) { $item.Milestone = $DefaultMilestone }
|
||||
}
|
||||
}
|
||||
|
||||
# Step 4: Export CSV
|
||||
$collected | Export-Csv -LiteralPath $OutputCsv -NoTypeInformation -Encoding UTF8
|
||||
Write-Info ("Wrote collected CSV -> {0}" -f (Resolve-Path -LiteralPath $OutputCsv))
|
||||
|
||||
# Step 5: Summaries
|
||||
if ($ApplyMissing) {
|
||||
$updated = ($applySummary | Where-Object { $_.Action -eq 'Set' }).Count
|
||||
$skipped = ($applySummary | Where-Object { $_.Action -like 'Skip*' }).Count
|
||||
$failed = ($applySummary | Where-Object { $_.Action -eq 'Failed' }).Count
|
||||
Write-Info ("ApplyMissing summary: Updated={0} Skipped={1} Failed={2}" -f $updated, $skipped, $failed)
|
||||
}
|
||||
|
||||
# Emit objects (final collected set)
|
||||
return $collected
|
||||
@@ -1,100 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Produce an incremental PR CSV containing rows present in a newer full export but absent from a baseline export.
|
||||
|
||||
.DESCRIPTION
|
||||
Compares two previously generated sorted PR CSV files (same schema). Any row whose key column value
|
||||
(defaults to 'Number') does not exist in the baseline file is emitted to a new incremental CSV, preserving
|
||||
the original column order. If no new rows are found, an empty CSV (with headers when determinable) is written.
|
||||
|
||||
.PARAMETER BaseCsv
|
||||
Path to the baseline (earlier) PR CSV.
|
||||
|
||||
.PARAMETER AllCsv
|
||||
Path to the newer full PR CSV containing superset (or equal set) of rows.
|
||||
|
||||
.PARAMETER OutCsv
|
||||
Path to write the incremental CSV containing only new rows.
|
||||
|
||||
.PARAMETER Key
|
||||
Column name used as unique identifier (defaults to 'Number'). Must exist in both CSVs.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh ./diff_prs.ps1 -BaseCsv sorted_prs_prev.csv -AllCsv sorted_prs.csv -OutCsv sorted_prs_incremental.csv
|
||||
|
||||
.NOTES
|
||||
Requires: PowerShell 7+, both CSVs with identical column schemas.
|
||||
Exit code 0 on success (even if zero incremental rows). Throws on missing files.
|
||||
#>
|
||||
|
||||
[CmdletBinding()] param(
|
||||
[Parameter(Mandatory=$false)][string]$BaseCsv = "./sorted_prs_93_round1.csv",
|
||||
[Parameter(Mandatory=$false)][string]$AllCsv = "./sorted_prs.csv",
|
||||
[Parameter(Mandatory=$false)][string]$OutCsv = "./sorted_prs_93_incremental.csv",
|
||||
[Parameter(Mandatory=$false)][string]$Key = "Number"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-Info($m) { Write-Host "[info] $m" -ForegroundColor Cyan }
|
||||
function Write-Warn($m) { Write-Host "[warn] $m" -ForegroundColor Yellow }
|
||||
|
||||
if (-not (Test-Path -LiteralPath $BaseCsv)) { throw "Base CSV not found: $BaseCsv" }
|
||||
if (-not (Test-Path -LiteralPath $AllCsv)) { throw "All CSV not found: $AllCsv" }
|
||||
|
||||
# Load CSVs
|
||||
$baseRows = Import-Csv -LiteralPath $BaseCsv
|
||||
$allRows = Import-Csv -LiteralPath $AllCsv
|
||||
|
||||
if (-not $baseRows) { Write-Warn "Base CSV has no rows." }
|
||||
if (-not $allRows) { Write-Warn "All CSV has no rows." }
|
||||
|
||||
# Validate key presence
|
||||
if ($baseRows -and -not ($baseRows[0].PSObject.Properties.Name -contains $Key)) { throw "Key column '$Key' not found in base CSV." }
|
||||
if ($allRows -and -not ($allRows[0].PSObject.Properties.Name -contains $Key)) { throw "Key column '$Key' not found in all CSV." }
|
||||
|
||||
# Build a set of existing keys from base
|
||||
$set = New-Object 'System.Collections.Generic.HashSet[string]'
|
||||
foreach ($row in $baseRows) {
|
||||
$val = [string]($row.$Key)
|
||||
if ($null -ne $val) { [void]$set.Add($val) }
|
||||
}
|
||||
|
||||
# Filter rows in AllCsv whose key is not in base (these are the new / incremental rows)
|
||||
$incremental = @()
|
||||
foreach ($row in $allRows) {
|
||||
$val = [string]($row.$Key)
|
||||
if (-not $set.Contains($val)) { $incremental += $row }
|
||||
}
|
||||
|
||||
# Preserve column order from the All CSV
|
||||
$columns = @()
|
||||
if ($allRows.Count -gt 0) {
|
||||
$columns = $allRows[0].PSObject.Properties.Name
|
||||
}
|
||||
|
||||
try {
|
||||
if ($incremental.Count -gt 0) {
|
||||
if ($columns.Count -gt 0) {
|
||||
$incremental | Select-Object -Property $columns | Export-Csv -LiteralPath $OutCsv -NoTypeInformation -Encoding UTF8
|
||||
} else {
|
||||
$incremental | Export-Csv -LiteralPath $OutCsv -NoTypeInformation -Encoding UTF8
|
||||
}
|
||||
} else {
|
||||
# Write an empty CSV with headers if we know them (facilitates downstream tooling expecting header row)
|
||||
if ($columns.Count -gt 0) {
|
||||
$obj = [PSCustomObject]@{}
|
||||
foreach ($c in $columns) { $obj | Add-Member -NotePropertyName $c -NotePropertyValue $null }
|
||||
$obj | Select-Object -Property $columns | Export-Csv -LiteralPath $OutCsv -NoTypeInformation -Encoding UTF8
|
||||
} else {
|
||||
'' | Out-File -LiteralPath $OutCsv -Encoding UTF8
|
||||
}
|
||||
}
|
||||
Write-Info ("Incremental rows: {0}" -f $incremental.Count)
|
||||
Write-Info ("Output: {0}" -f (Resolve-Path -LiteralPath $OutCsv))
|
||||
}
|
||||
catch {
|
||||
Write-Host "[error] Failed writing output CSV: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Export merged PR metadata between two commits (exclusive start, inclusive end) to JSON and CSV.
|
||||
|
||||
.DESCRIPTION
|
||||
Identifies merge/squash commits reachable from EndCommit but not StartCommit, extracts PR numbers,
|
||||
queries GitHub for metadata plus (optionally) Copilot review/comment summaries, filters labels, then
|
||||
emits a JSON artifact and a sorted CSV (first label alphabetical).
|
||||
|
||||
.PARAMETER StartCommit
|
||||
Exclusive starting commit (SHA, tag, or ref). Commits AFTER this one are considered.
|
||||
|
||||
.PARAMETER EndCommit
|
||||
Inclusive ending commit (SHA, tag, or ref). If not provided, uses origin/<Branch> when Branch is set; otherwise uses HEAD.
|
||||
|
||||
.PARAMETER Repo
|
||||
GitHub repository (owner/name). Default: microsoft/PowerToys.
|
||||
|
||||
.PARAMETER OutputCsv
|
||||
Destination CSV path. Default: sorted_prs.csv.
|
||||
|
||||
.PARAMETER OutputJson
|
||||
Destination JSON path containing raw PR objects. Default: milestone_prs.json.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh ./dump-prs-since-commit.ps1 -StartCommit 0123abcd -Branch stable
|
||||
|
||||
.EXAMPLE
|
||||
pwsh ./dump-prs-since-commit.ps1 -StartCommit 0123abcd -EndCommit 89ef7654 -OutputCsv delta.csv
|
||||
|
||||
.NOTES
|
||||
Requires: git, gh (authenticated). No Set-StrictMode to keep parity with existing release scripts.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$StartCommit, # exclusive start (commits AFTER this one)
|
||||
[string]$EndCommit,
|
||||
[string]$Branch,
|
||||
[string]$Repo = "microsoft/PowerToys",
|
||||
[string]$OutputDir,
|
||||
[string]$OutputCsv = "sorted_prs.csv",
|
||||
[string]$OutputJson = "milestone_prs.json"
|
||||
)
|
||||
|
||||
# (See top-level synopsis above for full documentation)
|
||||
|
||||
function Write-Info($msg) { Write-Host $msg -ForegroundColor Cyan }
|
||||
function Write-Warn($msg) { Write-Host $msg -ForegroundColor Yellow }
|
||||
function Write-Err($msg) { Write-Host $msg -ForegroundColor Red }
|
||||
function Write-DebugMsg($msg) { if ($PSBoundParameters.ContainsKey('Verbose') -or $VerbosePreference -eq 'Continue') { Write-Host "[VERBOSE] $msg" -ForegroundColor DarkGray } }
|
||||
|
||||
# Load member list from Generated Files/ReleaseNotes/MemberList.md (internal team - no thanks needed)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repoRoot = Resolve-Path (Join-Path $scriptDir "..\..\..\..")
|
||||
$defaultMemberListPath = Join-Path $repoRoot "Generated Files\ReleaseNotes\MemberList.md"
|
||||
$memberListPath = $defaultMemberListPath
|
||||
if ($OutputDir) {
|
||||
$memberListFromOutputDir = Join-Path $OutputDir "MemberList.md"
|
||||
if (Test-Path $memberListFromOutputDir) {
|
||||
$memberListPath = $memberListFromOutputDir
|
||||
}
|
||||
}
|
||||
$memberList = @()
|
||||
if (Test-Path $memberListPath) {
|
||||
$memberListContent = Get-Content $memberListPath -Raw
|
||||
# Extract usernames - skip markdown code fence lines, get all non-empty lines
|
||||
$memberList = ($memberListContent -split "`n") | Where-Object { $_ -notmatch '^\s*```' -and $_.Trim() -ne '' } | ForEach-Object { $_.Trim() }
|
||||
if (-not $memberList -or $memberList.Count -eq 0) {
|
||||
Write-Err "MemberList.md is empty at $memberListPath"
|
||||
exit 1
|
||||
}
|
||||
Write-DebugMsg "Loaded $($memberList.Count) members from MemberList.md"
|
||||
} else {
|
||||
Write-Err "MemberList.md not found at $memberListPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Validate we are in a git repo
|
||||
#if (-not (Test-Path .git)) {
|
||||
# Write-Err "Current directory does not appear to be the root of a git repository."
|
||||
# exit 1
|
||||
#}
|
||||
|
||||
# Resolve output directory (if specified)
|
||||
if ($OutputDir) {
|
||||
if (-not (Test-Path $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputCsv)) {
|
||||
$OutputCsv = Join-Path $OutputDir $OutputCsv
|
||||
}
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutputJson)) {
|
||||
$OutputJson = Join-Path $OutputDir $OutputJson
|
||||
}
|
||||
}
|
||||
|
||||
# Resolve commits
|
||||
try {
|
||||
if ($Branch) {
|
||||
Write-Info "Fetching latest '$Branch' from origin (with tags)..."
|
||||
git fetch origin $Branch --tags | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "git fetch origin $Branch --tags failed" }
|
||||
}
|
||||
|
||||
$startSha = (git rev-parse --verify $StartCommit) 2>$null
|
||||
if (-not $startSha) { throw "StartCommit '$StartCommit' not found" }
|
||||
if ($Branch) {
|
||||
$branchRef = $Branch
|
||||
$branchSha = (git rev-parse --verify $branchRef) 2>$null
|
||||
if (-not $branchSha) {
|
||||
$branchRef = "origin/$Branch"
|
||||
$branchSha = (git rev-parse --verify $branchRef) 2>$null
|
||||
}
|
||||
if (-not $branchSha) { throw "Branch '$Branch' not found" }
|
||||
if (-not $PSBoundParameters.ContainsKey('EndCommit') -or [string]::IsNullOrWhiteSpace($EndCommit)) {
|
||||
$EndCommit = $branchRef
|
||||
}
|
||||
}
|
||||
if (-not $PSBoundParameters.ContainsKey('EndCommit') -or [string]::IsNullOrWhiteSpace($EndCommit)) {
|
||||
$EndCommit = "HEAD"
|
||||
}
|
||||
$endSha = (git rev-parse --verify $EndCommit) 2>$null
|
||||
if (-not $endSha) { throw "EndCommit '$EndCommit' not found" }
|
||||
}
|
||||
catch {
|
||||
Write-Err $_
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Info "Collecting commits between $startSha..$endSha (excluding start, including end)."
|
||||
# Get list of commits reachable from end but not from start.
|
||||
# IMPORTANT: In PowerShell, the .. operator creates a numeric/char range. If $startSha and $endSha look like hex strings,
|
||||
# `$startSha..$endSha` must be passed as a single string argument.
|
||||
$rangeArg = "$startSha..$endSha"
|
||||
$commitList = git rev-list $rangeArg
|
||||
|
||||
# Normalize list (filter out empty strings)
|
||||
$normalizedCommits = $commitList | Where-Object { $_ -and $_.Trim() -ne '' }
|
||||
$commitCount = ($normalizedCommits | Measure-Object).Count
|
||||
Write-DebugMsg ("Raw commitList length (including blanks): {0}" -f (($commitList | Measure-Object).Count))
|
||||
Write-DebugMsg ("Normalized commit count: {0}" -f $commitCount)
|
||||
if ($commitCount -eq 0) {
|
||||
Write-Warn "No commits found in specified range ($startSha..$endSha)."; exit 0
|
||||
}
|
||||
Write-DebugMsg ("First 5 commits: {0}" -f (($normalizedCommits | Select-Object -First 5) -join ', '))
|
||||
|
||||
<#
|
||||
Extract PR numbers from commits.
|
||||
Patterns handled:
|
||||
1. Merge commits: 'Merge pull request #12345 from ...'
|
||||
2. Squash commits: 'Some feature change (#12345)' (GitHub default squash format)
|
||||
We collect both. If a commit matches both (unlikely), it's deduped later.
|
||||
#>
|
||||
# Extract PR numbers from merge or squash commits
|
||||
$mergeCommits = @()
|
||||
foreach ($c in $normalizedCommits) {
|
||||
$subject = git show -s --format=%s $c
|
||||
$matched = $false
|
||||
# Pattern 1: Traditional merge commit
|
||||
if ($subject -match 'Merge pull request #([0-9]+) ') {
|
||||
$prNumber = [int]$matches[1]
|
||||
$mergeCommits += [PSCustomObject]@{ Sha = $c; Pr = $prNumber; Subject = $subject; Pattern = 'merge' }
|
||||
Write-DebugMsg "Matched merge PR #$prNumber in commit $c"
|
||||
$matched = $true
|
||||
}
|
||||
# Pattern 2: Squash merge subject line with ' (#12345)' at end (allow possible whitespace before paren)
|
||||
if ($subject -match '\(#([0-9]+)\)$') {
|
||||
$prNumber2 = [int]$matches[1]
|
||||
# Avoid duplicate object if pattern 1 already captured same number for same commit
|
||||
if (-not ($mergeCommits | Where-Object { $_.Sha -eq $c -and $_.Pr -eq $prNumber2 })) {
|
||||
$mergeCommits += [PSCustomObject]@{ Sha = $c; Pr = $prNumber2; Subject = $subject; Pattern = 'squash' }
|
||||
Write-DebugMsg "Matched squash PR #$prNumber2 in commit $c"
|
||||
}
|
||||
$matched = $true
|
||||
}
|
||||
if (-not $matched) {
|
||||
Write-DebugMsg "No PR pattern in commit $c : $subject"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $mergeCommits -or $mergeCommits.Count -eq 0) {
|
||||
Write-Warn "No merge commits with PR numbers found in range."; exit 0
|
||||
}
|
||||
|
||||
# Deduplicate PR numbers (in case of revert or merges across branches)
|
||||
$prNumbers = $mergeCommits | Select-Object -ExpandProperty Pr -Unique | Sort-Object
|
||||
Write-Info ("Found {0} unique PRs: {1}" -f $prNumbers.Count, ($prNumbers -join ', '))
|
||||
Write-DebugMsg ("Total merge commits examined: {0}" -f $mergeCommits.Count)
|
||||
|
||||
# Build a map of PR number → list of commit SHAs (for co-author extraction)
|
||||
$prToCommits = @{}
|
||||
foreach ($mc in $mergeCommits) {
|
||||
if (-not $prToCommits.ContainsKey($mc.Pr)) {
|
||||
$prToCommits[$mc.Pr] = @()
|
||||
}
|
||||
$prToCommits[$mc.Pr] += $mc.Sha
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get all authors (including co-authors) for a set of commits via GitHub GraphQL API.
|
||||
.DESCRIPTION
|
||||
Uses the Commit.authors field in GitHub's GraphQL API which natively includes
|
||||
co-authors (from Co-authored-by trailers). Returns GitHub usernames (login)
|
||||
without any email parsing — GitHub resolves the association for us.
|
||||
|
||||
NOTE: For squash merges this captures all co-authors correctly because GitHub
|
||||
preserves Co-authored-by trailers in the squash commit. For traditional merge
|
||||
commits, only the merger's author is returned — co-authors on individual PR
|
||||
commits are not traversed. This is acceptable because PowerToys primarily uses
|
||||
squash merging.
|
||||
#>
|
||||
function Get-CommitAuthors {
|
||||
param(
|
||||
[string[]]$CommitShas,
|
||||
[string]$RepoFullName = "microsoft/PowerToys"
|
||||
)
|
||||
$parts = $RepoFullName -split '/'
|
||||
$owner = $parts[0]
|
||||
$repoName = $parts[1]
|
||||
$allAuthors = @()
|
||||
|
||||
foreach ($sha in $CommitShas) {
|
||||
try {
|
||||
$query = "{ repository(owner: `"$owner`", name: `"$repoName`") { object(expression: `"$sha`") { ... on Commit { authors(first: 20) { nodes { user { login } name } } } } } }"
|
||||
$result = gh api graphql -f query="$query" 2>$null | ConvertFrom-Json
|
||||
$nodes = $result.data.repository.object.authors.nodes
|
||||
if ($nodes) {
|
||||
foreach ($node in $nodes) {
|
||||
if ($node.user -and $node.user.login) {
|
||||
$allAuthors += $node.user.login
|
||||
} else {
|
||||
# User without a GitHub account (rare) — use display name as fallback
|
||||
Write-DebugMsg "Commit $sha has an author without GitHub account: $($node.name)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-DebugMsg "GraphQL authors query failed for commit ${sha}: $_"
|
||||
}
|
||||
}
|
||||
|
||||
return $allAuthors | Select-Object -Unique
|
||||
}
|
||||
|
||||
# Query GitHub for each PR
|
||||
$prDetails = @()
|
||||
function Get-CopilotSummaryFromPrJson {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]$PrJson,
|
||||
[switch]$VerboseMode
|
||||
)
|
||||
# Returns a hashtable with Summary and Source keys.
|
||||
$result = @{ Summary = ""; Source = "" }
|
||||
if (-not $PrJson) { return $result }
|
||||
|
||||
$candidateAuthors = @(
|
||||
'github-copilot[bot]', 'github-copilot', 'copilot'
|
||||
)
|
||||
|
||||
# 1. Reviews (preferred) – pick the LONGEST valid Copilot body, not the most recent
|
||||
$reviews = $PrJson.reviews
|
||||
if ($reviews) {
|
||||
$copilotReviews = $reviews | Where-Object {
|
||||
($candidateAuthors -contains $_.author.login -or $_.author.login -like '*copilot*') -and $_.body -and $_.body.Trim() -ne ''
|
||||
}
|
||||
if ($copilotReviews) {
|
||||
$longest = $copilotReviews | Sort-Object { $_.body.Length } -Descending | Select-Object -First 1
|
||||
if ($longest) {
|
||||
$body = $longest.body
|
||||
$norm = ($body -replace "`r", '') -replace "`n", ' '
|
||||
$norm = $norm -replace '\s+', ' '
|
||||
$result.Summary = $norm
|
||||
$result.Source = 'review'
|
||||
if ($VerboseMode) { Write-DebugMsg "Selected Copilot review length=$($body.Length) (longest)." }
|
||||
return $result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Comments fallback (some repos surface Copilot summaries as PR comments rather than review objects)
|
||||
if ($null -eq $PrJson.comments) {
|
||||
try {
|
||||
# Lazy fetch comments only if needed
|
||||
$commentsJson = gh pr view $PrJson.number --repo $Repo --json comments 2>$null | ConvertFrom-Json
|
||||
if ($commentsJson -and $commentsJson.comments) {
|
||||
$PrJson | Add-Member -NotePropertyName comments -NotePropertyValue $commentsJson.comments -Force
|
||||
}
|
||||
} catch {
|
||||
if ($VerboseMode) { Write-DebugMsg "Failed to fetch comments for PR #$($PrJson.number): $_" }
|
||||
}
|
||||
}
|
||||
if ($PrJson.comments) {
|
||||
$copilotComments = $PrJson.comments | Where-Object {
|
||||
($candidateAuthors -contains $_.author.login -or $_.author.login -like '*copilot*') -and $_.body -and $_.body.Trim() -ne ''
|
||||
}
|
||||
if ($copilotComments) {
|
||||
$longestC = $copilotComments | Sort-Object { $_.body.Length } -Descending | Select-Object -First 1
|
||||
if ($longestC) {
|
||||
$body = $longestC.body
|
||||
$norm = ($body -replace "`r", '') -replace "`n", ' '
|
||||
$norm = $norm -replace '\s+', ' '
|
||||
$result.Summary = $norm
|
||||
$result.Source = 'comment'
|
||||
if ($VerboseMode) { Write-DebugMsg "Selected Copilot comment length=$($body.Length) (longest)." }
|
||||
return $result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
foreach ($pr in $prNumbers) {
|
||||
Write-Info "Fetching PR #$pr ..."
|
||||
try {
|
||||
# Include comments only if Verbose asked; if not, we lazily pull when reviews are missing
|
||||
$fields = 'number,title,labels,author,url,body,reviews'
|
||||
if ($PSBoundParameters.ContainsKey('Verbose')) { $fields += ',comments' }
|
||||
$json = gh pr view $pr --repo $Repo --json $fields 2>$null | ConvertFrom-Json
|
||||
if ($null -eq $json) { throw "Empty response" }
|
||||
|
||||
$copilot = Get-CopilotSummaryFromPrJson -PrJson $json -VerboseMode:($PSBoundParameters.ContainsKey('Verbose'))
|
||||
if ($copilot.Summary -and $copilot.Source -and $PSBoundParameters.ContainsKey('Verbose')) {
|
||||
Write-DebugMsg "Copilot summary source=$($copilot.Source) chars=$($copilot.Summary.Length)"
|
||||
} elseif (-not $copilot.Summary) {
|
||||
Write-DebugMsg "No Copilot summary found for PR #$pr"
|
||||
}
|
||||
|
||||
# Filter labels
|
||||
$filteredLabels = $json.labels | Where-Object {
|
||||
($_.name -like "Product-*") -or
|
||||
($_.name -like "Area-*") -or
|
||||
($_.name -like "GitHub*") -or
|
||||
($_.name -like "*Plugin") -or
|
||||
($_.name -like "Issue-*")
|
||||
}
|
||||
$labelNames = ($filteredLabels | ForEach-Object { $_.name }) -join ", "
|
||||
|
||||
$bodyValue = if ($json.body) { ($json.body -replace "`r", '') -replace "`n", ' ' } else { '' }
|
||||
$bodyValue = $bodyValue -replace '\s+', ' '
|
||||
|
||||
# Collect all contributors: PR author + co-authors from commit messages
|
||||
$authorLogin = $json.author.login
|
||||
$allContributors = @($authorLogin)
|
||||
|
||||
# Extract all authors (including co-authors) from associated commits via GitHub GraphQL API
|
||||
if ($prToCommits.ContainsKey([int]$pr)) {
|
||||
$commitAuthors = Get-CommitAuthors -CommitShas $prToCommits[[int]$pr] -RepoFullName $Repo
|
||||
if ($commitAuthors) {
|
||||
$allContributors += $commitAuthors
|
||||
}
|
||||
}
|
||||
|
||||
# Deduplicate contributors (case-insensitive)
|
||||
$allContributors = $allContributors | Where-Object { $_ } | Sort-Object -Unique
|
||||
|
||||
# Filter to only external contributors (not in member list) for thanks
|
||||
$externalContributors = @()
|
||||
if ($memberList.Count -gt 0) {
|
||||
$externalContributors = $allContributors | Where-Object { -not ($memberList -contains $_) }
|
||||
} else {
|
||||
$externalContributors = $allContributors
|
||||
}
|
||||
|
||||
# Author column: all contributors (comma-separated)
|
||||
$authorField = ($allContributors -join ', ')
|
||||
|
||||
# NeedThanks column: comma-separated list of external contributors who
|
||||
# deserve thanks attribution. Empty string means no thanks needed.
|
||||
$needThanksField = ($externalContributors -join ', ')
|
||||
|
||||
$prDetails += [PSCustomObject]@{
|
||||
Id = $json.number
|
||||
Title = $json.title
|
||||
Labels = $labelNames
|
||||
Author = $authorField
|
||||
Url = $json.url
|
||||
Body = $bodyValue
|
||||
CopilotSummary = $copilot.Summary
|
||||
NeedThanks = $needThanksField
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$err = $_
|
||||
Write-Warn ("Failed to fetch PR #{0}: {1}" -f $pr, $err)
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $prDetails) { Write-Warn "No PR details fetched."; exit 0 }
|
||||
|
||||
# Sort by Labels like original script (first label alphabetical)
|
||||
$sorted = $prDetails | Sort-Object { ($_.Labels -split ',')[0] }
|
||||
|
||||
# Output JSON raw (optional)
|
||||
$sorted | ConvertTo-Json -Depth 6 | Out-File -Encoding UTF8 $OutputJson
|
||||
|
||||
Write-Info "Saving CSV to $OutputCsv ..."
|
||||
$sorted | Export-Csv $OutputCsv -NoTypeInformation
|
||||
Write-Host "✅ Done. Generated $($prDetails.Count) PR rows." -ForegroundColor Green
|
||||
@@ -1,80 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Find a commit on a branch that has the same subject line as a reference commit.
|
||||
|
||||
.DESCRIPTION
|
||||
Given a commit SHA (often from a release tag) and a branch name, this script
|
||||
resolves the reference commit's subject, then searches the branch history for
|
||||
commits with the exact same subject line. Useful when the release tag commit
|
||||
is not reachable from your current branch history.
|
||||
|
||||
.PARAMETER Commit
|
||||
The reference commit SHA or ref (e.g., v0.96.1 or a full SHA).
|
||||
|
||||
.PARAMETER Branch
|
||||
The branch to search (e.g., stable or main). Defaults to stable.
|
||||
|
||||
.PARAMETER RepoPath
|
||||
Path to the local repo. Defaults to current directory.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh ./find-commit-by-title.ps1 -Commit b62f6421845f7e5c92b8186868d98f46720db442 -Branch stable
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Commit,
|
||||
[string]$Branch = "stable",
|
||||
[string]$RepoPath = "."
|
||||
)
|
||||
|
||||
function Write-Info($msg) { Write-Host $msg -ForegroundColor Cyan }
|
||||
function Write-Err($msg) { Write-Host $msg -ForegroundColor Red }
|
||||
|
||||
Push-Location $RepoPath
|
||||
try {
|
||||
Write-Info "Fetching latest '$Branch' from origin (with tags)..."
|
||||
git fetch origin $Branch --tags | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "git fetch origin $Branch --tags failed" }
|
||||
|
||||
$commitSha = (git rev-parse --verify $Commit) 2>$null
|
||||
if (-not $commitSha) { throw "Commit '$Commit' not found" }
|
||||
|
||||
$subject = (git show -s --format=%s $commitSha) 2>$null
|
||||
if (-not $subject) { throw "Unable to read subject for '$commitSha'" }
|
||||
|
||||
$branchRef = $Branch
|
||||
$branchSha = (git rev-parse --verify $branchRef) 2>$null
|
||||
if (-not $branchSha) {
|
||||
$branchRef = "origin/$Branch"
|
||||
$branchSha = (git rev-parse --verify $branchRef) 2>$null
|
||||
}
|
||||
if (-not $branchSha) { throw "Branch '$Branch' not found" }
|
||||
|
||||
Write-Info "Reference commit: $commitSha"
|
||||
Write-Info "Reference title: $subject"
|
||||
Write-Info "Searching branch: $branchRef"
|
||||
|
||||
$matches = git log $branchRef --format="%H|%s" | Where-Object { $_ -match '\|' }
|
||||
$results = @()
|
||||
foreach ($line in $matches) {
|
||||
$parts = $line -split '\|', 2
|
||||
if ($parts.Count -eq 2 -and $parts[1] -eq $subject) {
|
||||
$results += [PSCustomObject]@{ Sha = $parts[0]; Title = $parts[1] }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $results -or $results.Count -eq 0) {
|
||||
Write-Info "No matching commit found on $branchRef for the given title."
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Info ("Found {0} matching commit(s):" -f $results.Count)
|
||||
$results | ForEach-Object { Write-Host ("{0} {1}" -f $_.Sha, $_.Title) }
|
||||
}
|
||||
catch {
|
||||
Write-Err $_
|
||||
exit 1
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Group PR rows by their Labels column and emit per-label CSV files.
|
||||
|
||||
.DESCRIPTION
|
||||
Reads a milestone PR CSV (usually produced by dump-prs-information / dump-prs-since-commit scripts),
|
||||
splits rows by label list, normalizes/sorts individual labels, and writes one CSV per unique label combination.
|
||||
Each output preserves the original row ordering within that subset and column order from the source.
|
||||
|
||||
.PARAMETER CsvPath
|
||||
Input CSV containing PR rows with a 'Labels' column (comma-separated list).
|
||||
|
||||
.PARAMETER OutDir
|
||||
Output directory to place grouped CSVs (created if missing). Default: 'grouped_csv'.
|
||||
|
||||
.NOTES
|
||||
Label combinations are joined using ' | ' when multiple labels present. Filenames are sanitized (invalid characters,
|
||||
whitespace collapsed) and truncated to <= 120 characters.
|
||||
#>
|
||||
param(
|
||||
[string]$CsvPath = "sorted_prs.csv",
|
||||
[string]$OutDir = "grouped_csv"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-Info($msg) { Write-Host "[info] $msg" -ForegroundColor Cyan }
|
||||
function Write-Warn($msg) { Write-Host "[warn] $msg" -ForegroundColor Yellow }
|
||||
|
||||
if (-not (Test-Path -LiteralPath $CsvPath)) { throw "CSV not found: $CsvPath" }
|
||||
|
||||
Write-Info "Reading CSV: $CsvPath"
|
||||
$rows = Import-Csv -LiteralPath $CsvPath
|
||||
Write-Info ("Loaded {0} rows" -f $rows.Count)
|
||||
|
||||
function ConvertTo-SafeFileName {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Name
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace($Name)) { return 'Unnamed' }
|
||||
$s = $Name -replace '[<>:"/\\|?*]', '-' # invalid path chars
|
||||
$s = $s -replace '\s+', '-' # spaces to dashes
|
||||
$s = $s -replace '-{2,}', '-' # collapse dashes
|
||||
$s = $s.Trim('-')
|
||||
if ($s.Length -gt 120) { $s = $s.Substring(0,120).Trim('-') }
|
||||
if ([string]::IsNullOrWhiteSpace($s)) { return 'Unnamed' }
|
||||
return $s
|
||||
}
|
||||
|
||||
# Build groups keyed by normalized, sorted label combinations. Preserve original CSV row order.
|
||||
$groups = @{}
|
||||
foreach ($row in $rows) {
|
||||
$labelsRaw = $row.Labels
|
||||
if ([string]::IsNullOrWhiteSpace($labelsRaw)) {
|
||||
$labelParts = @('Unlabeled')
|
||||
} else {
|
||||
$parts = $labelsRaw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
if (-not $parts -or $parts.Count -eq 0) { $labelParts = @('Unlabeled') }
|
||||
else { $labelParts = $parts | Sort-Object }
|
||||
}
|
||||
|
||||
$key = ($labelParts -join ' | ')
|
||||
if (-not $groups.ContainsKey($key)) { $groups[$key] = New-Object System.Collections.ArrayList }
|
||||
[void]$groups[$key].Add($row)
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $OutDir)) {
|
||||
Write-Info "Creating output directory: $OutDir"
|
||||
New-Item -ItemType Directory -Path $OutDir | Out-Null
|
||||
}
|
||||
|
||||
Write-Info ("Generating {0} grouped CSV file(s) into: {1}" -f $groups.Count, $OutDir)
|
||||
|
||||
foreach ($key in $groups.Keys) {
|
||||
$labelParts = if ($key -eq 'Unlabeled') { @('Unlabeled') } else { $key -split '\s\|\s' }
|
||||
$safeName = ($labelParts | ForEach-Object { ConvertTo-SafeFileName -Name $_ }) -join '-'
|
||||
$filePath = Join-Path $OutDir ("$safeName.csv")
|
||||
|
||||
# Keep same columns and order
|
||||
$groups[$key] | Export-Csv -LiteralPath $filePath -NoTypeInformation -Encoding UTF8
|
||||
}
|
||||
|
||||
Write-Info "Done. Sample output files:"
|
||||
Get-ChildItem -LiteralPath $OutDir | Select-Object -First 10 Name | Format-Table -HideTableHeaders
|
||||
@@ -1,334 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Prepares the binary assets for a PowerToys GitHub release: downloads the
|
||||
four installers (per-user/per-machine x x64/arm64) and the symbol archives
|
||||
from an ADO pipeline build, computes SHA256 hashes, and emits the
|
||||
"Installer Hashes" markdown table.
|
||||
|
||||
.DESCRIPTION
|
||||
Given an ADO Dart pipeline build id (e.g. from
|
||||
https://microsoft.visualstudio.com/Dart/_build/results?buildId=NNN),
|
||||
downloads the four installer EXEs and the per-arch symbol zips into a
|
||||
single per-version folder, then writes a hashes.md alongside them with a
|
||||
markdown table ready to paste into the GitHub release notes.
|
||||
|
||||
Requires: az login (Azure CLI authenticated), az devops extension.
|
||||
|
||||
.EXAMPLE
|
||||
.\prepare-release-assets.ps1 -BuildId 145505247
|
||||
.\prepare-release-assets.ps1 -BuildId 145505247 -OutputFolder D:\Releases
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$BuildId,
|
||||
|
||||
[string]$OutputFolder = "$env:USERPROFILE\Downloads",
|
||||
|
||||
[string]$Organization = "https://dev.azure.com/microsoft",
|
||||
[string]$Project = "Dart",
|
||||
|
||||
[string]$GitHubRepo = "microsoft/PowerToys"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$env:AZURE_CORE_NO_PROMPT = "true"
|
||||
|
||||
# --- Helpers -----------------------------------------------------------------
|
||||
|
||||
# Invoke an `az` CLI command and capture stderr in $script:LastAzError so
|
||||
# callers can surface the underlying message (expired login, blocked extension,
|
||||
# tenant policy, ...) instead of swallowing it with `2>$null`.
|
||||
function Invoke-Az {
|
||||
$tmpErr = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
$output = & az @args 2>$tmpErr
|
||||
# Get-Content -Raw returns $null for an empty file, and calling .Trim()
|
||||
# on $null throws under $ErrorActionPreference = 'Stop' -- which would
|
||||
# turn every successful (no-stderr) az call into a fatal error. Guard
|
||||
# explicitly so $script:LastAzError is always a (possibly empty) string.
|
||||
$rawErr = Get-Content $tmpErr -Raw -ErrorAction SilentlyContinue
|
||||
$script:LastAzError = if ($null -eq $rawErr) { '' } else { $rawErr.Trim() }
|
||||
return $output
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tmpErr -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Build an ADO artifact download URL from scratch instead of regex-replacing
|
||||
# the URL returned by `az pipelines runs artifact list`. Preserves any other
|
||||
# query parameters and only swaps `format` and `subPath`, so we don't break if
|
||||
# the upstream URL shape ever changes.
|
||||
function Get-ArtifactDownloadUrl {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$BaseUrl,
|
||||
[Parameter(Mandatory)][string]$SubPath,
|
||||
[Parameter(Mandatory)][ValidateSet('file', 'zip')][string]$Format
|
||||
)
|
||||
$encodedSubPath = [Uri]::EscapeDataString($SubPath)
|
||||
$idx = $BaseUrl.IndexOf('?')
|
||||
if ($idx -lt 0) {
|
||||
return "${BaseUrl}?format=${Format}&subPath=${encodedSubPath}"
|
||||
}
|
||||
$base = $BaseUrl.Substring(0, $idx)
|
||||
$kept = $BaseUrl.Substring($idx + 1) -split '&' | Where-Object {
|
||||
$_ -and -not ($_ -match '^(format|subPath)=')
|
||||
}
|
||||
$kept = @($kept) + @("format=$Format", "subPath=$encodedSubPath")
|
||||
return "${base}?$($kept -join '&')"
|
||||
}
|
||||
|
||||
# Download a single ADO artifact file with bearer auth and a small retry/backoff
|
||||
# loop. A transient network blip on a ~200 MB installer or symbol zip otherwise
|
||||
# aborts the entire release-prep run.
|
||||
function Invoke-AdoDownload {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Url,
|
||||
[Parameter(Mandatory)][string]$DestPath,
|
||||
[Parameter(Mandatory)][string]$Token,
|
||||
[int]$MaxAttempts = 3
|
||||
)
|
||||
$lastError = $null
|
||||
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
|
||||
$webClient = New-Object System.Net.WebClient
|
||||
$webClient.Headers.Add("Authorization", "Bearer $Token")
|
||||
try {
|
||||
$webClient.DownloadFile($Url, $DestPath)
|
||||
return
|
||||
}
|
||||
catch {
|
||||
$lastError = $_
|
||||
if (Test-Path $DestPath) {
|
||||
Remove-Item $DestPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($attempt -lt $MaxAttempts) {
|
||||
$backoffSec = [int][Math]::Pow(2, $attempt) # 2, 4, 8 ...
|
||||
Write-Host " Attempt $attempt failed: $($_.Exception.Message). Retrying in ${backoffSec}s..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds $backoffSec
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$webClient.Dispose()
|
||||
}
|
||||
}
|
||||
throw "Download failed after $MaxAttempts attempts. Last error: $($lastError.Exception.Message)`nURL: $Url"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Work around broken az extensions: if the default extension dir has
|
||||
# inaccessible files, redirect to a clean directory.
|
||||
$defaultExtDir = "$env:USERPROFILE\.azure\cliextensions"
|
||||
if (-not $env:AZURE_EXTENSION_DIR -and (Test-Path $defaultExtDir)) {
|
||||
$broken = Get-ChildItem "$defaultExtDir\*\*.dist-info" -Directory -ErrorAction SilentlyContinue | Where-Object {
|
||||
try { [System.IO.Directory]::GetFiles($_.FullName) | Out-Null; $false } catch { $true }
|
||||
}
|
||||
if ($broken) {
|
||||
$cleanDir = "$env:USERPROFILE\.azure\cliextensions_clean"
|
||||
Write-Host " Detected broken az extension, redirecting to $cleanDir" -ForegroundColor Yellow
|
||||
$env:AZURE_EXTENSION_DIR = $cleanDir
|
||||
if (-not (Test-Path $cleanDir)) { New-Item -ItemType Directory -Path $cleanDir -Force | Out-Null }
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure azure-devops extension is installed
|
||||
$ext = Invoke-Az extension list --query "[?name=='azure-devops']" -o tsv
|
||||
if (-not $ext) {
|
||||
Write-Host "Installing azure-devops extension..." -ForegroundColor Yellow
|
||||
Invoke-Az extension add --name azure-devops --yes | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to install azure-devops extension. (az: $script:LastAzError)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Configure az devops defaults
|
||||
Invoke-Az devops configure --defaults organization=$Organization project=$Project | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to configure az devops defaults. (az: $script:LastAzError)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Step 1: Get build info to determine version ---
|
||||
Write-Host "Fetching build $BuildId info..." -ForegroundColor Cyan
|
||||
$buildJson = Invoke-Az pipelines build show --id $BuildId --output json
|
||||
if (-not $buildJson) {
|
||||
Write-Error "Could not fetch build $BuildId. Are you logged in (az login)? (az: $script:LastAzError)"
|
||||
exit 1
|
||||
}
|
||||
$build = $buildJson | ConvertFrom-Json
|
||||
|
||||
$versionParam = $build.templateParameters.VersionNumber
|
||||
if (-not $versionParam) {
|
||||
Write-Error "Could not determine version from build $BuildId"
|
||||
exit 1
|
||||
}
|
||||
Write-Host " Version: $versionParam" -ForegroundColor DarkGray
|
||||
|
||||
# --- Step 2: Get artifact metadata once ---
|
||||
Write-Host "Fetching artifact metadata..." -ForegroundColor Cyan
|
||||
$artifactsJson = Invoke-Az pipelines runs artifact list --run-id $BuildId --output json
|
||||
if (-not $artifactsJson) {
|
||||
Write-Error "Could not list artifacts for build $BuildId. (az: $script:LastAzError)"
|
||||
exit 1
|
||||
}
|
||||
$artifacts = $artifactsJson | ConvertFrom-Json
|
||||
|
||||
# --- Step 3: Prepare destination folder ---
|
||||
$destFolder = Join-Path $OutputFolder "PowerToys-v$versionParam"
|
||||
if (-not (Test-Path $destFolder)) {
|
||||
New-Item -ItemType Directory -Path $destFolder -Force | Out-Null
|
||||
}
|
||||
Write-Host " Destination: $destFolder" -ForegroundColor DarkGray
|
||||
|
||||
# --- Step 4: Get an ADO access token once ---
|
||||
$token = Invoke-Az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" --query accessToken -o tsv
|
||||
if (-not $token) {
|
||||
Write-Error "Failed to acquire ADO access token. Run 'az login' first. (az: $script:LastAzError)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Step 5: Define the four installers to download ---
|
||||
$targets = @(
|
||||
[pscustomobject]@{ Description = "Per user - x64"; Scope = "perUser"; Arch = "x64"; Artifact = "build-x64-Release"; FileName = "PowerToysUserSetup-$versionParam-x64.exe" }
|
||||
[pscustomobject]@{ Description = "Per user - ARM64"; Scope = "perUser"; Arch = "arm64"; Artifact = "build-arm64-Release"; FileName = "PowerToysUserSetup-$versionParam-arm64.exe" }
|
||||
[pscustomobject]@{ Description = "Machine wide - x64"; Scope = "perMachine"; Arch = "x64"; Artifact = "build-x64-Release"; FileName = "PowerToysSetup-$versionParam-x64.exe" }
|
||||
[pscustomobject]@{ Description = "Machine wide - ARM64"; Scope = "perMachine"; Arch = "arm64"; Artifact = "build-arm64-Release"; FileName = "PowerToysSetup-$versionParam-arm64.exe" }
|
||||
)
|
||||
|
||||
# --- Step 6: Download each installer (skip if already present) ---
|
||||
foreach ($t in $targets) {
|
||||
$destPath = Join-Path $destFolder $t.FileName
|
||||
|
||||
if (Test-Path $destPath) {
|
||||
$sizeMB = [math]::Round((Get-Item $destPath).Length / 1MB, 1)
|
||||
Write-Host "[skip] $($t.FileName) already exists ($sizeMB MB)" -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
|
||||
$artifact = $artifacts | Where-Object { $_.name -eq $t.Artifact }
|
||||
if (-not $artifact) {
|
||||
Write-Error "Artifact '$($t.Artifact)' not found in build $BuildId. Available: $(($artifacts | ForEach-Object name) -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$fileUrl = Get-ArtifactDownloadUrl -BaseUrl $artifact.resource.downloadUrl -SubPath "/$($t.FileName)" -Format file
|
||||
|
||||
Write-Host "Downloading $($t.FileName) ..." -ForegroundColor Cyan
|
||||
try {
|
||||
Invoke-AdoDownload -Url $fileUrl -DestPath $destPath -Token $token
|
||||
}
|
||||
catch {
|
||||
Write-Error "Download failed for $($t.FileName): $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$sizeMB = [math]::Round((Get-Item $destPath).Length / 1MB, 1)
|
||||
Write-Host " Saved ($sizeMB MB)" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# --- Step 6b: Download symbols (one zip per arch) ---
|
||||
$symbolTargets = @(
|
||||
[pscustomobject]@{ Arch = "x64"; Artifact = "build-x64-Release"; SubPath = "/symbols-x64" }
|
||||
[pscustomobject]@{ Arch = "arm64"; Artifact = "build-arm64-Release"; SubPath = "/symbols-arm64" }
|
||||
)
|
||||
|
||||
foreach ($s in $symbolTargets) {
|
||||
$finalZip = Join-Path $destFolder "symbols-$($s.Arch).zip"
|
||||
if (Test-Path $finalZip) {
|
||||
$sizeMB = [math]::Round((Get-Item $finalZip).Length / 1MB, 1)
|
||||
Write-Host "[skip] symbols-$($s.Arch).zip already exists ($sizeMB MB)" -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
|
||||
$artifact = $artifacts | Where-Object { $_.name -eq $s.Artifact }
|
||||
if (-not $artifact) {
|
||||
Write-Error "Artifact '$($s.Artifact)' not found in build $BuildId."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Symbols are downloaded as a folder => keep format=zip and append subPath
|
||||
$symbolsUrl = Get-ArtifactDownloadUrl -BaseUrl $artifact.resource.downloadUrl -SubPath $s.SubPath -Format zip
|
||||
|
||||
$tmpZip = Join-Path ([System.IO.Path]::GetTempPath()) ("ptsym-$($s.Arch)-$([Guid]::NewGuid().ToString('N')).zip")
|
||||
$tmpExtract = Join-Path ([System.IO.Path]::GetTempPath()) ("ptsym-$($s.Arch)-$([Guid]::NewGuid().ToString('N'))")
|
||||
$stageRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("ptsym-stage-$([Guid]::NewGuid().ToString('N'))")
|
||||
|
||||
try {
|
||||
Write-Host "Downloading symbols-$($s.Arch).zip ..." -ForegroundColor Cyan
|
||||
try {
|
||||
Invoke-AdoDownload -Url $symbolsUrl -DestPath $tmpZip -Token $token
|
||||
}
|
||||
catch {
|
||||
Write-Error "Symbols download failed for $($s.Arch): $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host " Extracting..." -ForegroundColor DarkGray
|
||||
Expand-Archive -Path $tmpZip -DestinationPath $tmpExtract -Force
|
||||
|
||||
# Walk down while the current dir holds exactly one subfolder and no files.
|
||||
$current = Get-Item $tmpExtract
|
||||
while ($true) {
|
||||
$children = Get-ChildItem -LiteralPath $current.FullName -Force
|
||||
$subDirs = @($children | Where-Object { $_.PSIsContainer })
|
||||
$files = @($children | Where-Object { -not $_.PSIsContainer })
|
||||
if ($subDirs.Count -eq 1 -and $files.Count -eq 0) {
|
||||
$current = $subDirs[0]
|
||||
}
|
||||
else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Stage to a folder named symbols-<arch> so the zip extracts to that name.
|
||||
$stageInner = Join-Path $stageRoot "symbols-$($s.Arch)"
|
||||
New-Item -ItemType Directory -Path $stageInner -Force | Out-Null
|
||||
Get-ChildItem -LiteralPath $current.FullName -Force | ForEach-Object {
|
||||
Copy-Item -LiteralPath $_.FullName -Destination $stageInner -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Host " Repacking to $finalZip ..." -ForegroundColor DarkGray
|
||||
if (Test-Path $finalZip) { Remove-Item $finalZip -Force }
|
||||
Compress-Archive -Path "$stageInner\*" -DestinationPath $finalZip -CompressionLevel Optimal
|
||||
|
||||
$sizeMB = [math]::Round((Get-Item $finalZip).Length / 1MB, 1)
|
||||
Write-Host " Saved symbols-$($s.Arch).zip ($sizeMB MB)" -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
# Don't leave a half-built zip behind if anything in the pipeline blew up.
|
||||
if (Test-Path $finalZip) { Remove-Item $finalZip -Force -ErrorAction SilentlyContinue }
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tmpZip -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $tmpExtract -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $stageRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# --- Step 7: Compute SHA256 and build markdown ---
|
||||
Write-Host "`nComputing SHA256 hashes..." -ForegroundColor Cyan
|
||||
|
||||
$sb = [System.Text.StringBuilder]::new()
|
||||
[void]$sb.AppendLine("## Installer Hashes")
|
||||
[void]$sb.AppendLine("")
|
||||
[void]$sb.AppendLine("| Description | Filename | sha256 hash |")
|
||||
[void]$sb.AppendLine("| --- | --- | --- |")
|
||||
|
||||
foreach ($t in $targets) {
|
||||
$destPath = Join-Path $destFolder $t.FileName
|
||||
$hash = (Get-FileHash -Path $destPath -Algorithm SHA256).Hash.ToUpper()
|
||||
[void]$sb.AppendLine("| $($t.Description) | $($t.FileName) | $hash |")
|
||||
Write-Host " $($t.FileName) $hash" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
$markdown = $sb.ToString()
|
||||
$mdPath = Join-Path $destFolder "hashes.md"
|
||||
Set-Content -Path $mdPath -Value $markdown -Encoding UTF8
|
||||
|
||||
Write-Host "`nMarkdown written to: $mdPath" -ForegroundColor Green
|
||||
Write-Host "`n----- Installer Hashes -----`n" -ForegroundColor Yellow
|
||||
Write-Host $markdown
|
||||
|
||||
Write-Host "Draft a new GitHub release at: https://github.com/$GitHubRepo/releases/new?tag=v$versionParam" -ForegroundColor Green
|
||||
21
.github/skills/winmd-api-search/LICENSE.txt
vendored
21
.github/skills/winmd-api-search/LICENSE.txt
vendored
@@ -1,21 +0,0 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
192
.github/skills/winmd-api-search/SKILL.md
vendored
192
.github/skills/winmd-api-search/SKILL.md
vendored
@@ -1,192 +0,0 @@
|
||||
---
|
||||
name: winmd-api-search
|
||||
description: 'Find and explore Windows desktop APIs. Use when building features that need platform capabilities — camera, file access, notifications, UI controls, AI/ML, sensors, networking, etc. Discovers the right API for a task and retrieves full type details (methods, properties, events, enumeration values).'
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# WinMD API Search
|
||||
|
||||
This skill helps you find the right Windows API for any capability and get its full details. It searches a local cache of all WinMD metadata from:
|
||||
|
||||
- **Windows Platform SDK** — all `Windows.*` WinRT APIs (always available, no restore needed)
|
||||
- **WinAppSDK / WinUI** — bundled as a baseline in the cache generator (always available, no restore needed)
|
||||
- **NuGet packages** — any additional packages in restored projects that contain `.winmd` files
|
||||
- **Project-output WinMD** — class libraries (C++/WinRT, C#) that produce `.winmd` as build output
|
||||
|
||||
Even on a fresh clone with no restore or build, you still get full Platform SDK + WinAppSDK coverage.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- User wants to build a feature and you need to find which API provides that capability
|
||||
- User asks "how do I do X?" where X involves a platform feature (camera, files, notifications, sensors, AI, etc.)
|
||||
- You need the exact methods, properties, events, or enumeration values of a type before writing code
|
||||
- You're unsure which control, class, or interface to use for a UI or system task
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **.NET SDK 8.0 or later** — required to build the cache generator. Install from [dotnet.microsoft.com](https://dotnet.microsoft.com/download) if not available.
|
||||
|
||||
## Cache Setup (Required Before First Use)
|
||||
|
||||
All query and search commands read from a local JSON cache. **You must generate the cache before running any queries.**
|
||||
|
||||
```powershell
|
||||
# All projects in the repo (recommended for first run)
|
||||
.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1
|
||||
|
||||
# Single project
|
||||
.\.github\skills\winmd-api-search\scripts\Update-WinMdCache.ps1 -ProjectDir <project-folder>
|
||||
```
|
||||
|
||||
No project restore or build is needed for baseline coverage (Platform SDK + WinAppSDK). For additional NuGet packages, the project needs `dotnet restore` (which generates `project.assets.json`) or a `packages.config` file.
|
||||
|
||||
Cache is stored at `Generated Files\winmd-cache\`, deduplicated per-package+version.
|
||||
|
||||
### What gets indexed
|
||||
|
||||
| Source | When available |
|
||||
|--------|----------------|
|
||||
| Windows Platform SDK | Always (reads from local SDK install) |
|
||||
| WinAppSDK (latest) | Always (bundled as baseline in cache generator) |
|
||||
| WinAppSDK Runtime | When installed on the system (detected via `Get-AppxPackage`) |
|
||||
| Project NuGet packages | After `dotnet restore` or with `packages.config` |
|
||||
| Project-output `.winmd` | After project build (class libraries that produce WinMD) |
|
||||
|
||||
> **Note:** This cache directory should be in `.gitignore` — it's generated, not source.
|
||||
|
||||
## How to Use
|
||||
|
||||
Pick the path that matches the situation:
|
||||
|
||||
---
|
||||
|
||||
### Discover — "I don't know which API to use"
|
||||
|
||||
The user describes a capability in their own words. You need to find the right API.
|
||||
|
||||
**0. Ensure the cache exists**
|
||||
|
||||
If the cache hasn't been generated yet, run `Update-WinMdCache.ps1` first — see [Cache Setup](#cache-setup-required-before-first-use) above.
|
||||
|
||||
**1. Translate user language → search keywords**
|
||||
|
||||
Map the user's daily language to programming terms. Try multiple variations:
|
||||
|
||||
| User says | Search keywords to try (in order) |
|
||||
|-----------|-----------------------------------|
|
||||
| "take a picture" | `camera`, `capture`, `photo`, `MediaCapture` |
|
||||
| "load from disk" | `file open`, `picker`, `FileOpen`, `StorageFile` |
|
||||
| "describe what's in it" | `image description`, `Vision`, `Recognition` |
|
||||
| "show a popup" | `dialog`, `flyout`, `popup`, `ContentDialog` |
|
||||
| "drag and drop" | `drag`, `drop`, `DragDrop` |
|
||||
| "save settings" | `settings`, `ApplicationData`, `LocalSettings` |
|
||||
|
||||
Start with simple everyday words. If results are weak or irrelevant, try the more technical variation.
|
||||
|
||||
**2. Run searches**
|
||||
|
||||
```powershell
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action search -Query "<keyword>"
|
||||
```
|
||||
|
||||
This returns ranked namespaces with top matching types and the **JSON file path**.
|
||||
|
||||
If results have **low scores (below 60) or are irrelevant**, fall back to searching online documentation:
|
||||
|
||||
1. Use web search to find the right API on Microsoft Learn, for example:
|
||||
- `site:learn.microsoft.com/uwp/api <capability keywords>` for `Windows.*` APIs
|
||||
- `site:learn.microsoft.com/windows/windows-app-sdk/api/winrt <capability keywords>` for `Microsoft.*` WinAppSDK APIs
|
||||
2. Read the documentation pages to identify which type matches the user's requirement.
|
||||
3. Once you know the type name, come back and use `-Action members` or `-Action enums` to get the exact local signatures.
|
||||
|
||||
**3. Read the JSON to choose the right API**
|
||||
|
||||
Read the file at the path(s) from the top results. The JSON has all types in that namespace — full members, signatures, parameters, return types, enumeration values.
|
||||
|
||||
Read and decide which types and members fit the user's requirement.
|
||||
|
||||
**4. Look up official documentation for context**
|
||||
|
||||
The cache contains only signatures — no descriptions or usage guidance. For explanations, examples, and remarks, look up the type on Microsoft Learn:
|
||||
|
||||
| Namespace prefix | Documentation base URL |
|
||||
|-----------------|----------------------|
|
||||
| `Windows.*` | `https://learn.microsoft.com/uwp/api/{fully.qualified.typename}` |
|
||||
| `Microsoft.*` (WinAppSDK) | `https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/{fully.qualified.typename}` |
|
||||
|
||||
For example, `Microsoft.UI.Xaml.Controls.NavigationView` maps to:
|
||||
`https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.navigationview`
|
||||
|
||||
**5. Use the API knowledge to answer or write code**
|
||||
|
||||
---
|
||||
|
||||
### Lookup — "I know the API, show me the details"
|
||||
|
||||
You already know (or suspect) the type or namespace name. Go direct:
|
||||
|
||||
```powershell
|
||||
# Get all members of a known type
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.NavigationView"
|
||||
|
||||
# Get enum values
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
|
||||
|
||||
# List all types in a namespace
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
|
||||
|
||||
# Browse namespaces
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
|
||||
```
|
||||
|
||||
If you need full detail beyond what `-Action members` shows, use `-Action search` to get the JSON file path, then read the JSON file directly.
|
||||
|
||||
---
|
||||
|
||||
### Other Commands
|
||||
|
||||
```powershell
|
||||
# List cached projects
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action projects
|
||||
|
||||
# List packages for a project
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action packages
|
||||
|
||||
# Show stats
|
||||
.\.github\skills\winmd-api-search\scripts\Invoke-WinMdQuery.ps1 -Action stats
|
||||
```
|
||||
|
||||
> If only one project is cached, `-Project` is auto-selected.
|
||||
> If multiple projects exist, add `-Project <name>` (use `-Action projects` to see available names).
|
||||
> In scan mode, manifest names include a short hash suffix to avoid collisions; you can pass the base project name without the suffix if it's unambiguous.
|
||||
|
||||
## Search Scoring
|
||||
|
||||
The search ranks type names and member names against your query:
|
||||
|
||||
| Score | Match type | Example |
|
||||
|-------|-----------|---------|
|
||||
| 100 | Exact name | `Button` → `Button` |
|
||||
| 80 | Starts with | `Navigation` → `NavigationView` |
|
||||
| 60 | Contains | `Dialog` → `ContentDialog` |
|
||||
| 50 | PascalCase initials | `ASB` → `AutoSuggestBox` |
|
||||
| 40 | Multi-keyword AND | `navigation item` → `NavigationViewItem` |
|
||||
| 20 | Fuzzy character match | `NavVw` → `NavigationView` |
|
||||
|
||||
Results are grouped by namespace. Higher-scored namespaces appear first.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| "Cache not found" | Run `Update-WinMdCache.ps1` |
|
||||
| "Multiple projects cached" | Add `-Project <name>` |
|
||||
| "Namespace not found" | Use `-Action namespaces` to list available ones |
|
||||
| "Type not found" | Use fully qualified name (e.g., `Microsoft.UI.Xaml.Controls.Button`) |
|
||||
| Stale after NuGet update | Re-run `Update-WinMdCache.ps1` |
|
||||
| Cache in git history | Add `Generated Files/` to `.gitignore` |
|
||||
|
||||
## References
|
||||
|
||||
- [Windows Platform SDK API reference](https://learn.microsoft.com/uwp/api/) — documentation for `Windows.*` namespaces
|
||||
- [Windows App SDK API reference](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/) — documentation for `Microsoft.*` WinAppSDK namespaces
|
||||
@@ -1,505 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Query WinMD API metadata from cached JSON files.
|
||||
|
||||
.DESCRIPTION
|
||||
Reads pre-built JSON cache of WinMD types, members, and namespaces.
|
||||
The cache is organized per-package (deduplicated) with project manifests
|
||||
that map each project to its referenced packages.
|
||||
|
||||
Supports listing namespaces, types, members, searching, enum value lookup,
|
||||
and listing cached projects/packages.
|
||||
|
||||
.PARAMETER Action
|
||||
The query action to perform:
|
||||
- projects : List cached projects
|
||||
- packages : List packages for a project
|
||||
- stats : Show aggregate statistics for a project
|
||||
- namespaces : List all namespaces (optional -Filter prefix)
|
||||
- types : List types in a namespace (-Namespace required)
|
||||
- members : List members of a type (-TypeName required)
|
||||
- search : Search types and members by name (-Query required)
|
||||
- enums : List enum values (-TypeName required)
|
||||
|
||||
.PARAMETER Project
|
||||
Project name to query. Auto-selected if only one project is cached.
|
||||
Use -Action projects to list available projects.
|
||||
|
||||
.PARAMETER Namespace
|
||||
Namespace to query types from (used with -Action types).
|
||||
|
||||
.PARAMETER TypeName
|
||||
Full type name e.g. "Microsoft.UI.Xaml.Controls.Button" (used with -Action members, enums).
|
||||
|
||||
.PARAMETER Query
|
||||
Search query string (used with -Action search).
|
||||
|
||||
.PARAMETER Filter
|
||||
Optional prefix filter for namespaces (used with -Action namespaces).
|
||||
|
||||
.PARAMETER CacheDir
|
||||
Path to the winmd-cache directory. Defaults to "Generated Files\winmd-cache"
|
||||
relative to the workspace root.
|
||||
|
||||
.PARAMETER MaxResults
|
||||
Maximum number of results to return for search. Defaults to 30.
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-WinMdQuery.ps1 -Action projects
|
||||
.\Invoke-WinMdQuery.ps1 -Action packages -Project BlankWinUI
|
||||
.\Invoke-WinMdQuery.ps1 -Action stats -Project BlankWinUI
|
||||
.\Invoke-WinMdQuery.ps1 -Action namespaces -Filter "Microsoft.UI"
|
||||
.\Invoke-WinMdQuery.ps1 -Action types -Namespace "Microsoft.UI.Xaml.Controls"
|
||||
.\Invoke-WinMdQuery.ps1 -Action members -TypeName "Microsoft.UI.Xaml.Controls.Button"
|
||||
.\Invoke-WinMdQuery.ps1 -Action search -Query "NavigationView"
|
||||
.\Invoke-WinMdQuery.ps1 -Action enums -TypeName "Microsoft.UI.Xaml.Visibility"
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet('projects', 'packages', 'stats', 'namespaces', 'types', 'members', 'search', 'enums')]
|
||||
[string]$Action,
|
||||
|
||||
[string]$Project,
|
||||
[string]$Namespace,
|
||||
[string]$TypeName,
|
||||
[string]$Query,
|
||||
[string]$Filter,
|
||||
[string]$CacheDir,
|
||||
[int]$MaxResults = 30
|
||||
)
|
||||
|
||||
# ─── Resolve cache directory ─────────────────────────────────────────────────
|
||||
|
||||
if (-not $CacheDir) {
|
||||
# Convention: skill lives at .github/skills/winmd-api-search/scripts/
|
||||
# so workspace root is 4 levels up from $PSScriptRoot.
|
||||
$scriptDir = $PSScriptRoot
|
||||
$root = (Resolve-Path (Join-Path $scriptDir '..\..\..\..')).Path
|
||||
$CacheDir = Join-Path $root 'Generated Files\winmd-cache'
|
||||
}
|
||||
|
||||
if (-not (Test-Path $CacheDir)) {
|
||||
Write-Error "Cache not found at: $CacheDir`nRun: .\Update-WinMdCache.ps1 (from .github\skills\winmd-api-search\scripts\)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ─── Project resolution helpers ──────────────────────────────────────────────
|
||||
|
||||
function Get-CachedProjects {
|
||||
$projectsDir = Join-Path $CacheDir 'projects'
|
||||
if (-not (Test-Path $projectsDir)) { return @() }
|
||||
Get-ChildItem $projectsDir -Filter '*.json' | ForEach-Object { $_.BaseName }
|
||||
}
|
||||
|
||||
function Resolve-ProjectManifest {
|
||||
param([string]$Name)
|
||||
|
||||
$projectsDir = Join-Path $CacheDir 'projects'
|
||||
if (-not (Test-Path $projectsDir)) {
|
||||
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Name) {
|
||||
$path = Join-Path $projectsDir "$Name.json"
|
||||
if (-not (Test-Path $path)) {
|
||||
# Scan mode appends a hash suffix -- try prefix match
|
||||
$matching = @(Get-ChildItem $projectsDir -Filter "${Name}_*.json" -ErrorAction SilentlyContinue)
|
||||
if ($matching.Count -eq 1) {
|
||||
return Get-Content $matching[0].FullName -Raw | ConvertFrom-Json
|
||||
}
|
||||
if ($matching.Count -gt 1) {
|
||||
$names = ($matching | ForEach-Object { $_.BaseName }) -join ', '
|
||||
Write-Error "Multiple projects match '$Name'. Specify the full name: $names"
|
||||
exit 1
|
||||
}
|
||||
$available = (Get-CachedProjects) -join ', '
|
||||
Write-Error "Project '$Name' not found. Available: $available"
|
||||
exit 1
|
||||
}
|
||||
return Get-Content $path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
# Auto-select if only one project
|
||||
$manifests = Get-ChildItem $projectsDir -Filter '*.json' -ErrorAction SilentlyContinue
|
||||
if ($manifests.Count -eq 0) {
|
||||
Write-Error "No projects cached. Run Update-WinMdCache.ps1 first."
|
||||
exit 1
|
||||
}
|
||||
if ($manifests.Count -eq 1) {
|
||||
return Get-Content $manifests[0].FullName -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
$available = ($manifests | ForEach-Object { $_.BaseName }) -join ', '
|
||||
Write-Error "Multiple projects cached -- use -Project to specify. Available: $available"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Get-PackageCacheDirs {
|
||||
param($Manifest)
|
||||
$dirs = @()
|
||||
foreach ($pkg in $Manifest.packages) {
|
||||
$dir = Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version
|
||||
if (Test-Path $dir) {
|
||||
$dirs += $dir
|
||||
}
|
||||
}
|
||||
return $dirs
|
||||
}
|
||||
|
||||
# ─── Action: projects ────────────────────────────────────────────────────────
|
||||
|
||||
function Show-Projects {
|
||||
$projects = Get-CachedProjects
|
||||
if ($projects.Count -eq 0) {
|
||||
Write-Output "No projects cached."
|
||||
return
|
||||
}
|
||||
Write-Output "Cached projects ($($projects.Count)):"
|
||||
foreach ($p in $projects) {
|
||||
$manifest = Get-Content (Join-Path (Join-Path $CacheDir 'projects') "$p.json") -Raw | ConvertFrom-Json
|
||||
$pkgCount = $manifest.packages.Count
|
||||
Write-Output " $p ($pkgCount package(s))"
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Action: packages ────────────────────────────────────────────────────────
|
||||
|
||||
function Show-Packages {
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
Write-Output "Packages for project '$($manifest.projectName)' ($($manifest.packages.Count)):"
|
||||
foreach ($pkg in $manifest.packages) {
|
||||
$metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
|
||||
if (Test-Path $metaPath) {
|
||||
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
Write-Output " $($pkg.id)@$($pkg.version) -- $($meta.totalTypes) types, $($meta.totalMembers) members"
|
||||
} else {
|
||||
Write-Output " $($pkg.id)@$($pkg.version) -- (cache missing)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Action: stats ───────────────────────────────────────────────────────────
|
||||
|
||||
function Show-Stats {
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$totalTypes = 0
|
||||
$totalMembers = 0
|
||||
$totalNamespaces = 0
|
||||
$totalWinMd = 0
|
||||
|
||||
foreach ($pkg in $manifest.packages) {
|
||||
$metaPath = Join-Path (Join-Path (Join-Path (Join-Path $CacheDir 'packages') $pkg.id) $pkg.version) 'meta.json'
|
||||
if (Test-Path $metaPath) {
|
||||
$meta = Get-Content $metaPath -Raw | ConvertFrom-Json
|
||||
$totalTypes += $meta.totalTypes
|
||||
$totalMembers += $meta.totalMembers
|
||||
$totalNamespaces += $meta.totalNamespaces
|
||||
$totalWinMd += $meta.winMdFiles.Count
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "WinMD Index Statistics -- $($manifest.projectName)"
|
||||
Write-Output "======================================"
|
||||
Write-Output " Packages: $($manifest.packages.Count)"
|
||||
Write-Output " Namespaces: $totalNamespaces (may overlap across packages)"
|
||||
Write-Output " Types: $totalTypes"
|
||||
Write-Output " Members: $totalMembers"
|
||||
Write-Output " WinMD files: $totalWinMd"
|
||||
}
|
||||
|
||||
# ─── Action: namespaces ──────────────────────────────────────────────────────
|
||||
|
||||
function Get-Namespaces {
|
||||
param([string]$Prefix)
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
$allNs = @()
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$nsFile = Join-Path $dir 'namespaces.json'
|
||||
if (Test-Path $nsFile) {
|
||||
$allNs += (Get-Content $nsFile -Raw | ConvertFrom-Json)
|
||||
}
|
||||
}
|
||||
|
||||
$allNs = $allNs | Sort-Object -Unique
|
||||
if ($Prefix) {
|
||||
$allNs = $allNs | Where-Object { $_ -like "$Prefix*" }
|
||||
}
|
||||
$allNs | ForEach-Object { Write-Output $_ }
|
||||
}
|
||||
|
||||
# ─── Action: types ───────────────────────────────────────────────────────────
|
||||
|
||||
function Get-TypesInNamespace {
|
||||
param([string]$Ns)
|
||||
if (-not $Ns) {
|
||||
Write-Error "-Namespace is required for 'types' action."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
$safeFile = $Ns.Replace('.', '_') + '.json'
|
||||
$found = $false
|
||||
$seen = @{}
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$filePath = Join-Path $dir "types\$safeFile"
|
||||
if (-not (Test-Path $filePath)) { continue }
|
||||
$found = $true
|
||||
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
||||
foreach ($t in $types) {
|
||||
if ($seen.ContainsKey($t.fullName)) { continue }
|
||||
$seen[$t.fullName] = $true
|
||||
Write-Output "$($t.kind) $($t.fullName)$(if ($t.baseType) { " : $($t.baseType)" } else { '' })"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Error "Namespace not found: $Ns"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Action: members ─────────────────────────────────────────────────────────
|
||||
|
||||
function Get-MembersOfType {
|
||||
param([string]$FullName)
|
||||
if (-not $FullName) {
|
||||
Write-Error "-TypeName is required for 'members' action."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$lastDot = $FullName.LastIndexOf('.')
|
||||
if ($lastDot -lt 0) {
|
||||
Write-Error "-TypeName must include a namespace (for example: 'MyNamespace.MyType'). Provided: $FullName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$ns = $FullName.Substring(0, $lastDot)
|
||||
$safeFile = $ns.Replace('.', '_') + '.json'
|
||||
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$filePath = Join-Path $dir "types\$safeFile"
|
||||
if (-not (Test-Path $filePath)) { continue }
|
||||
|
||||
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
||||
$type = $types | Where-Object { $_.fullName -eq $FullName }
|
||||
if (-not $type) { continue }
|
||||
|
||||
Write-Output "$($type.kind) $($type.fullName)"
|
||||
if ($type.baseType) { Write-Output " Extends: $($type.baseType)" }
|
||||
Write-Output ""
|
||||
foreach ($m in $type.members) {
|
||||
Write-Output " [$($m.kind)] $($m.signature)"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Write-Error "Type not found: $FullName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ─── Action: search ──────────────────────────────────────────────────────────
|
||||
# Ranks namespaces by best match score on type names and member names.
|
||||
# Outputs: ranked namespaces with top matching types and the JSON file path.
|
||||
# The agent can then read the JSON file to inspect all members intelligently.
|
||||
|
||||
function Search-WinMd {
|
||||
param([string]$SearchQuery, [int]$Max)
|
||||
if (-not $SearchQuery) {
|
||||
Write-Error "-Query is required for 'search' action."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
|
||||
# Collect: namespace -> { bestScore, matchingTypes[], filePath }
|
||||
$nsResults = @{}
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$nsFile = Join-Path $dir 'namespaces.json'
|
||||
if (-not (Test-Path $nsFile)) { continue }
|
||||
$nsList = Get-Content $nsFile -Raw | ConvertFrom-Json
|
||||
|
||||
foreach ($n in $nsList) {
|
||||
$safeFile = $n.Replace('.', '_') + '.json'
|
||||
$filePath = Join-Path $dir "types\$safeFile"
|
||||
if (-not (Test-Path $filePath)) { continue }
|
||||
|
||||
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
||||
foreach ($t in $types) {
|
||||
$typeScore = Get-MatchScore -Name $t.name -FullName $t.fullName -Query $SearchQuery
|
||||
|
||||
# Also search member names for matches
|
||||
$bestMemberScore = 0
|
||||
$matchingMember = $null
|
||||
if ($t.members) {
|
||||
foreach ($m in $t.members) {
|
||||
$memberName = $m.name
|
||||
$mScore = Get-MatchScore -Name $memberName -FullName "$($t.fullName).$memberName" -Query $SearchQuery
|
||||
if ($mScore -gt $bestMemberScore) {
|
||||
$bestMemberScore = $mScore
|
||||
$matchingMember = $m.signature
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$score = [Math]::Max($typeScore, $bestMemberScore)
|
||||
if ($score -le 0) { continue }
|
||||
|
||||
if (-not $nsResults.ContainsKey($n)) {
|
||||
$nsResults[$n] = @{ BestScore = 0; Types = @(); FilePaths = @() }
|
||||
}
|
||||
$entry = $nsResults[$n]
|
||||
if ($score -gt $entry.BestScore) { $entry.BestScore = $score }
|
||||
if ($entry.FilePaths -notcontains $filePath) {
|
||||
$entry.FilePaths += $filePath
|
||||
}
|
||||
|
||||
if ($typeScore -ge $bestMemberScore) {
|
||||
$entry.Types += @{ Text = "$($t.kind) $($t.fullName) [$typeScore]"; Score = $typeScore }
|
||||
} else {
|
||||
$entry.Types += @{ Text = "$($t.kind) $($t.fullName) -> $matchingMember [$bestMemberScore]"; Score = $bestMemberScore }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($nsResults.Count -eq 0) {
|
||||
Write-Output "No results found for: $SearchQuery"
|
||||
return
|
||||
}
|
||||
|
||||
$ranked = $nsResults.GetEnumerator() |
|
||||
Sort-Object { $_.Value.BestScore } -Descending |
|
||||
Select-Object -First $Max
|
||||
|
||||
foreach ($r in $ranked) {
|
||||
$ns = $r.Key
|
||||
$info = $r.Value
|
||||
Write-Output "[$($info.BestScore)] $ns"
|
||||
foreach ($fp in $info.FilePaths) {
|
||||
Write-Output " File: $fp"
|
||||
}
|
||||
# Show top 5 highest-scoring matching types in this namespace
|
||||
$info.Types | Sort-Object { $_.Score } -Descending |
|
||||
Select-Object -First 5 |
|
||||
ForEach-Object { Write-Output " $($_.Text)" }
|
||||
Write-Output ""
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Search scoring ──────────────────────────────────────────────────────────
|
||||
# Simple ranked scoring on type names. Higher = better.
|
||||
# 100 = exact name 80 = starts-with 60 = substring
|
||||
# 50 = PascalCase 40 = multi-keyword 20 = fuzzy subsequence
|
||||
|
||||
function Get-MatchScore {
|
||||
param([string]$Name, [string]$FullName, [string]$Query)
|
||||
|
||||
$q = $Query.Trim()
|
||||
if (-not $q) { return 0 }
|
||||
|
||||
if ($Name -eq $q) { return 100 }
|
||||
if ($Name -like "$q*") { return 80 }
|
||||
if ($Name -like "*$q*" -or $FullName -like "*$q*") { return 60 }
|
||||
|
||||
$initials = ($Name.ToCharArray() | Where-Object { [char]::IsUpper($_) }) -join ''
|
||||
if ($initials.Length -ge 2 -and $initials -like "*$q*") { return 50 }
|
||||
|
||||
$words = $q -split '\s+' | Where-Object { $_.Length -gt 0 }
|
||||
if ($words.Count -gt 1) {
|
||||
$allFound = $true
|
||||
foreach ($w in $words) {
|
||||
if ($Name -notlike "*$w*" -and $FullName -notlike "*$w*") {
|
||||
$allFound = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($allFound) { return 40 }
|
||||
}
|
||||
|
||||
if (Test-FuzzySubsequence -Text $Name -Pattern $q) { return 20 }
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function Test-FuzzySubsequence {
|
||||
param([string]$Text, [string]$Pattern)
|
||||
$ti = 0
|
||||
$tLower = $Text.ToLowerInvariant()
|
||||
$pLower = $Pattern.ToLowerInvariant()
|
||||
foreach ($ch in $pLower.ToCharArray()) {
|
||||
$idx = $tLower.IndexOf($ch, $ti)
|
||||
if ($idx -lt 0) { return $false }
|
||||
$ti = $idx + 1
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
# ─── Action: enums ───────────────────────────────────────────────────────────
|
||||
|
||||
function Get-EnumValues {
|
||||
param([string]$FullName)
|
||||
if (-not $FullName) {
|
||||
Write-Error "-TypeName is required for 'enums' action."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$lastDot = $FullName.LastIndexOf('.')
|
||||
if ($lastDot -lt 1) {
|
||||
Write-Error "-TypeName must be a fully-qualified type name including namespace, e.g. 'Namespace.TypeName'. Provided: $FullName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$ns = $FullName.Substring(0, $lastDot)
|
||||
$safeFile = $ns.Replace('.', '_') + '.json'
|
||||
|
||||
$manifest = Resolve-ProjectManifest -Name $Project
|
||||
$dirs = Get-PackageCacheDirs -Manifest $manifest
|
||||
|
||||
foreach ($dir in $dirs) {
|
||||
$filePath = Join-Path $dir "types\$safeFile"
|
||||
if (-not (Test-Path $filePath)) { continue }
|
||||
|
||||
$types = Get-Content $filePath -Raw | ConvertFrom-Json
|
||||
$type = $types | Where-Object { $_.fullName -eq $FullName }
|
||||
if (-not $type) { continue }
|
||||
|
||||
if ($type.kind -ne 'Enum') {
|
||||
Write-Error "$FullName is not an Enum (kind: $($type.kind))"
|
||||
exit 1
|
||||
}
|
||||
Write-Output "Enum $($type.fullName)"
|
||||
if ($type.enumValues) {
|
||||
$type.enumValues | ForEach-Object { Write-Output " $_" }
|
||||
} else {
|
||||
Write-Output " (no values)"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Write-Error "Type not found: $FullName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ─── Dispatch ─────────────────────────────────────────────────────────────────
|
||||
|
||||
switch ($Action) {
|
||||
'projects' { Show-Projects }
|
||||
'packages' { Show-Packages }
|
||||
'stats' { Show-Stats }
|
||||
'namespaces' { Get-Namespaces -Prefix $Filter }
|
||||
'types' { Get-TypesInNamespace -Ns $Namespace }
|
||||
'members' { Get-MembersOfType -FullName $TypeName }
|
||||
'search' { Search-WinMd -SearchQuery $Query -Max $MaxResults }
|
||||
'enums' { Get-EnumValues -FullName $TypeName }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user