Public/Application/Get-CardResponse.ps1

function Get-CardResponse {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Variable used in template')]
    [system.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Settings variable used in module')]
    [system.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Justification = 'Variable used in runspace via parameter')]
    param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Json,

        [parameter(Mandatory = $false)]
        [string]$PromptTitle = $_MvRACSettings.'Get-Response'.PromptTitle,

        [parameter(Mandatory = $false)]
        [string]$CardTitle = $_MvRACSettings.'Get-Response'.CardTitle,
        [parameter(Mandatory = $false)]
        [string]$LogoUrl = $_MvRACSettings.'Get-Response'.LogoUrl,
        [parameter(Mandatory = $false)]
        [string]$LogoHeaderText = $_MvRACSettings.'Get-Response'.LogoHeader,

        [bool]$ShowVersion = $_MvRACSettings.'Get-Response'.ShowVersion,

        [parameter(Mandatory = $false)]
        [int]$PortNumber = $_MvRACSettings.'Get-Response'.PortNumber,

        [parameter(Mandatory = $false)]
        [string]$HeaderBackgroundStart = $_MvRACSettings.'Get-Response'.HeaderBackgroundStart,
        [parameter(Mandatory = $false)]
        [string]$HeaderBackgroundEnd = $_MvRACSettings.'Get-Response'.HeaderBackgroundEnd,

        [parameter(Mandatory = $false)]
        [ValidateSet("Browser", "EdgeApp")]
        [string]$ViewMethod = $_MvRACSettings.'Get-Response'.ViewMethod,

        [parameter(Mandatory = $false)]
        [int]$WindowWidth = 400,

        [parameter(Mandatory = $false)]
        [int]$WindowHeight = 600,

        [switch]$HideHeader,

        [switch]$ShowTitle,

        [switch]$ServeOnly,

        [switch]$AutoSize
    )

    #Serve the card as a web page to capture response
    process {
        $html = Get-Content -Path "$PSScriptRoot\Templates\PromptCard.html" -Raw

        # Find an available port if the default is in use
        $MaxPortRetries = 10
        $CurrentPort = $PortNumber
        $PortFound = $false

        for ($i = 0; $i -lt $MaxPortRetries; $i++) {
            $TestPort = $CurrentPort + $i

            # Test if port is available
            try {
                $TestListener = [System.Net.HttpListener]::new()
                if ($IsWindows) {
                    $TestListener.Prefixes.Add("http://localhost:$TestPort/")
                }
                else {
                    $TestListener.Prefixes.Add("http://+:$TestPort/")
                }
                $TestListener.Start()
                $TestListener.Stop()
                $TestListener.Close()

                # Port is available
                $CurrentPort = $TestPort
                $PortFound = $true
                if ($i -gt 0) {
                    Write-Verbose "Port $PortNumber was in use, using port $CurrentPort instead"
                }
                break
            }
            catch {
                # Port is in use, try next one
                Write-Verbose "Port $TestPort is in use, trying next port..."
                continue
            }
        }

        if (-not $PortFound) {
            Write-Error "Could not find an available port after $MaxPortRetries attempts starting from $PortNumber"
            return
        }

        # Set the service URL based on OS
        if ($IsWindows) {
            $ServiceUrl = "http://localhost:$CurrentPort/"
        }
        else {
            $ServiceUrl = "http://+:$CurrentPort/"
        }

        $LogoHeader = $LogoHeaderText

        if ( $ShowVersion ) {
            $LogoHeader = "$LogoHeaderText <span class='version'>v$ModuleVersion</span>"
        }

        if ( -not $ShowTitle ) {
            $TitleClass = "hide-title"
        }
        else {
            $TitleClass = ""
        }

        # Build the extensions payload using the provided JSON
        $ExtensionsPayload = Build-ExtensionsPayload -Json $Json -ScriptsPath "$PSScriptRoot\Templates\Extension\Script" -StylesPath "$PSScriptRoot\Templates\Extension\Style" -EncapsulateStyles

        if ($ExtensionsPayload) {
            $ExtensionsJs = $ExtensionsPayload.Scripts
            $ExtensionsCss = $ExtensionsPayload.Styles
        }
        else {
            $ExtensionsJs = ''
            $ExtensionsCss = ''
        }

        # Generate a unique response GUID to track the response of the card
        $ResponseGuid = [guid]::NewGuid().ToString()


        #Hide header class
        if ( $HideHeader ) {
            $HideHeaderClass = "hide-header"
        }
        else {
            $HideHeaderClass = ""
        }

        # Set max width and height for autosize
        if ( $AutoSize ) {
            Add-Type -AssemblyName System.Windows.Forms
            $Screen = [System.Windows.Forms.Screen]::PrimaryScreen
            $MaxWidth = [math]::Max(1200, $Screen.WorkingArea.Width - 100)
            $MaxHeight = [math]::Max(900, $Screen.WorkingArea.Height - 100)
        }
        else {
            $MaxWidth = 1200
            $MaxHeight = 900
        }

        # Expand any variables in the HTML template (ResponseGuid, ServiceUrl, CardTitle, LogoUrl, etc)
        $html = $ExecutionContext.InvokeCommand.ExpandString($html)

        #Start the local web server to serve the card and listen for response
        $WSSession = New-LocalCardWebserver -Html $html -ServiceUrl $ServiceUrl -ResponseGuid $ResponseGuid

        #Start the listener
        $asyncResult = $WSSession.PowerShell.BeginInvoke()

        #Open browser to the page
        if (!$ServeOnly) {
            # Initialize EdgeAppProcess variable for window close detection
            $EdgeAppProcess = $null

            switch ($ViewMethod) {
                "EdgeApp" {
                    try {
                        # Use Edge in app mode for clean WebView2 experience
                        Write-Information "Opening in Edge (WebView2 browser mode)..."

                        # Create a wrapper HTML that resizes window and redirects
                        $wrapperHtml = $ExecutionContext.InvokeCommand.ExpandString((Get-Content -Path "$PSScriptRoot\Templates\EdgeAppLoader.html" -Raw))

                        $tempFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "AdaptiveCard_$(Get-Random).html")
                        [System.IO.File]::WriteAllText($tempFile, $wrapperHtml, [System.Text.Encoding]::UTF8)

                        # Open with Edge app mode and capture the process
                        $ParentEdgeProcess = Start-Process "msedge" -ArgumentList "--app=file:///$($tempFile.Replace('\','/'))" -PassThru

                        # Wait for Edge to create the app window
                        Start-Sleep -Milliseconds 100

                        #loop to find the correct Edge window
                        $MaxPollTries = 50
                        do {
                            Start-Sleep -Milliseconds 100
                            $EdgeAppProcess = Get-Process -Name "msedge" -ErrorAction SilentlyContinue |
                            Where-Object { $_.MainWindowTitle -eq $CardTitle -and $_.HasExited -eq $false -and $_.MainWindowHandle -ne 0 }

                        } while (-not $EdgeAppProcess -and $MaxPollTries--)

                        if ($EdgeAppProcess) {
                            Write-Verbose "Found Edge app process: ID=$($EdgeAppProcess.Id), Title='$($EdgeAppProcess.MainWindowTitle)'"
                        }
                        else {
                            Write-Warning "Could not find Edge app window. Window close detection will not work."
                        }

                        # Clean up temp file after a delay
                        Start-Job -ScriptBlock {
                            param($file)
                            Start-Sleep -Seconds 10
                            if (Test-Path $file) {
                                Remove-Item $file -Force -ErrorAction SilentlyContinue
                            }
                        } -ArgumentList $tempFile | Out-Null

                    }
                    catch {
                        Write-Warning "Failed to launch Edge: $($_.Exception.Message)"
                        Write-Warning "Falling back to default browser..."
                        Start-Process $ServiceUrl
                    }
                }
                "Browser" {

                    #Test for parameters that are not compatible with browser view
                    if ( $AutoSize ) {
                        Write-Warning "AutoSize parameter is not supported in Browser view. Ignoring."
                    }
                    if ( $WindowWidth -ne 400 -or $WindowHeight -ne 600 ) {
                        Write-Warning "Custom window size parameters are not supported in Browser view. Ignoring."
                    }
                    Start-Process $ServiceUrl
                }
                default {}
            }

            $WaitingPrompt = "{blue}[{white}Waiting for user response{gray}{use Ctrl+C to cancel}{blue}]"

            #Set The Dot count for animation
            $DotCount = 0

            Write-ColoredHost $WaitingPrompt -NoNewLine
            [console]::CursorVisible = $false

            #Test to see if $asyncResult halted abnormally
            if ($asyncResult.IsCompleted -eq $True ) {
                Write-Warning "Async operation did not complete as expected."

                #Grab the log stream from the runspace
                $logStream = $PowerShell.Streams.Error

                $logStream | ForEach-Object { Write-Verbose "Error: $_" }
            }

            try {
                while ($asyncResult.IsCompleted -eq $false) {
                    #If crtl+c is pressed, stop listening
                    Start-Sleep -Milliseconds 250
                    $DotCount = ($DotCount + 1) % 7
                    $Dots = "►" * $DotCount

                    if ($DotCount -eq 0) {
                        $Dots = " "
                    }
                    $PromptToShow = "{blue}[{white}Waiting for user response{gray}(use Ctrl+C to cancel){blue}] $Dots"

                    #Overwrite the previous line
                    $Host.UI.RawUI.CursorPosition = @{X = 0; Y = $Host.UI.RawUI.CursorPosition.Y }

                    #Hide the cursor while waiting
                    Write-ColoredHost ("`r" + $PromptToShow) -NoNewLine


                    #If the the viewMode is EdgeApp and the window is no longer open, cancel waiting
                    if ( $ViewMethod -eq "EdgeApp") {
                        # Check if the Edge process is still running
                        if ($EdgeAppProcess -and $EdgeAppProcess.HasExited -and $asyncResult.IsCompleted -eq $false) {
                            Write-Verbose "EdgeApp window was closed by user"
                            throw "WindowClosed"
                        }
                    }
                }
                Write-ColoredHost "{Green}[V]"
                #Show the cursor again
                [console]::CursorVisible = $true
                $data = $WSSession.PowerShell.EndInvoke($asyncResult)
            }


            catch {
                Write-Error "An error occurred: $_"
            }
            finally {
                if ($null -eq $data) {
                    try { [void](Invoke-WebRequest -Uri $ServiceUrl -Method Post -OperationTimeoutSeconds 1 -ConnectionTimeoutSeconds 1 -Body @{responseGuid = $ResponseGuid }) } catch { [void]$_ }
                    [void]($WSSession.PowerShell.Stop())
                }

                #Kill the Edge app process if still running
                # if ($EdgeAppProcess) {
                # # Stop-Process -Id $EdgeAppProcess.Id -Force -ErrorAction SilentlyContinue
                # }
                #Force kill the powershell if still running
                [void]($WSSession.PowerShell.Dispose())


                #Close the runspace
                $WSSession.Runspace.Close()
                $WSSession.Runspace.Dispose()
            }
            if ( ![string]::IsNullOrWhiteSpace($data) ) {
                return $data | ConvertFrom-Json | Select-Object -ExcludeProperty ResponseGuid
            }
        }
    }
}