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", "WindowsForms", "EdgeApp")]
        [string]$ViewMethod = $_MvRACSettings.'Get-Response'.ViewMethod,

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

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

        [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
        }

        if ($IsWindows) {
            $ServiceUrl = "http://localhost:$CurrentPort/"
        }
        else {
            $ServiceUrl = "http://+:$CurrentPort/"
        }

        $LogoHeader = $LogoHeaderText

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

        #Read the JSON and only load needed extensions
        $AvailableExtensions = (Get-ChildItem -Path "$PSScriptRoot\Templates\Extension\Script" -Filter *.js | ForEach-Object { $_.BaseName })
        $ExtensionsToLoad = @()

        foreach ($Extension in $AvailableExtensions) {
            if ($Json -match [regex]::escape($Extension)) {
                $ExtensionsToLoad += $Extension
            }
        }

        $ExtensionsJs = ''
        $ExtensionsCss = ''
        foreach ($Extension in $ExtensionsToLoad) {
            #Get the file content
            $ExtensionPath = "$PSScriptRoot\Templates\Extension\Script\$Extension.js"


            if (Test-Path -Path $ExtensionPath) {
                $ExtensionContent = Get-Content -Path $ExtensionPath -Raw
                $ExtensionsJs += "`n`n// Extension: $Extension`n" + $ExtensionContent
            }
            $ExtensionCssPath = "$PSScriptRoot\Templates\Extension\Style\$Extension.css"
            if (Test-Path -Path $ExtensionCssPath) {
                $ExtensionCssContent = Get-Content -Path $ExtensionCssPath -Raw
                $ExtensionsCss += "`n/* Extension: $Extension */`n" + $ExtensionCssContent
            }
        }

        $ExtensionsCss = "<style type='text/css'>$ExtensionsCss</style>"

        $ResponseGuid = [guid]::NewGuid().ToString()
        $html = $ExecutionContext.InvokeCommand.ExpandString($html)

        #Create a task to listen for requests
        $Runspace = [runspacefactory]::CreateRunspace()
        $Runspace.Open()

        $ScriptBlock = {
            param ($html, $ServiceUrl, $ResponseGuid)

            $listener = [System.Net.HttpListener]::new()
            #Test if the host is a windows system to determine the correct prefix

            $listener.Prefixes.Add($ServiceUrl)

            $listener.Start()
            while ($listener.IsListening) {
                # Wait for request, but handle Ctrl+C safely
                if ($listener.IsListening) {
                    $context = $listener.GetContext()
                    $request = $context.Request
                    $response = $context.Response

                    if ($request.HttpMethod -eq "GET") {
                        $buffer = [System.Text.Encoding]::UTF8.GetBytes($html)
                        $response.OutputStream.Write($buffer, 0, $buffer.Length)
                        $response.Close()
                    }
                    elseif ($request.HttpMethod -eq "POST") {
                        $reader = New-Object IO.StreamReader($request.InputStream)
                        $data = $reader.ReadToEnd()
                        $reader.Close()

                        $responseString = "Thanks! Data received"
                        $buffer = [System.Text.Encoding]::UTF8.GetBytes($responseString)

                        # Set response headers
                        $response.ContentLength64 = $buffer.Length
                        $response.ContentType = "text/plain; charset=utf-8"
                        $response.StatusCode = 200

                        # Write response
                        $response.OutputStream.Write($buffer, 0, $buffer.Length)

                        # CRITICAL: Flush and close the output stream before breaking
                        $response.OutputStream.Flush()
                        $response.OutputStream.Close()
                        $response.Close()

                        # Small delay to ensure response is sent
                        Start-Sleep -Milliseconds 100

                        #Test to see the response GUID matches
                        $jsonData = $data | ConvertFrom-Json
                        if ($jsonData.ResponseGuid -eq $ResponseGuid) {
                            $data
                            break
                        }

                    }
                }
            }
            $listener.Stop()
            $listener.Close()
        }
        $PowerShell = [powershell]::Create()
        $PowerShell.Runspace = $Runspace
        [void]($PowerShell.AddScript($ScriptBlock).AddArgument($html).AddArgument($ServiceUrl))

        $asyncResult = $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 = $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]($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]($PowerShell.Dispose())


                #Close the runspace
                $Runspace.Close()
                $Runspace.Dispose()
            }
            if ( $null -ne $data ) {
                return $data | ConvertFrom-Json
            }
        }
    }
}