public/tool/New-UiTool.ps1

function New-UiTool {
    <#
    .SYNOPSIS
        Transforms any PowerShell command into a GUI application automatically.
    .DESCRIPTION
        New-UiTool introspects a command's parameter metadata and generates a responsive
        GUI with matching controls for each parameter type. It maps types and validation
        attributes to visual controls:
 
        - [ValidateSet] → Dropdown
        - [switch] → Toggle checkbox
        - [int]/[double] with ValidateRange → Slider
        - [int]/[double] → Number input
        - [string] → Text input
        - [string[]] → Multi-line text area
        - [datetime] → Date picker
        - [SecureString] → Password input
        - [PSCredential] → Credential dialog button
        - [bool] → Toggle
        - Mandatory → Required field validation
        - HelpMessage → Tooltip
 
        Execution runs on a background thread via AsyncExecutor, keeping the UI responsive.
        Results are displayed in a structured output viewer.
         
        This is parsing PowerShell's parameter binder output and building UI
        from it. If Microsoft adds weird new validation attributes or changes how parameter
        sets work, this code will need updates. We're fighting the binder, and the binder
        usually has opinions. That said, it works for the common cases, and
        "time to GUI" for a script drops to zero. That's the whole damn point.
    .PARAMETER Command
        The name of the command to wrap. Can be a cmdlet, function, or alias.
    .PARAMETER Title
        Window title. Defaults to the command name.
    .PARAMETER Width
        Window width in pixels. Default 600.
    .PARAMETER Height
        Window height in pixels. Default 500.
    .PARAMETER ParameterSet
        If the command has multiple parameter sets, specify which one to use.
        If not specified, uses the default parameter set or shows a selector.
    .PARAMETER Theme
        UI theme (Light, Dark, etc.)
    .PARAMETER ExcludeParameters
        Array of parameter names to exclude from the UI.
    .PARAMETER IncludeCommonParameters
        Include common parameters like -Verbose, -Debug, etc. Default is false.
    .PARAMETER ResultActions
        Array of hashtables defining action buttons for the results grid.
        Each hashtable should have 'Text' (button label) and 'Action' (scriptblock).
        The scriptblock receives $_ as the selected row(s).
    .PARAMETER SingleSelect
        When used with ResultActions, limits selection to a single row.
    .PARAMETER HideThemeButton
        Removes the theme switcher from the titlebar.
    .PARAMETER ShowParamType
        Displays the parameter type next to each input label.
    .PARAMETER FilePickerParameters
        Parameter names that should get a file browse button.
    .PARAMETER FolderPickerParameters
        Parameter names that should get a folder browse button.
    .PARAMETER ComputerPickerParameters
        Parameter names that should get an AD computer picker button.
    .PARAMETER NoAutoHelpers
        Disables automatic helper button detection for common parameter names
        like Path or ComputerName.
    .PARAMETER LayoutStyle
        Control arrangement: Stack (vertical) or Wrap (multi-column).
    .PARAMETER MaxColumns
        Maximum columns when using Wrap layout. 0 means auto-detect based on window width.
    .EXAMPLE
        New-UiTool -Command 'Get-Process'
 
        Creates a GUI for Get-Process with inputs for Name, Id, etc.
    .EXAMPLE
        New-UiTool -Command 'Get-ChildItem' -Title "File Browser" -ExcludeParameters 'LiteralPath'
 
        Creates a file browser tool, excluding the LiteralPath parameter.
    .EXAMPLE
        New-UiTool -Command 'Stop-Service' -ParameterSet 'InputObject'
 
        Creates a service stopper using a specific parameter set.
    .EXAMPLE
        New-UiTool -Command 'Get-Process' -ResultActions @(
            @{ Text = 'Stop'; Icon = 'Stop'; Action = { $_ | Stop-Process -Force } }
        )
 
        Creates a process viewer with a Stop button that kills selected processes.
    .EXAMPLE
        # Local function - no need to register globally
        function My-CustomTool { param([string]$Name) Write-Host "Hello $Name" }
        New-UiTool -Command 'My-CustomTool'
 
        Creates a GUI for a locally-defined function (auto-detected from caller scope).
    .EXAMPLE
        New-UiTool -Command '.\MyScript.ps1'
 
        Creates a GUI for a parameterized script file.
    #>

    [CmdletBinding()]
    param(
        # Command can be: cmdlet name, function name, script path, or CommandInfo object
        [Parameter(Mandatory, Position = 0)]
        [object]$Command,

        [string]$Title,

        [int]$Width = 600,

        [int]$Height = 500,

        [string]$ParameterSet,

        [ArgumentCompleter({ [PsUi.ThemeEngine]::GetAvailableThemes() })]
        [string]$Theme,

        [string[]]$ExcludeParameters = @(),

        [switch]$IncludeCommonParameters,

        [switch]$HideThemeButton,

        [switch]$ShowParamType,

        [hashtable[]]$ResultActions,

        [switch]$SingleSelect,

        # Input helper parameters - add browse buttons next to TextBox inputs
        [string[]]$FilePickerParameters = @(),

        [string[]]$FolderPickerParameters = @(),

        [string[]]$ComputerPickerParameters = @(),

        [switch]$NoAutoHelpers,

        # Layout options for parameter panel
        [ValidateSet('Stack', 'Wrap')]
        [string]$LayoutStyle = 'Stack',

        [ValidateScript({ $_ -eq 0 -or ($_ -ge 1 -and $_ -le 4) })]
        [int]$MaxColumns = 0
    )

    Write-Debug "Starting for command '$Command', Width=$Width, Height=$Height"

    # Get caller's SessionState for local function lookup
    $callerSessionState = $null
    try {
        $callerScope = (Get-PSCallStack)[1]
        if ($callerScope -and $callerScope.InvocationInfo.MyCommand.ScriptBlock) {
            $flags = [System.Reflection.BindingFlags]'Instance, NonPublic, Public'
            $prop = [System.Management.Automation.ScriptBlock].GetProperty('SessionState', $flags)
            if ($prop) {
                $callerSessionState = $prop.GetValue($callerScope.InvocationInfo.MyCommand.ScriptBlock)
            }
        }
    }
    catch {
        Write-Verbose "[New-UiTool] Could not extract caller SessionState: $_"
    }

    Write-Debug "Introspecting command metadata"
    $defParams = @{
        Command                 = $Command
        ParameterSet            = $ParameterSet
        ExcludeParameters       = $ExcludeParameters
        IncludeCommonParameters = $IncludeCommonParameters
        FilePickerParameters    = $FilePickerParameters
        FolderPickerParameters  = $FolderPickerParameters
        ComputerPickerParameters = $ComputerPickerParameters
        NoAutoHelpers           = $NoAutoHelpers
        CallerSessionState      = $callerSessionState
    }
    $uiDef = Get-UiDefinition @defParams
    Write-Debug "Got definition: $($uiDef.Parameters.Count) parameters, sets: $($uiDef.ParameterSets -join ', ')"

    # Store the definition in session context for stateless button access
    # This lets button handlers read command info without closures
    try {
        $existingSession = [PsUi.SessionManager]::Current
        if ($existingSession) {
            $existingSession.CurrentDefinition = $uiDef
        }
    }
    catch {
        Write-Verbose "[New-UiTool] Could not store definition in SessionContext: $_"
    }

    Write-Debug "Introspection complete: $($uiDef.Parameters.Count) parameter(s) detected"

    $cmdInfo               = $uiDef.CommandInfo
    $commandInvocation     = $uiDef.CommandName
    $commandDefinition     = $uiDef.CommandDefinition
    $commandDisplayName    = $uiDef.DisplayName
    $description           = $uiDef.Description
    $isExternalScript      = $uiDef.IsExternalScript
    $parameterSetName      = $uiDef.ParameterSetName
    $parameterSets         = $uiDef.ParameterSets
    $hasMultipleSets       = $uiDef.HasMultipleSets
    $targetParams          = $uiDef.Parameters
    $paramDescriptions     = $uiDef.ParamDescriptions
    $inputHelpers          = $uiDef.InputHelpers

    if (!$Title) {
        $Title = $commandDisplayName
    }

    Write-Debug "Rendering UI for '$commandDisplayName'"

    # Detect if we're already inside a window context (embedded mode)
    $existingSession = try { [PsUi.SessionManager]::Current } catch { $null }
    $isEmbedded = $existingSession -and $existingSession.Window
    Write-Debug "Embedded mode: $isEmbedded"

    # Copy variables to avoid GetNewClosure issues with ValidateSet attributes
    $capturedTheme           = if ($Theme) { $Theme } else { 'Light' }
    $capturedTitle           = $Title
    $capturedWidth           = $Width
    $capturedHeight          = $Height
    $capturedHeightExplicit  = $PSBoundParameters.ContainsKey('Height')
    $capturedHideThemeButton = $HideThemeButton

    $capturedCommandInvocation  = $commandInvocation
    $capturedCommandDefinition  = $commandDefinition
    $capturedIsExternalScript   = $isExternalScript
    $capturedCommandDisplayName = $commandDisplayName
    $capturedShowParamType      = $ShowParamType

    $inputHelpers = @{
        FilePicker      = [System.Collections.Generic.List[string]]::new()
        FolderPicker    = [System.Collections.Generic.List[string]]::new()
        ComputerPicker  = [System.Collections.Generic.List[string]]::new()
        FilterBuilder   = @{}  # Hashtable: ParamName -> FilterMode
    }
    if ($FilePickerParameters) { $inputHelpers.FilePicker.AddRange($FilePickerParameters) }
    if ($FolderPickerParameters) { $inputHelpers.FolderPicker.AddRange($FolderPickerParameters) }
    if ($ComputerPickerParameters) { $inputHelpers.ComputerPicker.AddRange($ComputerPickerParameters) }

    # Detect command type to determine filter mode
    $cmdName = $cmdInfo.Name
    $filterMode = 'Generic'
    if ($cmdName -match '^Get-AD|^Set-AD|^New-AD|^Remove-AD') {
        $filterMode = 'AD'
    }
    elseif ($cmdName -match '^Get-Wmi|^Get-Cim|^Invoke-Wmi|^Invoke-Cim') {
        $filterMode = 'WMI'
    }
    elseif ($cmdName -match '^Get-ChildItem$|^Get-Item$|^Copy-Item$|^Move-Item$|^Remove-Item$|^Rename-Item$') {
        $filterMode = 'File'
    }
    else {
        # For scripts/functions, detect file mode if both Path-like and Filter params exist
        $paramNames = $targetParams | ForEach-Object { $_.Name }
        $hasPathParam   = $paramNames | Where-Object { $_ -match '^Path$|Directory|Folder' }
        $hasFilterParam = $paramNames | Where-Object { $_ -match '^Filter$' }
        if ($hasPathParam -and $hasFilterParam) {
            $filterMode = 'File'
        }
    }

    if (!$NoAutoHelpers) {
        foreach ($param in $targetParams) {
            $pName = $param.Name

            # Skip if already manually specified
            if ($inputHelpers.FilePicker -contains $pName -or $inputHelpers.FolderPicker -contains $pName -or $inputHelpers.ComputerPicker -contains $pName) {
                continue
            }

            # Skip non-string types (helpers only make sense for text inputs)
            if ($param.Type -and $param.Type -ne [string] -and $param.Type -ne [string[]]) {
                continue
            }

            # Auto-detect folder parameters (explicit folder names OR generic path params)
            if ($pName -match 'Directory|Folder|FolderPath|DirectoryPath|^Path$|^LiteralPath$') {
                $inputHelpers.FolderPicker.Add($pName)
            }
            # Auto-detect file parameters (only when 'File' is explicitly in the name)
            elseif ($pName -match 'File|FileName|FilePath') {
                $inputHelpers.FilePicker.Add($pName)
            }
            # Auto-detect filter parameters, apply detected mode
            elseif ($pName -match '^Filter$|^Include$|^Exclude$') {
                $inputHelpers.FilterBuilder[$pName] = $filterMode
            }
            # Auto-detect computer name parameters
            elseif ($pName -match 'ComputerName|Computer|Server|ServerName|HostName|Host|^CN$|MachineName|Machine') {
                $inputHelpers.ComputerPicker.Add($pName)
            }
        }
    }
    $capturedInputHelpers = $inputHelpers
    $capturedLayoutStyle  = $LayoutStyle
    $capturedMaxColumns   = $MaxColumns
    $capturedCommand      = $Command
    $capturedExcludes     = $ExcludeParameters

    # Nullify the validated parameter to prevent GetNewClosure from failing
    Remove-Variable -Name Theme -Scope Local -ErrorAction SilentlyContinue

    $toolContent = {

        # Header with command description in a full-width card
        if ($description -and $description -ne $commandDisplayName) {
            # Capture description locally for the nested scriptblock (PS 5.1 closure workaround)
            $aboutText = $description
            New-UiCard -Header "About" -FullWidth -Content {
                $colors = Get-ThemeColors
                $formattedText = ConvertTo-FormattedTextBlock -Text $aboutText -FontSize 12 -Foreground $colors.SecondaryText
                
                # Add to current parent
                $session = Get-UiSession
                $parent = $session.CurrentParent
                if ($parent -is [System.Windows.Controls.Panel]) {
                    [void]$parent.Children.Add($formattedText)
                }
                elseif ($parent -is [System.Windows.Controls.ContentControl]) {
                    $parent.Content = $formattedText
                }
            }
        }

        $session = Get-UiSession
        $colors = Get-ThemeColors

        $paramsGroupBox = [System.Windows.Controls.GroupBox]::new()
        $paramsGroupBox.Margin = [System.Windows.Thickness]::new(0,0,0,8)

        if ($hasMultipleSets) {
            $setItems = @($parameterSets)
            $defaultSet = if ($parameterSetName) { $parameterSetName } else { $setItems[0] }

            # Re-capture values for the nested OnChange closure (PS 5.1 closure workaround)
            $capturedHelpers       = $capturedInputHelpers
            $capturedCmdInfoForOnChange = $cmdInfo
            $capturedCmdForOnChange = $capturedCommand
            $capturedExcludesForOnChange = $capturedExcludes
            $capturedShowParamTypeForOnChange = $capturedShowParamType
            $capturedDescriptionsForOnChange = $paramDescriptions

            $headerGrid = [System.Windows.Controls.Grid]::new()
            $col1 = [System.Windows.Controls.ColumnDefinition]::new()
            $col1.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
            $col2 = [System.Windows.Controls.ColumnDefinition]::new()
            $col2.Width = [System.Windows.GridLength]::Auto
            [void]$headerGrid.ColumnDefinitions.Add($col1)
            [void]$headerGrid.ColumnDefinitions.Add($col2)

            $headerText = [System.Windows.Controls.TextBlock]::new()
            $headerText.Text = "Parameters"
            $headerText.VerticalAlignment = 'Center'
            $headerText.FontWeight = 'SemiBold'
            [System.Windows.Controls.Grid]::SetColumn($headerText, 0)
            [void]$headerGrid.Children.Add($headerText)

            $comboResult = New-UiDropdownButton -Items $setItems -Default $defaultSet -Variable 'selectedParameterSet' -Icon 'Filter' -Tooltip "Parameter Set: $defaultSet" -ShowText -NoAutoAdd -OnChange {
                param($newSet)

                $sess = Get-UiSession

                # Skip if selecting the same set that's already active
                $lastSet = $sess.GetControl('_uiTool_lastParamSet')
                if ($lastSet -and $lastSet.Tag -eq $newSet) {
                    return
                }

                $paramsPanel = $sess.GetControl('_uiTool_paramsContent')
                if (!$paramsPanel) { return }

                # Guard against null command info (PS 5.1 closure issue)
                if (!$capturedCmdInfoForOnChange) {
                    Write-Warning "Parameter set switch failed: command info not captured"
                    return
                }

                # Use captured CommandInfo directly (don't re-fetch - extracted functions may be gone)
                $cmdInfo = $capturedCmdInfoForOnChange
                $commonParams = @('Verbose','Debug','ErrorAction','WarningAction','InformationAction','ErrorVariable','WarningVariable','InformationVariable','OutVariable','OutBuffer','PipelineVariable','WhatIf','Confirm','UseTransaction')
                $excludeList = @($capturedExcludesForOnChange) + $commonParams

                # Get the parameter set definition to check mandatory correctly
                $paramSetDef = $cmdInfo.ParameterSets | Where-Object { $_.Name -eq $newSet }

                # Suppress DynamicParam errors when iterating parameters (extracted functions lack module context)
                $oldErrorAction = $ErrorActionPreference
                $ErrorActionPreference = 'SilentlyContinue'

                $newParams = [System.Collections.Generic.List[object]]::new()
                foreach ($paramName in $cmdInfo.Parameters.Keys) {
                    if ($excludeList -contains $paramName) { continue }
                    $param = $cmdInfo.Parameters[$paramName]
                    $inSet = $param.ParameterSets.ContainsKey($newSet) -or $param.ParameterSets.ContainsKey('__AllParameterSets')
                    if (!$inSet) { continue }

                    # Check mandatory for THIS specific parameter set
                    $isMandatoryInSet = $false
                    if ($paramSetDef) {
                        $paramInSet = $paramSetDef.Parameters | Where-Object { $_.Name -eq $paramName }
                        if ($paramInSet) {
                            $isMandatoryInSet = $paramInSet.IsMandatory
                        }
                    }

                    # A switch is "set-defining" if its name matches the parameter set name
                    # AND the set has no other mandatory parameters
                    $isSetDefiningSwitch = $false
                    if ($param.ParameterType -eq [switch]) {
                        if ($paramName -eq $newSet) {
                            $hasMandatoryParams = $paramSetDef.Parameters | Where-Object { $_.IsMandatory } | Select-Object -First 1
                            if (!$hasMandatoryParams) {
                                $isSetDefiningSwitch = $true
                            }
                        }
                    }

                    $newParams.Add([PSCustomObject]@{
                        Name        = $paramName
                        Type        = $param.ParameterType
                        IsMandatory = $isMandatoryInSet -or $isSetDefiningSwitch
                        ValidateSet = ($param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues
                        ValidateRange = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] } | Select-Object -First 1
                        DefaultValue = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.PSDefaultValueAttribute] } | Select-Object -First 1
                        IsSwitch    = $param.ParameterType -eq [switch]
                        Position    = ($param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] }).Position | Where-Object { $_ -ge 0 } | Select-Object -First 1
                    })
                }

                # Restore error action
                $ErrorActionPreference = $oldErrorAction

                # Sort by mandatory first, then position, then alphabetical (matches initial load)
                $newParams = $newParams | Sort-Object @{Expression={!$_.IsMandatory}},
                                                      @{Expression={if ($null -eq $_.Position) { 999 } else { $_.Position }}},
                                                      Name

                # Track the current set
                $tracker = $sess.GetControl('_uiTool_lastParamSet')
                if ($tracker) { $tracker.Tag = $newSet }

                # Use cached descriptions (already loaded at startup - no need to re-parse help)
                $descriptions = $capturedDescriptionsForOnChange

                # Determine if we're in wrap mode by checking panel type
                $isWrapMode = $paramsPanel -is [System.Windows.Controls.WrapPanel]

                # Clear and rebuild with correct layout mode
                $paramsPanel.Children.Clear()
                Initialize-UiToolParameters -TargetPanel $paramsPanel -Parameters $newParams -Descriptions $descriptions -ShowParamType:$capturedShowParamTypeForOnChange -InputHelpers $capturedHelpers -UseWrapLayout:$isWrapMode

                # Manually apply column widths since SizeChanged won't fire (panel size didn't change)
                if ($isWrapMode -and $paramsPanel.Tag -is [hashtable] -and $paramsPanel.Tag.MaxColumns -gt 0) {
                    $maxCols = $paramsPanel.Tag.MaxColumns

                    $paddingBuffer = 16
                    $availableWidth = $paramsPanel.ActualWidth - $paddingBuffer
                    if ($availableWidth -gt 0) {
                        $minColumnWidth = 150
                        $possibleCols = [Math]::Max(1, [Math]::Floor($availableWidth / $minColumnWidth))
                        $actualCols = [Math]::Min($possibleCols, $maxCols)
                        $actualCols = [Math]::Max($actualCols, 1)
                        $childWidth = [Math]::Floor(($availableWidth / $actualCols) - 8)

                        foreach ($child in $paramsPanel.Children) {
                            if ($child -is [System.Windows.FrameworkElement]) {
                                if ($child.Tag -eq 'FullWidth') { continue }
                                $child.Width = $childWidth
                            }
                        }
                    }
                }

                # Update stored param info and validate Run button state
                $sess.Variables['_uiTool_paramInfo'] = $newParams
                Update-UiToolRunButtonState

                Write-Debug "Switched to parameter set: $newSet"
            }.GetNewClosure()

            $comboContainer = $comboResult.Container
            $comboContainer.Margin = [System.Windows.Thickness]::new(8, 0, 0, 0)
            [System.Windows.Controls.Grid]::SetColumn($comboContainer, 1)
            [void]$headerGrid.Children.Add($comboContainer)

            $paramsGroupBox.Header = $headerGrid
        }
        else {
            $paramsGroupBox.Header = "Parameters"
        }

        Set-GroupBoxStyle -GroupBox $paramsGroupBox

        # Create inner panel for parameters (StackPanel or WrapPanel based on LayoutStyle)
        if ($capturedLayoutStyle -eq 'Wrap') {
            $paramsContent = [System.Windows.Controls.WrapPanel]@{
                Orientation         = 'Horizontal'
                HorizontalAlignment = 'Stretch'
            }

            # Store MaxColumns on Tag so it's accessible during param set switching
            $paramsContent.Tag = @{ MaxColumns = $capturedMaxColumns }

            # Add responsive sizing when MaxColumns is specified
            if ($capturedMaxColumns -gt 0) {
                $paramsContent.Add_SizeChanged({
                    param($sender, $eventArgs)

                    # Read MaxColumns from Tag
                    $maxCols = 2
                    if ($sender.Tag -is [hashtable] -and $sender.Tag.MaxColumns) {
                        $maxCols = $sender.Tag.MaxColumns
                    }

                    $paddingBuffer = 16
                    $availableWidth = $sender.ActualWidth - $paddingBuffer
                    if ($availableWidth -le 0) { return }

                    # Calculate column width based on MaxColumns
                    $minColumnWidth = 150
                    $possibleCols = [Math]::Max(1, [Math]::Floor($availableWidth / $minColumnWidth))
                    $actualCols = [Math]::Min($possibleCols, $maxCols)
                    $actualCols = [Math]::Max($actualCols, 1)

                    $childWidth = [Math]::Floor(($availableWidth / $actualCols) - 8)

                    # Apply width to all children that support it
                    foreach ($child in $sender.Children) {
                        if ($child -is [System.Windows.FrameworkElement]) {
                            if ($child.Tag -eq 'FullWidth') { continue }
                            $child.Width = $childWidth
                        }
                    }
                })
            }
        }
        else {
            $paramsContent = [System.Windows.Controls.StackPanel]::new()
            $paramsContent.Orientation = 'Vertical'
        }
        $paramsGroupBox.Content = $paramsContent

        # Register the inner panel so we can reference it later
        $session.AddControlSafe('_uiTool_paramsContent', $paramsContent)

        # Create a hidden tracker for the last selected parameter set (to avoid redundant refreshes)
        $paramSetTracker = [System.Windows.Controls.TextBlock]::new()
        $paramSetTracker.Visibility = 'Collapsed'
        $paramSetTracker.Tag = $parameterSetName
        $session.AddControlSafe('_uiTool_lastParamSet', $paramSetTracker)

        # Add to current parent with full-width constraint
        $parent = $session.CurrentParent
        if ($parent -is [System.Windows.Controls.Panel]) {
            $parent.Children.Add($paramsGroupBox) | Out-Null
        }
        elseif ($parent -is [System.Windows.Controls.ContentControl]) {
            $parent.Content = $paramsGroupBox
        }
        Set-FullWidthConstraint -Control $paramsGroupBox -Parent $parent -FullWidth

        # Build initial parameters using the helper
        Initialize-UiToolParameters -TargetPanel $paramsContent -Parameters $targetParams -Descriptions $paramDescriptions -ThemeColors $colors -ShowParamType:$capturedShowParamType -InputHelpers $capturedInputHelpers -UseWrapLayout:($capturedLayoutStyle -eq 'Wrap')

        # Store parameter info in session for validation/clear scripts (works for local functions)
        $session.Variables['_uiTool_paramInfo'] = $targetParams

        # Store the definition in session now that session is initialized
        # This enables stateless button access to command info
        $session.PSBase.CurrentDefinition = $uiDef

        New-UiSeparator

        # Display name for button label
        $cmdDisplayName = $capturedCommandDisplayName

        # Action buttons panel - buttons are stateless, reading from SessionContext.CurrentDefinition
        New-UiPanel -Orientation Horizontal {

            # Stateless validation script - reads command info from session
            $validateScript = { Invoke-UiToolValidation }

            # Stateless run script - reads command info from session
            $runScript = { Invoke-UiToolAction }

            $runBtnParams = @{
                Text           = "Run $cmdDisplayName"
                Icon           = 'Play'
                Accent         = $true
                ValidateScript = $validateScript
                Action         = $runScript
                ResultActions  = $ResultActions
                SingleSelect   = $SingleSelect
            }
            New-UiButton @runBtnParams

            # Store Run button reference for enable/disable based on mandatory params
            $session = Get-UiSession
            $parent  = $session.CurrentParent
            if ($parent -is [System.Windows.Controls.Panel] -and $parent.Children.Count -gt 0) {
                $runBtn = $parent.Children[$parent.Children.Count - 1]
                $session.Variables['_uiTool_runButton'] = $runBtn
            }

            # Stateless clear script - reads parameter names from session
            $clearScript = { Clear-UiToolParameters }

            New-UiButton -Text "Clear" -Icon "Delete" -NoAsync -Action $clearScript

            # Stateless help script - reads command info from session
            $helpScript = { Show-UiToolHelp }

            New-UiButton -Text "Help" -Icon "Help" -ScrollToTop -Action $helpScript
        }

        # Initial validation to set Run button state based on mandatory params
        Update-UiToolRunButtonState
    }.GetNewClosure()

    # Either embed the content directly or wrap in a new window
    if ($isEmbedded) {
        # Already inside a window - just execute the content scriptblock
        & $toolContent
    }
    else {
        # Standalone mode - create a window
        $windowParams = @{
            Title           = $capturedTitle
            Width           = $capturedWidth
            HideThemeButton = $capturedHideThemeButton
        }
        
        # Only pass Height if explicitly specified - otherwise let New-UiWindow auto-size
        if ($capturedHeightExplicit) {
            $windowParams.Height = $capturedHeight
        }
        if ($capturedTheme) { $windowParams.Theme = $capturedTheme }

        New-UiWindow @windowParams -Content $toolContent
    }
}