public/dialogs/Show-UiSaveDialog.ps1

function Show-UiSaveDialog {
    <#
    .SYNOPSIS
        Shows a file save dialog.
    .DESCRIPTION
        Show-UiFilePicker's counterpart for picking a destination, parented the same way.
        Returns the target path, $null on cancel. Writing the file is still your job.
    .PARAMETER Title
        Dialog title.
    .PARAMETER Filter
        File type filter.
    .PARAMETER DefaultName
        Default file name.
    .PARAMETER InitialDirectory
        Starting folder path when the dialog opens.
    .EXAMPLE
        $path = Show-UiSaveDialog -Filter 'CSV files|*.csv' -DefaultName 'export.csv'
    #>

    [CmdletBinding()]
    param(
        [string]$Title = 'Save File',
        
        [string]$Filter = 'All files|*.*',
        
        [string]$DefaultName,
        
        [string]$InitialDirectory
    )

    Write-Debug "Title='$Title' Filter='$Filter' DefaultName='$DefaultName'"

    # The dialog needs to run on an STA thread with a message pump
    $showDialog = {
        param($dialogTitle, $dialogFilter, $defaultFileName, $initialDir)
        
        Add-Type -AssemblyName PresentationFramework
        
        $dialog = [Microsoft.Win32.SaveFileDialog]::new()
        $dialog.Title = $dialogTitle
        $dialog.Filter = $dialogFilter

        if ($defaultFileName) {
            $dialog.FileName = $defaultFileName
        }

        if ($initialDir -and (Test-Path $initialDir)) {
            $dialog.InitialDirectory = $initialDir
        }

        $result = $dialog.ShowDialog()

        if ($result -eq $true) {
            return $dialog.FileName
        }
        return $null
    }

    # If we're inside a PsUi window, use its dispatcher to show the dialog
    $session = Get-UiSession -ErrorAction SilentlyContinue
    if ($session -and $session.Window) {
        # We have a window, use its dispatcher
        return $session.Window.Dispatcher.Invoke([Func[object]]{
            $dialog = [Microsoft.Win32.SaveFileDialog]::new()
            $dialog.Title = $Title
            $dialog.Filter = $Filter
            if ($DefaultName) { $dialog.FileName = $DefaultName }
            if ($InitialDirectory -and (Test-Path $InitialDirectory)) { 
                $dialog.InitialDirectory = $InitialDirectory 
            }
            $result = $dialog.ShowDialog($session.Window)
            if ($result -eq $true) { return $dialog.FileName }
            return $null
        })
    }

    # No UI context - run in STA runspace
    $runspace = [runspacefactory]::CreateRunspace()
    $runspace.ApartmentState = 'STA'
    $runspace.ThreadOptions = 'ReuseThread'
    $runspace.Open()

    $ps = [powershell]::Create()
    $ps.Runspace = $runspace
    [void]$ps.AddScript($showDialog)
    [void]$ps.AddArgument($Title)
    [void]$ps.AddArgument($Filter)
    [void]$ps.AddArgument($DefaultName)
    [void]$ps.AddArgument($InitialDirectory)

    try {
        $result = $ps.Invoke()
        Write-Debug "Result: $result"
        return $result
    }
    finally {
        $ps.Dispose()
        $runspace.Dispose()
    }
}