Terminal/SRxTerminalBoxControl.psm1

Import-Module (Join-Path $PSScriptRoot 'SRxConPty.psm1') -DisableNameChecking -Force -ErrorAction SilentlyContinue
Import-Module (Join-Path $PSScriptRoot 'SRxMonacoEditor.psm1') -DisableNameChecking -Force -ErrorAction Stop
Import-Module (Join-Path $PSScriptRoot 'SRxVSCode.psm1')       -DisableNameChecking -Force -ErrorAction Stop

function Get-SRxLoadedAssembly {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$SimpleName)
    return [System.AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object { $_.GetName().Name -eq $SimpleName } |
        Select-Object -First 1
}

function Import-SRxWebView2Assemblies {
    [CmdletBinding()]
    param(
        [string]$LibPath = (Join-Path $PSScriptRoot 'Lib\WebView2')
    )
    if (Get-SRxLoadedAssembly 'Microsoft.Web.WebView2.WinForms') {
        return $true
    }
    try {
        $core = Join-Path $LibPath 'Microsoft.Web.WebView2.Core.dll'
        $winf = Join-Path $LibPath 'Microsoft.Web.WebView2.WinForms.dll'
        if (-not (Test-Path $core) -or -not (Test-Path $winf)) {
            Write-Host "Import-SRxWebView2Assemblies - SDK assemblies not found under '$LibPath'."
            return $false
        }
        Add-Type -Path $core -ErrorAction Stop
        Add-Type -Path $winf -ErrorAction Stop
        return $true
    }
    catch {
        Write-Host "Import-SRxWebView2Assemblies - $($_.Exception.Message)"
        return $false
    }
}

function Get-SRxWebView2Type {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$ShortName)
    $assemblySimpleName = if ($ShortName -like 'WinForms.*') { 'Microsoft.Web.WebView2.WinForms' } else { 'Microsoft.Web.WebView2.Core' }
    $fullTypeName = "Microsoft.Web.WebView2.$ShortName"

    $asm = Get-SRxLoadedAssembly $assemblySimpleName
    if ($asm) {
        $t = $asm.GetType($fullTypeName)
        if ($t) { return $t }
    }
    try { return [type]::GetType("$fullTypeName, $assemblySimpleName") } catch { return $null }
}

class SRxTerminalBoxControl : System.Windows.Forms.UserControl {
    [string]$SourcePath = ""
    [hashtable]$Icons = @{}
    [System.Management.Automation.ScriptBlock]$OnStatus = $null
    [System.Management.Automation.ScriptBlock]$OnClose  = $null
    [string]$StartupCommandLine = "powershell.exe -NoLogo -NoExit"

    [object]$Editors = $null
    [string]$EditorTheme = 'vs-dark'
    [bool]$EnsureVSCode = $true
    [string]$VSCodeBuildEdition = 'Stable-User'
    [bool]$DisableEditorSurface = $false
    [System.Management.Automation.ScriptBlock]$OnRequestShow = $null
    [System.Management.Automation.ScriptBlock]$OnDebugCommand = $null
    [System.Management.Automation.ScriptBlock]$OnEditorOpened = $null
    [System.Management.Automation.ScriptBlock]$OnEditorClosed = $null
    [System.Management.Automation.ScriptBlock]$OnDebugConsoleInput = $null

    hidden $_headerPanel   = $null
    hidden $_titleLabel    = $null
    hidden $_closeButton   = $null
    hidden $_placeholder   = $null
    hidden $_webView       = $null
    hidden $_conPty        = $null
    hidden $_startupTimer  = $null
    hidden $_ioTimer       = $null
    hidden $_pendingCommands = $null
    hidden [bool]$_started       = $false
    hidden [bool]$_webViewReady   = $false
    hidden [int]$_lastCols  = 80
    hidden [int]$_lastRows  = 24

    hidden $_contentPanel     = $null
    hidden $_terminalPanel    = $null
    hidden $_editorPanel      = $null
    hidden $_editorWebView    = $null
    hidden $_editorPlaceholder = $null
    hidden $_btnSave          = $null
    hidden $_tabs             = $null
    hidden [string]$_activeView = 'terminal'
    hidden [bool]$_editorStarted = $false
    hidden [bool]$_editorJsReady = $false
    hidden $_editorOutbox     = $null
    hidden $_overlay          = $null
    hidden $_overlayMsg       = $null
    hidden $_overlaySave      = $null
    hidden $_overlayDiscard   = $null
    hidden $_overlayCancel    = $null
    hidden $_promptEditor     = $null
    hidden $_environment      = $null
    hidden $_envTask          = $null
    hidden [string]$_envError = ''
    hidden $_envWaiters       = $null
    hidden [bool]$_vscodeChecked = $false
    hidden $_vscodeInstall    = $null
    hidden $_vscodeTimer      = $null
    hidden $_tabToolTip       = $null
    hidden $_crossBitmap      = $null
    hidden $_dbgBitmaps       = $null
    hidden $_debugLabel       = $null
    hidden $_debugPanel       = $null
    hidden $_debugWebView     = $null
    hidden $_debugPlaceholder = $null
    hidden $_debugOutbox      = $null
    hidden [bool]$_debugStarted   = $false
    hidden [bool]$_debugJsReady   = $false
    hidden [bool]$_debugConsoleOn = $false
    hidden [int]$_debugCols       = 120

    SRxTerminalBoxControl() : base() {
        $this._pendingCommands = New-Object 'System.Collections.Generic.List[string]'
        $this._editorOutbox = New-Object 'System.Collections.Generic.List[string]'
        $this._envWaiters   = New-Object 'System.Collections.Generic.List[string]'
        $this._debugOutbox  = New-Object 'System.Collections.Generic.List[string]'
        $this._tabs = @{}
        $this.Editors = New-SRxMonacoEditorCollection -Owner $this
        $this.buildLayout()
    }

    hidden [void] buildLayout() {
        $this.Dock = [System.Windows.Forms.DockStyle]::Fill

        $this._headerPanel = [System.Windows.Forms.ToolStrip]::new()
        $this._headerPanel.Dock = [System.Windows.Forms.DockStyle]::Top
        $this._headerPanel.GripStyle = [System.Windows.Forms.ToolStripGripStyle]::Hidden
        $this._headerPanel.BackColor = [System.Drawing.SystemColors]::ControlLight
        $this._headerPanel.ImageScalingSize = [System.Drawing.Size]::new(20, 20)
        $this._headerPanel.AutoSize = $false
        $this._headerPanel.Height = 35

        $this._btnSave = [System.Windows.Forms.ToolStripButton]::new()
        $this._btnSave.Name = '_btnSave'
        $this._btnSave.DisplayStyle = [System.Windows.Forms.ToolStripItemDisplayStyle]::Image
        $this._btnSave.Alignment = [System.Windows.Forms.ToolStripItemAlignment]::Left
        $this._btnSave.Size = [System.Drawing.Size]::new(29, 24)
        $this._btnSave.ImageScaling = [System.Windows.Forms.ToolStripItemImageScaling]::SizeToFit
        $this._btnSave.AutoToolTip = $false
        $this._btnSave.ToolTipText = ''
        $this._btnSave.Enabled = $false
        $this._btnSave.Visible = $false
        $this._btnSave.Tag = $this
        $this._btnSave.add_Click({ param($s, $e) $s.Tag.onSaveButtonClick() })
        $this._btnSave.add_MouseEnter({ param($s, $e) $s.Tag.onSaveButtonMouseEnter() })
        $this._btnSave.add_MouseLeave({ param($s, $e) $s.Tag.onSaveButtonMouseLeave() })

        $this._titleLabel = [System.Windows.Forms.ToolStripLabel]::new('Terminal')
        $this._titleLabel.Tag = $this
        $this._titleLabel.add_Click({ param($s, $e) $s.Tag.ShowTerminal() })

        $this._closeButton = [System.Windows.Forms.ToolStripButton]::new()
        $this._closeButton.Alignment = [System.Windows.Forms.ToolStripItemAlignment]::Right
        $this._closeButton.DisplayStyle = [System.Windows.Forms.ToolStripItemDisplayStyle]::Image
        $this._closeButton.Size = [System.Drawing.Size]::new(29, 24)
        $this._closeButton.ImageScaling = [System.Windows.Forms.ToolStripItemImageScaling]::SizeToFit
        $this._closeButton.AutoToolTip = $false
        $this._closeButton.Tag = $this
        $this._closeButton.add_Click({
            param($source, $e)
            $_this = $source.Tag
            if ($_this.OnClose) { $_this.OnClose.Invoke() }
        })

        [void]$this._headerPanel.Items.Add($this._btnSave)
        [void]$this._headerPanel.Items.Add($this._titleLabel)
        $this._debugLabel = [System.Windows.Forms.ToolStripLabel]::new('Debug Console')
        $this._debugLabel.Visible = $false
        $this._debugLabel.Tag = $this
        $this._debugLabel.add_Click({ param($s, $e) $s.Tag.SelectDebugConsole() })
        [void]$this._headerPanel.Items.Add($this._debugLabel)
        [void]$this._headerPanel.Items.Add($this._closeButton)

        $this._placeholder = [System.Windows.Forms.Label]::new()
        $this._placeholder.Dock = [System.Windows.Forms.DockStyle]::Fill
        $this._placeholder.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
        $this._placeholder.Text = "Terminal is starting..."
        $this._placeholder.Visible = $false

        $this._contentPanel = [System.Windows.Forms.Panel]::new()
        $this._contentPanel.Dock = [System.Windows.Forms.DockStyle]::Fill

        $this._terminalPanel = [System.Windows.Forms.Panel]::new()
        $this._terminalPanel.Dock = [System.Windows.Forms.DockStyle]::Fill
        [void]$this._terminalPanel.Controls.Add($this._placeholder)

        $this._editorPanel = [System.Windows.Forms.Panel]::new()
        $this._editorPanel.Dock = [System.Windows.Forms.DockStyle]::Fill
        $this._editorPanel.BackColor = [System.Drawing.Color]::FromArgb(30, 30, 30)
        $this._editorPanel.Visible = $false

        $this._editorPlaceholder = [System.Windows.Forms.Label]::new()
        $this._editorPlaceholder.Dock = [System.Windows.Forms.DockStyle]::Fill
        $this._editorPlaceholder.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
        $this._editorPlaceholder.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 200)
        $this._editorPlaceholder.Text = "Editor is starting..."
        $this._editorPlaceholder.Visible = $false
        [void]$this._editorPanel.Controls.Add($this._editorPlaceholder)

        $this.buildOverlay()
        $this._debugPanel = [System.Windows.Forms.Panel]::new()
        $this._debugPanel.Dock = [System.Windows.Forms.DockStyle]::Fill
        $this._debugPanel.BackColor = [System.Drawing.Color]::FromArgb(30, 30, 30)
        $this._debugPanel.Visible = $false
        $this._debugPlaceholder = [System.Windows.Forms.Label]::new()
        $this._debugPlaceholder.Dock = [System.Windows.Forms.DockStyle]::Fill
        $this._debugPlaceholder.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
        $this._debugPlaceholder.ForeColor = [System.Drawing.Color]::FromArgb(200, 200, 200)
        $this._debugPlaceholder.Text = "Debug Console is starting..."
        $this._debugPlaceholder.Visible = $false
        [void]$this._debugPanel.Controls.Add($this._debugPlaceholder)
        [void]$this._contentPanel.Controls.Add($this._debugPanel)
        [void]$this._contentPanel.Controls.Add($this._editorPanel)
        [void]$this._contentPanel.Controls.Add($this._terminalPanel)
        [void]$this.Controls.Add($this._contentPanel)
        [void]$this.Controls.Add($this._headerPanel)
    }

    [void] ApplyIcons() {
        try {
            if ($this.Icons.ContainsKey('Close') -and $this.Icons['Close']) {
                $this._closeButton.Image = $this.Icons['Close']
            }
            if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) {
                $this._btnSave.Image = $this.Icons['Save']
                $this._overlaySave.Image = $this.Icons['Save']
            }
            foreach ($t in @($this._tabs.Values)) { $this.applyTabIcon($t) }
            $this.updateSaveButton()
        } catch { Write-Host "SRxTerminalBoxControl.ApplyIcons - $($_.Exception.Message)" }
    }

    hidden [void] status([string]$text) {
        try { if ($this.OnStatus) { $this.OnStatus.Invoke($text) } } catch { }
    }

    [void] Activate() {
        if ($this._started) {
            try { if ($this._webView) { $this._webView.Focus() } } catch { }
            return
        }
        $this._started = $true
        $this.status("Starting terminal...")

        if (-not (Test-SRxConPtySupported)) {
            $this.showPlaceholder("Terminal unavailable - this Windows build does not support ConPTY (needs 1809+).")
            $this.status("Terminal unavailable (ConPTY unsupported).")
            return
        }
        if (-not (Import-SRxWebView2Assemblies)) {
            $this.showPlaceholder("Terminal unavailable - WebView2 SDK assemblies are not deployed. See Terminal\Lib\WebView2.")
            $this.status("Terminal unavailable (WebView2 SDK missing).")
            return
        }

        try {
            $this._webView = New-Object -TypeName 'Microsoft.Web.WebView2.WinForms.WebView2'
            $this._webView.Dock = [System.Windows.Forms.DockStyle]::Fill
            [void]$this._terminalPanel.Controls.Add($this._webView)
            $this._webView.BringToFront()

            $this.acquireEnvironment('terminal')
        }
        catch {
            $this.showPlaceholder("Terminal failed to start: $($_.Exception.Message)")
            $this.status("Terminal start failed: $($_.Exception.Message)")
        }
    }

    hidden [void] clearStartupTimer() {
        $this._startupTimer = $null
    }

    hidden [void] acquireEnvironment([string]$consumer) {
        if ($this._environment) { $this.dispatchEnvironment($consumer); return }
        if ($this._envError)    { $this.dispatchEnvironmentFailure($consumer, $this._envError); return }
        if (-not $this._envWaiters.Contains($consumer)) { $this._envWaiters.Add($consumer) }
        if ($this._envTask) { return }

        try {
            $userDataFolder = Join-Path $(if ($this.SourcePath) { $this.SourcePath } else { $env:TEMP }) 'WebView2UserData'
            if (-not (Test-Path $userDataFolder)) { New-Item -ItemType Directory -Path $userDataFolder -Force | Out-Null }

            $envType = Get-SRxWebView2Type 'Core.CoreWebView2Environment'
            if (-not $envType) {
                $this.failEnvironment("CoreWebView2Environment type not found after loading WebView2 SDK.")
                return
            }
            $this._envTask = $envType::CreateAsync($null, $userDataFolder, $null)

            $this._startupTimer = New-Object System.Windows.Forms.Timer
            $this._startupTimer.Interval = 50
            $this._startupTimer.Tag = $this
            $this._startupTimer.add_Tick({
                param($source, $e)
                $ctrl = $source.Tag
                $t = $ctrl._envTask
                if ($null -eq $t -or -not $t.IsCompleted) { return }
                $source.Stop()
                $source.Dispose()
                $ctrl.clearStartupTimer()
                if ($t.IsFaulted -or -not $t.Result) {
                    $detail = ""
                    try { if ($t.Exception) { $detail = " ($($t.Exception.GetBaseException().Message))" } } catch { }
                    $ctrl.failEnvironment("WebView2 runtime not found or failed to initialize$detail. Install the Evergreen WebView2 Runtime.")
                    return
                }
                $ctrl.environmentReady($t.Result)
            })
            $this._startupTimer.Start()
        }
        catch {
            $this.failEnvironment("WebView2 environment creation failed: $($_.Exception.Message)")
        }
    }

    hidden [void] environmentReady($environment) {
        $this._environment = $environment
        $waiters = @($this._envWaiters.ToArray())
        $this._envWaiters.Clear()
        foreach ($w in $waiters) { $this.dispatchEnvironment($w) }
    }

    hidden [void] failEnvironment([string]$reason) {
        $this._envError = $reason
        $waiters = @($this._envWaiters.ToArray())
        $this._envWaiters.Clear()
        foreach ($w in $waiters) { $this.dispatchEnvironmentFailure($w, $reason) }
    }

    hidden [void] dispatchEnvironment([string]$consumer) {
        if ($consumer -eq 'terminal') { $this.onEnvironmentReady($this._environment) }
        elseif ($consumer -eq 'editor') { $this.onEditorEnvironmentReady($this._environment) }
        elseif ($consumer -eq 'debug') { $this.onDebugEnvironmentReady($this._environment) }
    }

    hidden [void] dispatchEnvironmentFailure([string]$consumer, [string]$reason) {
        if ($consumer -eq 'terminal') {
            $this.showPlaceholder("Terminal unavailable - $reason")
            $this.status("Terminal unavailable.")
        }
        elseif ($consumer -eq 'editor') {
            $this.showEditorPlaceholder("Editor unavailable - $reason")
            $this.status("Editor unavailable.")
        }
        elseif ($consumer -eq 'debug') { $this.showDebugPlaceholder("Debug Console unavailable - $reason") }
    }

    hidden [void] onEnvironmentReady($environment) {
        try {
            $this._webView.add_CoreWebView2InitializationCompleted({
                param($source, $e)
                $ctrl = $source.Tag
                if (-not $e.IsSuccess) {
                    $ctrl.showPlaceholder("Terminal unavailable - CoreWebView2 initialization failed: $($e.InitializationException.Message)")
                    return
                }
                $ctrl.wireCoreWebView2()
            })
            $this._webView.Tag = $this
            $this._webView.EnsureCoreWebView2Async($environment) | Out-Null
        }
        catch { $this.showPlaceholder("Terminal failed to initialize: $($_.Exception.Message)") }
    }

    hidden [void] wireCoreWebView2() {
        try {
            $core = $this._webView.CoreWebView2
            $wwwPath = Join-Path $PSScriptRoot 'www'

            $kindType = Get-SRxWebView2Type 'Core.CoreWebView2HostResourceAccessKind'
            $allowKind = if ($kindType) { [Enum]::Parse($kindType, 'Allow') } else { 1 }

            $core.SetVirtualHostNameToFolderMapping('srx-terminal.local', $wwwPath, $allowKind)

            $control = $this
            $onWebMessage = {
                param($source, $e)
                try { $control.onWebMessage($e.TryGetWebMessageAsString()) } catch { Write-Host "onWebMessage - $($_.Exception.Message)" }
            }.GetNewClosure()
            $core.add_WebMessageReceived($onWebMessage)

            $this._webViewReady = $true
            $core.Navigate('https://srx-terminal.local/terminal.html')
            $this.status("Terminal ready.")
        }
        catch { $this.showPlaceholder("Terminal failed to wire CoreWebView2: $($_.Exception.Message)") }
    }

    hidden [void] onWebMessage([string]$json) {
        if ([string]::IsNullOrEmpty($json)) { return }
        $msg = $null
        try { $msg = $json | ConvertFrom-Json -ErrorAction Stop } catch { return }
        switch ([string]$msg.t) {
            'ready' {
                $this.startConPty($this._lastCols, $this._lastRows)
            }
            'in' {
                if ($this._conPty -and $msg.d) { $this._conPty.Write([string]$msg.d) }
            }
            'resize' {
                $cols = [int]$msg.cols; $rows = [int]$msg.rows
                if ($cols -gt 0 -and $rows -gt 0) {
                    $this._lastCols = $cols; $this._lastRows = $rows
                    if ($this._conPty) { $this._conPty.Resize($cols, $rows) }
                }
            }
            default { }
        }
    }

    hidden [void] startConPty([int]$cols, [int]$rows) {
        if ($this._conPty) { return }
        try {
            $workDir = if ($this.SourcePath) { $this.SourcePath } else { $null }
            $this._conPty = Start-SRxConPtySession -CommandLine $this.StartupCommandLine -WorkingDirectory $workDir -Columns $cols -Rows $rows
            if (-not $this._conPty) {
                $this.status("Terminal: failed to start ConPTY session.")
                return
            }
            $this.status("Terminal session started (PID $($this._conPty.ProcessId)).")
            $this.startIoPump()

            if ($this._pendingCommands -and $this._pendingCommands.Count -gt 0) {
                foreach ($pending in $this._pendingCommands) {
                    $this._conPty.Write($pending + "`r")
                }
                $this._pendingCommands.Clear()
            }
        }
        catch {
            $this.status("Terminal: ConPTY start failed: $($_.Exception.Message)")
        }
    }

    [void] RunCommand([string]$text) {
        if ([string]::IsNullOrEmpty($text)) { return }
        if ($this._activeView -ne 'terminal') { $this.switchView('terminal') }
        $this.Activate()
        if ($this._conPty) {
            $this._conPty.Write($text + "`r")
        }
        else {
            if (-not $this._pendingCommands) { $this._pendingCommands = New-Object 'System.Collections.Generic.List[string]' }
            $this._pendingCommands.Add($text)
        }
    }

    [int] GetProcessId() {
        if ($this._conPty) { return $this._conPty.ProcessId }
        return 0
    }

    [void] RestartSession() {
        if (-not $this._started) {
            return
        }

        $hadExistingSession = ($null -ne $this._conPty)

        try { if ($this._ioTimer) { $this._ioTimer.Stop(); $this._ioTimer.Dispose() } } catch { }
        $this._ioTimer = $null

        try { if ($this._conPty) { $this._conPty.Stop(1500) } } catch { }
        $this._conPty = $null

        try { if ($this._pendingCommands) { $this._pendingCommands.Clear() } } catch { }

        if (-not $hadExistingSession) {
            $this.status("Terminal settings updated - will apply once the session starts.")
            return
        }

        $this.status("Restarting terminal session...")

        try {
            if ($this._webView -and $this._webView.CoreWebView2) {
                $payload = @{ t = 'clear' } | ConvertTo-Json -Compress
                $this._webView.CoreWebView2.PostWebMessageAsJson($payload)
            }
        } catch { }

        $this.startConPty($this._lastCols, $this._lastRows)
    }

    hidden [void] pumpConPtyIo() {
        if (-not $this._conPty) { return }
        try {
            $chunk = $null
            $budget = 64
            while ($budget -gt 0 -and $this._conPty.TryDequeueOutput([ref]$chunk)) {
                $budget--
                if ($chunk -and $this._webView -and $this._webView.CoreWebView2) {
                    $ints = New-Object 'System.Collections.Generic.List[int]'
                    foreach ($b in $chunk) { [void]$ints.Add([int]$b) }
                    $payload = @{ t = 'out'; d = $ints } | ConvertTo-Json -Compress -Depth 4
                    $this._webView.CoreWebView2.PostWebMessageAsJson($payload)
                }
            }
            if ($this._conPty.HasExited) {
                $code = $this._conPty.LastExitCode
                $this.status("Terminal session ended (exit code $code).")
                try {
                    if ($this._webView -and $this._webView.CoreWebView2) {
                        $payload = @{ t = 'exit'; code = $code } | ConvertTo-Json -Compress
                        $this._webView.CoreWebView2.PostWebMessageAsJson($payload)
                    }
                } catch { }
                $this._conPty = $null
                if ($this._ioTimer) { $this._ioTimer.Stop(); $this._ioTimer.Dispose(); $this._ioTimer = $null }
            }
        }
        catch { Write-Host "pumpConPtyIo - $($_.Exception.Message)" }
    }

    hidden [void] startIoPump() {
        if ($this._ioTimer) { return }
        $this._ioTimer = New-Object System.Windows.Forms.Timer
        $this._ioTimer.Interval = 30
        $this._ioTimer.Tag = $this
        $this._ioTimer.add_Tick({
            param($source, $e)
            $ctrl = $source.Tag
            $ctrl.pumpConPtyIo()
        })
        $this._ioTimer.Start()
    }

    hidden [void] showPlaceholder([string]$text) {
        try {
            if ($this._webView) { $this._webView.Visible = $false }
            $this._placeholder.Text = $text
            $this._placeholder.Visible = $true
            $this._placeholder.BringToFront()
        } catch { }
    }

    [object] OpenEditor([string]$path) { return $this.Editors.Open($path) }

    [object] OpenEditor([string]$path, [bool]$readOnly) { return $this.Editors.Open($path, $readOnly) }

    [void] ShowTerminal() {
        $this.switchView('terminal')
        $this.Activate()
    }

    [bool] HasUnsavedEditors() { return $this.Editors.HasUnsavedChanges() }

    [string] ActiveView() { return $this._activeView }

    hidden [void] editorOpened($editor, [string]$text) {
        $this.ensureEditorSurface()
        $this.addTab($editor)
        $this.postEditor(@{
            t    = 'open'
            id   = [string]$editor.Id
            path = [string]$editor.Path
            name = [string]$editor.FileName
            lang = [string]$editor.Language
            text = $text
            eol  = [string]$editor.NewLine
            ro   = [bool]$editor.IsReadOnly
        })
        $this.raiseEditorEvent($this.OnEditorOpened, $editor)
    }

    hidden [void] raiseEditorEvent([System.Management.Automation.ScriptBlock]$handler, $editor) {
        if ($null -eq $handler) { return }
        try { $handler.Invoke($editor) } catch { Write-Host "SRxTerminalBoxControl editor event - $($_.Exception.Message)" }
    }

    hidden [void] editorReadOnlyChanged($editor) {
        $this.postEditor(@{ t = 'readonly'; id = [string]$editor.Id; ro = [bool]$editor.IsReadOnly })
        $this.updateTab($editor)
        $this.updateSaveButton()
    }

    hidden [void] editorSelected($editor, [bool]$show) {
        if ($null -eq $editor) {
            if ($show) {
                $hv = $this.homeView()
                $this.switchView($hv)
                if ($hv -eq 'terminal' -and $this._started) { $this.Activate() }
            }
            $this.refreshTabs()
            return
        }
        if ($show) {
            $this.requestShow()
            $this.postEditor(@{ t = 'select'; id = [string]$editor.Id })
            $this.switchView([string]$editor.Id)
        }
        else {
            $this.refreshTabs()
        }
    }

    hidden [bool] editorClosed($editor) {
        $wasShown = ($this._activeView -eq [string]$editor.Id)
        if ($null -ne $this._promptEditor -and $this._promptEditor -eq $editor) { $this.hideOverlay() }
        $this.removeTab($editor)
        $this.postEditor(@{ t = 'close'; id = [string]$editor.Id })
        if ($wasShown) { $this._activeView = '' }
        $this.updateSaveButton()
        $this.raiseEditorEvent($this.OnEditorClosed, $editor)
        return $wasShown
    }

    hidden [void] editorStateChanged($editor) {
        $this.updateTab($editor)
        $this.updateSaveButton()
    }

    hidden [void] promptUnsaved($editor) { $this.showOverlay($editor) }

    hidden [void] requestContent($editor, [string]$requestId) {
        $this.postEditor(@{ t = 'get'; id = [string]$editor.Id; req = $requestId })
    }

    hidden [void] markSaved($editor, [int]$version) {
        $this.postEditor(@{ t = 'saved'; id = [string]$editor.Id; ver = $version })
    }

    hidden [void] ensureEditorSurface() {
        if ($this._editorStarted) { return }
        $this._editorStarted = $true

        $this.checkVSCode()

        if ($this.DisableEditorSurface) { return }

        if (-not (Import-SRxWebView2Assemblies)) {
            $this.showEditorPlaceholder("Editor unavailable - WebView2 SDK assemblies are not deployed. See Terminal\Lib\WebView2.")
            $this.status("Editor unavailable (WebView2 SDK missing).")
            return
        }
        try {
            $this._editorWebView = New-Object -TypeName 'Microsoft.Web.WebView2.WinForms.WebView2'
            $this._editorWebView.Dock = [System.Windows.Forms.DockStyle]::Fill
            [void]$this._editorPanel.Controls.Add($this._editorWebView)
            $this._editorWebView.BringToFront()
            $this.showEditorPlaceholder("Editor is starting...")
            $this.acquireEnvironment('editor')
        }
        catch {
            $this.showEditorPlaceholder("Editor failed to start: $($_.Exception.Message)")
            $this.status("Editor start failed: $($_.Exception.Message)")
        }
    }

    hidden [void] onEditorEnvironmentReady($environment) {
        if ($null -eq $this._editorWebView) { return }
        try {
            $this._editorWebView.add_CoreWebView2InitializationCompleted({
                param($source, $e)
                $ctrl = $source.Tag
                if (-not $e.IsSuccess) {
                    $ctrl.showEditorPlaceholder("Editor unavailable - CoreWebView2 initialization failed: $($e.InitializationException.Message)")
                    return
                }
                $ctrl.wireEditorCoreWebView2()
            })
            $this._editorWebView.Tag = $this
            $this._editorWebView.EnsureCoreWebView2Async($environment) | Out-Null
        }
        catch { $this.showEditorPlaceholder("Editor failed to initialize: $($_.Exception.Message)") }
    }

    hidden [void] wireEditorCoreWebView2() {
        try {
            $core = $this._editorWebView.CoreWebView2
            $wwwPath = Join-Path $PSScriptRoot 'www'
            $kindType = Get-SRxWebView2Type 'Core.CoreWebView2HostResourceAccessKind'
            $allowKind = if ($kindType) { [Enum]::Parse($kindType, 'Allow') } else { 1 }
            $core.SetVirtualHostNameToFolderMapping('srx-terminal.local', $wwwPath, $allowKind)

            $control = $this
            $onMessage = {
                param($source, $e)
                try { $control.onEditorWebMessage($e.TryGetWebMessageAsString()) }
                catch { Write-Host "onEditorWebMessage - $($_.Exception.Message)" }
            }.GetNewClosure()
            $core.add_WebMessageReceived($onMessage)
            try { $core.Settings.AreBrowserAcceleratorKeysEnabled = $false } catch { }

            $theme = [System.Uri]::EscapeDataString($this.EditorTheme)
            $core.Navigate("https://srx-terminal.local/editor.html?theme=$theme")
        }
        catch { $this.showEditorPlaceholder("Editor failed to wire CoreWebView2: $($_.Exception.Message)") }
    }

    hidden [void] postEditor([hashtable]$message) {
        $json = $null
        try { $json = ConvertTo-SRxJsonMessage -Message $message }
        catch { Write-Host "postEditor - serialize failed: $($_.Exception.Message)"; return }
        if ($this._editorJsReady -and $this._editorWebView -and $this._editorWebView.CoreWebView2) {
            try { $this._editorWebView.CoreWebView2.PostWebMessageAsJson($json) }
            catch { Write-Host "postEditor - $($_.Exception.Message)" }
        }
        else {
            $this._editorOutbox.Add($json)
        }
    }

    hidden [void] flushEditorOutbox() {
        if (-not ($this._editorWebView -and $this._editorWebView.CoreWebView2)) { return }
        $queued = $this._editorOutbox.ToArray()
        $this._editorOutbox.Clear()
        foreach ($json in $queued) {
            try { $this._editorWebView.CoreWebView2.PostWebMessageAsJson($json) }
            catch { Write-Host "flushEditorOutbox - $($_.Exception.Message)" }
        }
    }

    hidden [object] msgValue($message, [string]$key) {
        if ($null -ne $message -and $message.ContainsKey($key)) { return $message[$key] }
        return $null
    }

    hidden [void] onEditorWebMessage([string]$json) {
        $msg = ConvertFrom-SRxJsonMessage -Json $json
        if ($null -eq $msg) { return }
        $t  = [string]$this.msgValue($msg, 't')
        $id = [string]$this.msgValue($msg, 'id')
        switch ($t) {
            'ready' {
                $this._editorJsReady = $true
                $this._editorPlaceholder.Visible = $false
                if ($this._editorWebView) { $this._editorWebView.Visible = $true }
                $this.flushEditorOutbox()
                $this.status("Editor ready (Monaco: $([string]$this.msgValue($msg, 'source'))).")
            }
            'dirty' {
                $this.Editors.SetDirty($id, [bool]$this.msgValue($msg, 'dirty'))
            }
            'content' {
                $ver = 0
                try { $ver = [int]$this.msgValue($msg, 'ver') } catch { $ver = 0 }
                $this.Editors.CompleteContentRequest([string]$this.msgValue($msg, 'req'), [string]$this.msgValue($msg, 'd'), $ver)
            }
            'contentError' {
                $this.Editors.FailContentRequest([string]$this.msgValue($msg, 'req'), [string]$this.msgValue($msg, 'msg'))
            }
            'save' {
                $ed = $this.Editors.GetById($id)
                if ($null -ne $ed -and -not $ed.IsReadOnly) { [void]$ed.Save() }
            }
            'dbg' {
                $ed = $this.Editors.GetById($id)
                $cmd = [string]$this.msgValue($msg, 'cmd')
                $arg = $this.msgValue($msg, 'line')
                if ($cmd -eq 'bpsync') { $arg = @($this.msgValue($msg, 'lines')) }
                if ($null -ne $ed) { $this.raiseDebugCommand($ed, $cmd, $arg) }
            }
            'error' {
                $text = [string]$this.msgValue($msg, 'msg')
                $this.showEditorPlaceholder("Editor unavailable - $text")
                $this.status("Editor error: $text")
            }
            default { }
        }
    }

    hidden [void] showEditorPlaceholder([string]$text) {
        try {
            if ($this._editorWebView) { $this._editorWebView.Visible = $false }
            $this._editorPlaceholder.Text = $text
            $this._editorPlaceholder.Visible = $true
            $this._editorPlaceholder.BringToFront()
            if ($this._overlay -and $this._overlay.Visible) { $this._overlay.BringToFront() }
        } catch { }
    }

    hidden [void] switchView([string]$view) {
        if ([string]::IsNullOrEmpty($view)) { $view = 'terminal' }
        $this._activeView = $view
        try {
            if ($view -eq 'terminal') {
                $this._editorPanel.Visible = $false
                $this._debugPanel.Visible = $false
                $this._terminalPanel.Visible = $true
                $this._terminalPanel.BringToFront()
            }
            elseif ($view -eq 'debug') {
                $this._editorPanel.Visible = $false
                $this._terminalPanel.Visible = $false
                $this._debugPanel.Visible = $true
                $this._debugPanel.BringToFront()
                if ($this._debugWebView -and $this._debugWebView.Visible) {
                    try { $this._debugWebView.Focus() | Out-Null } catch { }
                }
                $this.postDebug(@{ t = 'focus' })
            }
            else {
                $this._terminalPanel.Visible = $false
                $this._debugPanel.Visible = $false
                $this._editorPanel.Visible = $true
                $this._editorPanel.BringToFront()
                if ($this._editorWebView -and $this._editorWebView.Visible) {
                    try { $this._editorWebView.Focus() | Out-Null } catch { }
                }
            }
        } catch { Write-Host "switchView - $($_.Exception.Message)" }
        $this.refreshTabs()
        $this.updateSaveButton()
    }

    hidden [void] requestShow() {
        try { if ($this.OnRequestShow) { $this.OnRequestShow.Invoke() } } catch { }
    }

    hidden [object] displayedEditor() {
        if ($this._activeView -eq 'terminal' -or $this._activeView -eq 'debug' -or [string]::IsNullOrEmpty($this._activeView)) { return $null }
        return $this.Editors.GetById($this._activeView)
    }

    hidden [void] addTab($editor) {
        try {
            $id = [string]$editor.Id
            if ($this._tabs.ContainsKey($id)) { return }

            $tagObj = [pscustomobject]@{ Control = $this; Editor = $editor }

            $font = $this.tabFont()
            $labelMargin = $this._titleLabel.Margin
            $tabHeight = $this.tabHeight()

            $panel = [System.Windows.Forms.Panel]::new()
            $panel.Height = $tabHeight
            $panel.Font = $font
            $panel.Margin = [System.Windows.Forms.Padding]::new(0)
            $panel.BackColor = [System.Drawing.SystemColors]::ControlLight
            $panel.Cursor = [System.Windows.Forms.Cursors]::Hand
            $panel.Tag = $tagObj

            $close = [System.Windows.Forms.PictureBox]::new()
            $close.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
            $close.Size = [System.Drawing.Size]::new(14, 14)
            $close.Location = [System.Drawing.Point]::new(6, [int][Math]::Floor(($tabHeight - 14) / 2))
            $close.BackColor = [System.Drawing.Color]::Transparent
            $close.Cursor = [System.Windows.Forms.Cursors]::Hand
            $close.Tag = $tagObj

            $label = [System.Windows.Forms.Label]::new()
            $label.AutoSize = $false
            $label.Location = [System.Drawing.Point]::new(24, 0)
            $label.Height = $tabHeight
            $label.Padding = [System.Windows.Forms.Padding]::new(0)
            $label.Margin = [System.Windows.Forms.Padding]::new(0)
            $label.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
            $label.UseCompatibleTextRendering = $false
            $label.UseMnemonic = $false
            $label.Font = $font
            $label.BackColor = [System.Drawing.Color]::Transparent
            $label.Cursor = [System.Windows.Forms.Cursors]::Hand
            $label.Text = $editor.DisplayTitle()
            $label.Tag = $tagObj

            [void]$panel.Controls.Add($close)
            [void]$panel.Controls.Add($label)

            $close.add_Click({ param($s, $e) $s.Tag.Control.onTabCloseClick($s.Tag.Editor) })
            $close.add_MouseEnter({ param($s, $e) $s.BackColor = [System.Drawing.Color]::FromArgb(205, 220, 235) })
            $close.add_MouseLeave({ param($s, $e) $s.BackColor = [System.Drawing.Color]::Transparent })
            $label.add_Click({ param($s, $e) $s.Tag.Control.onTabClick($s.Tag.Editor) })
            $panel.add_Click({ param($s, $e) $s.Tag.Control.onTabClick($s.Tag.Editor) })
            $label.add_MouseUp({ param($s, $e) if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Middle) { $s.Tag.Control.onTabCloseClick($s.Tag.Editor) } })
            $panel.add_MouseUp({ param($s, $e) if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Middle) { $s.Tag.Control.onTabCloseClick($s.Tag.Editor) } })

            $tabHost = [System.Windows.Forms.ToolStripControlHost]::new($panel)
            $tabHost.AutoSize = $false
            $tabHost.Margin = [System.Windows.Forms.Padding]::new(2, $labelMargin.Top, 0, $labelMargin.Bottom)
            $tabHost.Name = "tab_$id"

            if ($null -eq $this._tabToolTip) { $this._tabToolTip = [System.Windows.Forms.ToolTip]::new() }
            $this._tabToolTip.SetToolTip($label, $this.tabToolTipText($editor))
            $this._tabToolTip.SetToolTip($panel, $this.tabToolTipText($editor))
            $this._tabToolTip.SetToolTip($close, 'Close')

            $tab = @{ Host = $tabHost; Panel = $panel; Close = $close; Label = $label; Editor = $editor }
            $this._tabs[$id] = $tab
            $this.applyTabIcon($tab)
            $this.sizeTab($tab)

            $idx = $this._headerPanel.Items.IndexOf($this._closeButton)
            if ($idx -lt 0) { [void]$this._headerPanel.Items.Add($tabHost) }
            else { $this._headerPanel.Items.Insert($idx, $tabHost) }
        }
        catch { Write-Host "addTab - $($_.Exception.Message)" }
    }

    hidden [void] sizeTab($tab) {
        $flags = [System.Windows.Forms.TextFormatFlags]::NoPrefix -bor [System.Windows.Forms.TextFormatFlags]::SingleLine
        $w = [System.Windows.Forms.TextRenderer]::MeasureText([string]$tab.Label.Text, $tab.Label.Font, [System.Drawing.Size]::new([int]::MaxValue, [int]::MaxValue), $flags).Width
        $h = $this.tabHeight()
        $tab.Label.Size = [System.Drawing.Size]::new($w + 2, $h)
        $extra = 0
        $dbg = $tab['Dbg']
        if ($null -ne $dbg -and $dbg.Count -gt 0) {
            $x = 24 + $w + 2 + 4
            $y = [int][Math]::Floor(($h - 16) / 2)
            foreach ($b in $dbg) {
                $b.Location = [System.Drawing.Point]::new($x, $y)
                $x += 19
            }
            $extra = 4 + 19 * $dbg.Count
        }
        $tab.Panel.Size = [System.Drawing.Size]::new(24 + $w + 2 + 6 + $extra, $h)
        $tab.Close.Top = [int][Math]::Floor(($h - $tab.Close.Height) / 2)
        $tab.Host.Size = $tab.Panel.Size
    }

    hidden [object[]] debugButtonDefs() {
        return @(
            @{ Cmd = 'continue'; Icon = 'DbgContinue'; Tip = 'Continue (F5)';             Step = $true  }
            @{ Cmd = 'stepOver'; Icon = 'DbgStepOver'; Tip = 'Step Over (F10)';           Step = $true  }
            @{ Cmd = 'stepInto'; Icon = 'DbgStepInto'; Tip = 'Step Into (F11)';           Step = $true  }
            @{ Cmd = 'stepOut';  Icon = 'DbgStepOut';  Tip = 'Step Out (Shift+F11)';      Step = $true  }
            @{ Cmd = 'stop';     Icon = 'DbgStop';     Tip = 'Stop Debugger (Shift+F5)';  Step = $false }
        )
    }

    [void] SetEditorDebugControls([string]$editorId, [bool]$visible, [bool]$stopped) {
        try {
            if (-not $this._tabs.ContainsKey($editorId)) { return }
            $tab = $this._tabs[$editorId]
            $dbg = $tab['Dbg']
            if (-not $visible) {
                if ($null -ne $dbg) {
                    foreach ($b in @($dbg)) {
                        try { [void]$tab.Panel.Controls.Remove($b); $b.Dispose() } catch { }
                    }
                    $tab.Remove('Dbg')
                    $this.sizeTab($tab)
                }
                return
            }
            if ($null -eq $dbg) {
                $dbg = New-Object System.Collections.ArrayList
                if ($null -eq $this._tabToolTip) { $this._tabToolTip = [System.Windows.Forms.ToolTip]::new() }
                foreach ($def in $this.debugButtonDefs()) {
                    $pb = [System.Windows.Forms.PictureBox]::new()
                    $pb.Name = 'dbg_' + [string]$def.Cmd
                    $pb.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
                    $pb.Size = [System.Drawing.Size]::new(16, 16)
                    $pb.BackColor = [System.Drawing.Color]::Transparent
                    $pb.Cursor = [System.Windows.Forms.Cursors]::Hand
                    $pb.Tag = [pscustomobject]@{ Control = $this; Editor = $tab.Editor; Cmd = [string]$def.Cmd; Def = $def }
                    $pb.add_Click({ param($s, $e) if ($s.Enabled) { $s.Tag.Control.raiseDebugCommand($s.Tag.Editor, [string]$s.Tag.Cmd, $null) } })
                    $pb.add_MouseEnter({ param($s, $e) if ($s.Enabled) { $s.BackColor = [System.Drawing.Color]::FromArgb(205, 220, 235) } })
                    $pb.add_MouseLeave({ param($s, $e) $s.BackColor = [System.Drawing.Color]::Transparent })
                    $this._tabToolTip.SetToolTip($pb, [string]$def.Tip)
                    [void]$tab.Panel.Controls.Add($pb)
                    [void]$dbg.Add($pb)
                }
                $tab['Dbg'] = $dbg
            }
            foreach ($b in @($dbg)) {
                $def = $b.Tag.Def
                $on = (-not [bool]$def.Step) -or $stopped
                $b.Enabled = $on
                $b.Image = $this.debugBitmap([string]$def.Cmd, [string]$def.Icon, $on)
                if (-not $on) { $b.BackColor = [System.Drawing.Color]::Transparent }
            }
            $tab['DbgStopped'] = $stopped
            $this.sizeTab($tab)
        } catch { Write-Host "SetEditorDebugControls - $($_.Exception.Message)" }
    }

    [void] SetEditorDebugLine([string]$editorId, [int]$line) {
        $this.postEditor(@{ t = 'dbgline'; id = $editorId; line = $line })
    }

    [void] SetEditorBreakpoints([string]$editorId, [int[]]$lines) {
        $list = New-Object System.Collections.ArrayList
        foreach ($l in @($lines)) { if ([int]$l -gt 0) { [void]$list.Add([int]$l) } }
        $this.postEditor(@{ t = 'bps'; id = $editorId; lines = [object[]]$list.ToArray() })
    }

    [string[]] TabDebugButtons([string]$editorId) {
        $out = New-Object 'System.Collections.Generic.List[string]'
        if (-not $this._tabs.ContainsKey($editorId)) { return $out.ToArray() }
        $dbg = $this._tabs[$editorId]['Dbg']
        if ($null -eq $dbg) { return $out.ToArray() }
        foreach ($b in @($dbg)) { $out.Add(('{0}|{1}|{2}' -f $b.Tag.Cmd, $b.Enabled, $b.Tag.Def.Tip)) }
        return $out.ToArray()
    }

    hidden [void] raiseDebugCommand($editor, [string]$command, $argument) {
        if ($null -eq $this.OnDebugCommand -or $null -eq $editor -or [string]::IsNullOrEmpty($command)) { return }
        try { $this.OnDebugCommand.Invoke($editor, $command, $argument) }
        catch { Write-Host "SRxTerminalBoxControl.OnDebugCommand - $($_.Exception.Message)" }
    }

    hidden [object] debugBitmap([string]$cmd, [string]$iconKey, [bool]$enabled) {
        if ($null -eq $this._dbgBitmaps) { $this._dbgBitmaps = @{} }
        $key = $cmd + '|' + [string]$enabled
        if ($this._dbgBitmaps.ContainsKey($key)) { return $this._dbgBitmaps[$key] }
        $img = $null
        if ($this.Icons.ContainsKey($iconKey) -and $this.Icons[$iconKey]) { $img = $this.Icons[$iconKey] }
        else { $img = $this.drawDebugBitmap($cmd) }
        if (-not $enabled) {
            try { $img = [System.Windows.Forms.ToolStripRenderer]::CreateDisabledImage($img) } catch { }
        }
        $this._dbgBitmaps[$key] = $img
        return $img
    }

    hidden [object] drawDebugBitmap([string]$cmd) {
        $bmp = [System.Drawing.Bitmap]::new(32, 32)
        $g = [System.Drawing.Graphics]::FromImage($bmp)
        $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
        $green = [System.Drawing.Color]::FromArgb(56, 138, 52)
        $blue  = [System.Drawing.Color]::FromArgb(30, 111, 200)
        $red   = [System.Drawing.Color]::FromArgb(209, 52, 56)
        $dark  = [System.Drawing.Color]::FromArgb(70, 70, 70)
        $brush = $null
        $pen = $null
        try {
            switch ($cmd) {
                'continue' {
                    $brush = [System.Drawing.SolidBrush]::new($green)
                    $g.FillPolygon($brush, [System.Drawing.Point[]]@([System.Drawing.Point]::new(8, 4), [System.Drawing.Point]::new(28, 16), [System.Drawing.Point]::new(8, 28)))
                }
                'stop' {
                    $brush = [System.Drawing.SolidBrush]::new($red)
                    $g.FillRectangle($brush, 6, 6, 20, 20)
                }
                'stepOver' {
                    $pen = [System.Drawing.Pen]::new($blue, 3.0)
                    $brush = [System.Drawing.SolidBrush]::new($blue)
                    $g.DrawArc($pen, 5, 5, 22, 18, 200, 140)
                    $g.FillPolygon($brush, [System.Drawing.Point[]]@([System.Drawing.Point]::new(21, 10), [System.Drawing.Point]::new(31, 10), [System.Drawing.Point]::new(26, 17)))
                    $brush.Dispose()
                    $brush = [System.Drawing.SolidBrush]::new($dark)
                    $g.FillEllipse($brush, 12, 21, 9, 9)
                }
                'stepInto' {
                    $pen = [System.Drawing.Pen]::new($blue, 3.0)
                    $brush = [System.Drawing.SolidBrush]::new($blue)
                    $g.DrawLine($pen, 16, 2, 16, 13)
                    $g.FillPolygon($brush, [System.Drawing.Point[]]@([System.Drawing.Point]::new(9, 11), [System.Drawing.Point]::new(23, 11), [System.Drawing.Point]::new(16, 19)))
                    $brush.Dispose()
                    $brush = [System.Drawing.SolidBrush]::new($dark)
                    $g.FillEllipse($brush, 12, 21, 9, 9)
                }
                'stepOut' {
                    $pen = [System.Drawing.Pen]::new($blue, 3.0)
                    $brush = [System.Drawing.SolidBrush]::new($blue)
                    $g.DrawLine($pen, 16, 19, 16, 9)
                    $g.FillPolygon($brush, [System.Drawing.Point[]]@([System.Drawing.Point]::new(9, 10), [System.Drawing.Point]::new(23, 10), [System.Drawing.Point]::new(16, 2)))
                    $brush.Dispose()
                    $brush = [System.Drawing.SolidBrush]::new($dark)
                    $g.FillEllipse($brush, 12, 21, 9, 9)
                }
            }
        }
        finally {
            if ($null -ne $brush) { $brush.Dispose() }
            if ($null -ne $pen) { $pen.Dispose() }
            $g.Dispose()
        }
        return $bmp
    }

    [void] ShowDebugConsole([string]$title) {
        try {
            $this._debugConsoleOn = $true
            $tip = 'Debug Console'
            if (-not [string]::IsNullOrEmpty($title)) { $tip = "Debug Console - $title" }
            $this._debugLabel.ToolTipText = $tip
            $this._titleLabel.Visible = $false
            $this._debugLabel.Visible = $true
            $this.ensureDebugSurface()
            $this.postDebug(@{ t = 'clear' })
            $this.requestShow()
            $this.switchView('debug')
            $this._headerPanel.PerformLayout()
        } catch { Write-Host "ShowDebugConsole - $($_.Exception.Message)" }
    }
    [void] SelectDebugConsole() {
        if (-not $this._debugConsoleOn) { return }
        $this.requestShow()
        $this.switchView('debug')
    }
    [void] HideDebugConsole() {
        try {
            if (-not $this._debugConsoleOn) { return }
            $this._debugConsoleOn = $false
            $this._debugLabel.Visible = $false
            $this._titleLabel.Visible = $true
            if ($this._activeView -eq 'debug') {
                $this.switchView('terminal')
                if ($this._started) { $this.Activate() }
            }
            else { $this.refreshTabs() }
            $this._headerPanel.PerformLayout()
        } catch { Write-Host "HideDebugConsole - $($_.Exception.Message)" }
    }
    [void] DebugConsoleWrite([string]$stream, [string]$text) {
        $this.DebugConsoleWriteLines(@(@{ s = $stream; d = $text }))
    }
    [void] DebugConsoleWriteLines([object[]]$items) {
        $list = New-Object System.Collections.ArrayList
        foreach ($it in @($items)) {
            if ($null -eq $it) { continue }
            [void]$list.Add(@{ s = [string]$it['s']; d = [string]$it['d'] })
        }
        if ($list.Count -eq 0) { return }
        $this.postDebug(@{ t = 'lines'; items = [object[]]$list.ToArray() })
    }
    [void] DebugConsoleState([string]$mode, [string]$prompt) {
        $this.postDebug(@{ t = 'state'; mode = $mode; prompt = $prompt })
    }
    [bool] IsDebugConsoleVisible() { return $this._debugConsoleOn }
    [int] DebugConsoleColumns() { return $this._debugCols }
    [string[]] DebugConsoleOutbox() { return $this._debugOutbox.ToArray() }

    hidden [string] homeView() {
        if ($this._debugConsoleOn) { return 'debug' }
        return 'terminal'
    }

    hidden [void] ensureDebugSurface() {
        if ($this._debugStarted) { return }
        $this._debugStarted = $true
        if ($this.DisableEditorSurface) { return }
        if (-not (Import-SRxWebView2Assemblies)) {
            $this.showDebugPlaceholder("Debug Console unavailable - WebView2 SDK assemblies are not deployed. See Terminal\Lib\WebView2.")
            return
        }
        try {
            $this._debugWebView = New-Object -TypeName 'Microsoft.Web.WebView2.WinForms.WebView2'
            $this._debugWebView.Dock = [System.Windows.Forms.DockStyle]::Fill
            [void]$this._debugPanel.Controls.Add($this._debugWebView)
            $this._debugWebView.BringToFront()
            $this.showDebugPlaceholder("Debug Console is starting...")
            $this.acquireEnvironment('debug')
        }
        catch { $this.showDebugPlaceholder("Debug Console failed to start: $($_.Exception.Message)") }
    }
    hidden [void] onDebugEnvironmentReady($environment) {
        if ($null -eq $this._debugWebView) { return }
        try {
            $this._debugWebView.add_CoreWebView2InitializationCompleted({
                param($source, $e)
                $ctrl = $source.Tag
                if (-not $e.IsSuccess) {
                    $ctrl.showDebugPlaceholder("Debug Console unavailable - CoreWebView2 initialization failed: $($e.InitializationException.Message)")
                    return
                }
                $ctrl.wireDebugCoreWebView2()
            })
            $this._debugWebView.Tag = $this
            $this._debugWebView.EnsureCoreWebView2Async($environment) | Out-Null
        }
        catch { $this.showDebugPlaceholder("Debug Console failed to initialize: $($_.Exception.Message)") }
    }
    hidden [void] wireDebugCoreWebView2() {
        try {
            $core = $this._debugWebView.CoreWebView2
            $wwwPath = Join-Path $PSScriptRoot 'www'
            $kindType = Get-SRxWebView2Type 'Core.CoreWebView2HostResourceAccessKind'
            $allowKind = if ($kindType) { [Enum]::Parse($kindType, 'Allow') } else { 1 }
            $core.SetVirtualHostNameToFolderMapping('srx-terminal.local', $wwwPath, $allowKind)
            $control = $this
            $onMessage = {
                param($source, $e)
                try { $control.onDebugWebMessage($e.TryGetWebMessageAsString()) }
                catch { Write-Host "onDebugWebMessage - $($_.Exception.Message)" }
            }.GetNewClosure()
            $core.add_WebMessageReceived($onMessage)
            try { $core.Settings.AreBrowserAcceleratorKeysEnabled = $false } catch { }
            $core.Navigate('https://srx-terminal.local/debugconsole.html')
        }
        catch { $this.showDebugPlaceholder("Debug Console failed to wire CoreWebView2: $($_.Exception.Message)") }
    }
    hidden [void] postDebug([hashtable]$message) {
        $json = $null
        try { $json = ConvertTo-SRxJsonMessage -Message $message }
        catch { Write-Host "postDebug - serialize failed: $($_.Exception.Message)"; return }
        if ($this._debugJsReady -and $this._debugWebView -and $this._debugWebView.CoreWebView2) {
            try { $this._debugWebView.CoreWebView2.PostWebMessageAsJson($json) }
            catch { Write-Host "postDebug - $($_.Exception.Message)" }
        }
        else {
            $this._debugOutbox.Add($json)
            while ($this._debugOutbox.Count -gt 5000) { $this._debugOutbox.RemoveAt(0) }
        }
    }
    hidden [void] flushDebugOutbox() {
        if (-not ($this._debugWebView -and $this._debugWebView.CoreWebView2)) { return }
        $queued = $this._debugOutbox.ToArray()
        $this._debugOutbox.Clear()
        foreach ($json in $queued) {
            try { $this._debugWebView.CoreWebView2.PostWebMessageAsJson($json) }
            catch { Write-Host "flushDebugOutbox - $($_.Exception.Message)" }
        }
    }
    hidden [void] onDebugWebMessage([string]$json) {
        $msg = ConvertFrom-SRxJsonMessage -Json $json
        if ($null -eq $msg) { return }
        $t = [string]$this.msgValue($msg, 't')
        switch ($t) {
            'ready' {
                $this._debugJsReady = $true
                $this._debugPlaceholder.Visible = $false
                if ($this._debugWebView) { $this._debugWebView.Visible = $true }
                $this.setDebugCols($this.msgValue($msg, 'cols'))
                $this.flushDebugOutbox()
            }
            'resize' { $this.setDebugCols($this.msgValue($msg, 'cols')) }
            'input'  { $this.raiseDebugConsole([string]$this.msgValue($msg, 'd'), [string]$this.msgValue($msg, 'mode')) }
            'cancel' { $this.raiseDebugConsole('', 'cancel') }
            'key'    { $this.raiseDebugConsole([string]$this.msgValue($msg, 'cmd'), 'key') }
            default { }
        }
    }
    hidden [void] setDebugCols($cols) {
        $n = 0
        try { $n = [int]$cols } catch { $n = 0 }
        if ($n -ge 20) { $this._debugCols = $n }
    }
    hidden [void] raiseDebugConsole([string]$text, [string]$mode) {
        if ($null -eq $this.OnDebugConsoleInput) { return }
        try { $this.OnDebugConsoleInput.Invoke($text, $mode) }
        catch { Write-Host "SRxTerminalBoxControl.OnDebugConsoleInput - $($_.Exception.Message)" }
    }
    hidden [void] showDebugPlaceholder([string]$text) {
        try {
            if ($this._debugWebView) { $this._debugWebView.Visible = $false }
            $this._debugPlaceholder.Text = $text
            $this._debugPlaceholder.Visible = $true
            $this._debugPlaceholder.BringToFront()
        } catch { }
    }

    hidden [object] tabFont() {
        $f = $this._titleLabel.Font
        if ($null -eq $f) { $f = $this._headerPanel.Font }
        return $f
    }

    hidden [int] tabHeight() {
        $m = $this._titleLabel.Margin
        $h = $this._headerPanel.DisplayRectangle.Height - $m.Top - $m.Bottom
        if ($h -lt 16) { $h = $this._headerPanel.Height - $m.Top - $m.Bottom }
        if ($h -lt 16) { $h = 26 }
        return $h
    }

    hidden [string] tabToolTipText($editor) {
        if ($editor.IsReadOnly) { return ([string]$editor.Path + ' (read-only)') }
        return [string]$editor.Path
    }

    [object] TabLayoutInfo([string]$editorId) {
        if (-not $this._tabs.ContainsKey($editorId)) { return $null }
        $tab = $this._tabs[$editorId]
        $hostB = $tab.Host.Bounds
        return [pscustomobject]@{
            TitleFont       = $this._titleLabel.Font
            TabFont         = $tab.Label.Font
            SameFont        = $this._titleLabel.Font.Equals($tab.Label.Font)
            TitleBounds     = $this._titleLabel.Bounds
            TabBounds       = [System.Drawing.Rectangle]::new($hostB.X + $tab.Label.Left, $hostB.Y + $tab.Label.Top, $tab.Label.Width, $tab.Label.Height)
            TitleTextAlign  = $this._titleLabel.TextAlign
            TabTextAlign    = $tab.Label.TextAlign
        }
    }

    hidden [void] applyTabIcon($tab) {
        try {
            if ($this.Icons.ContainsKey('Close') -and $this.Icons['Close']) { $tab.Close.Image = $this.Icons['Close'] }
            elseif ($null -eq $tab.Close.Image) { $tab.Close.Image = $this.crossBitmap() }
        } catch { }
    }

    hidden [object] crossBitmap() {
        if ($null -eq $this._crossBitmap) {
            $bmp = [System.Drawing.Bitmap]::new(14, 14)
            $g = [System.Drawing.Graphics]::FromImage($bmp)
            $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
            $pen = [System.Drawing.Pen]::new([System.Drawing.Color]::FromArgb(60, 60, 60), 1.6)
            $g.DrawLine($pen, 3, 3, 10, 10)
            $g.DrawLine($pen, 10, 3, 3, 10)
            $pen.Dispose(); $g.Dispose()
            $this._crossBitmap = $bmp
        }
        return $this._crossBitmap
    }

    hidden [void] updateTab($editor) {
        try {
            $id = [string]$editor.Id
            if (-not $this._tabs.ContainsKey($id)) { return }
            $tab = $this._tabs[$id]
            $tab.Label.Text = $editor.DisplayTitle()
            if ($this._tabToolTip) {
                $this._tabToolTip.SetToolTip($tab.Label, $this.tabToolTipText($editor))
                $this._tabToolTip.SetToolTip($tab.Panel, $this.tabToolTipText($editor))
            }
            $this.sizeTab($tab)
        } catch { Write-Host "updateTab - $($_.Exception.Message)" }
    }

    hidden [void] removeTab($editor) {
        try {
            $id = [string]$editor.Id
            if (-not $this._tabs.ContainsKey($id)) { return }
            $tab = $this._tabs[$id]
            $this._tabs.Remove($id)
            [void]$this._headerPanel.Items.Remove($tab.Host)
            try { $tab.Host.Dispose() } catch { }
        } catch { Write-Host "removeTab - $($_.Exception.Message)" }
    }

    hidden [void] refreshTabs() {
        try {
            $activeFore   = [System.Drawing.Color]::Black
            $inactiveFore = [System.Drawing.Color]::DimGray
            $this._titleLabel.ForeColor = $(if ($this._activeView -eq 'terminal') { $activeFore } else { $inactiveFore })
            $this._debugLabel.ForeColor = $(if ($this._activeView -eq 'debug') { $activeFore } else { $inactiveFore })
            foreach ($tab in @($this._tabs.Values)) {
                $isActive = ($this._activeView -eq [string]$tab.Editor.Id)
                $tab.Panel.BackColor = $(if ($isActive) { [System.Drawing.Color]::White } else { [System.Drawing.SystemColors]::ControlLight })
                $tab.Label.ForeColor = $(if ($isActive) { $activeFore } else { $inactiveFore })
            }
        } catch { Write-Host "refreshTabs - $($_.Exception.Message)" }
    }

    [string[]] TabTitles() {
        $titles = New-Object 'System.Collections.Generic.List[string]'
        foreach ($item in $this._headerPanel.Items) {
            if ($item -is [System.Windows.Forms.ToolStripControlHost] -and $item.Name -like 'tab_*') {
                $titles.Add([string]$item.Control.Controls[1].Text)
            }
        }
        return $titles.ToArray()
    }

    hidden [void] onTabClick($editor) {
        if ($null -eq $editor -or $editor.IsClosed) { return }
        $editor.Select()
    }

    hidden [void] onTabCloseClick($editor) {
        if ($null -eq $editor -or $editor.IsClosed) { return }
        [void]$editor.Close()
    }

    hidden [void] updateSaveButton() {
        try {
            $ed = $this.displayedEditor()
            $show = $false
            $saving = $false
            if ($null -ne $ed) {
                $show = ([bool]$ed.IsDirty -and -not [bool]$ed.IsReadOnly)
                $saving = [bool]$ed.IsSaving
            }
            $this._btnSave.Visible = $show
            $this._btnSave.Enabled = ($show -and -not $saving)
            if ($show) {
                if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._btnSave.Image = $this.Icons['Save'] }
                $this._btnSave.ToolTipText = "Save $($ed.FileName) (Ctrl+S)"
            }
        } catch { Write-Host "updateSaveButton - $($_.Exception.Message)" }
    }

    [bool] IsSaveButtonVisible() { return [bool]$this._btnSave.Available }

    hidden [void] onSaveButtonClick() {
        try {
            $ed = $this.displayedEditor()
            if ($null -ne $ed -and -not $ed.IsReadOnly) { [void]$ed.Save() }
        } catch { Write-Host "onSaveButtonClick - $($_.Exception.Message)" }
    }

    hidden [void] onSaveButtonMouseEnter() {
        try {
            if (-not $this._btnSave.Enabled) { return }
            $this._btnSave.BackColor = [System.Drawing.Color]::FromArgb(225, 235, 245)
            $this._headerPanel.Cursor = [System.Windows.Forms.Cursors]::Hand
        } catch { }
    }

    hidden [void] onSaveButtonMouseLeave() {
        try {
            $this._btnSave.BackColor = [System.Drawing.Color]::Transparent
            $this._headerPanel.Cursor = [System.Windows.Forms.Cursors]::Default
        } catch { }
    }

    hidden [void] buildOverlay() {
        $this._overlay = [System.Windows.Forms.Panel]::new()
        $this._overlay.BackColor = [System.Drawing.Color]::FromArgb(250, 250, 250)
        $this._overlay.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
        $this._overlay.Visible = $false
        $this._overlay.Dock = [System.Windows.Forms.DockStyle]::Fill

        $this._overlayMsg = [System.Windows.Forms.Label]::new()
        $this._overlayMsg.AutoSize = $false
        $this._overlayMsg.Font = [System.Drawing.Font]::new('Segoe UI', 9)
        $this._overlayMsg.ForeColor = [System.Drawing.Color]::FromArgb(30, 30, 30)
        $this._overlayMsg.Location = [System.Drawing.Point]::new(16, 16)
        $this._overlayMsg.Size = [System.Drawing.Size]::new(360, 48)
        $this._overlayMsg.Text = 'You have unsaved changes.'
        [void]$this._overlay.Controls.Add($this._overlayMsg)

        $this._overlaySave = [System.Windows.Forms.Button]::new()
        $this._overlaySave.Text = 'Save changes'
        $this._overlaySave.Font = [System.Drawing.Font]::new('Segoe UI', 9)
        $this._overlaySave.Size = [System.Drawing.Size]::new(160, 30)
        $this._overlaySave.Location = [System.Drawing.Point]::new(16, 76)
        $this._overlaySave.FlatStyle = [System.Windows.Forms.FlatStyle]::System
        $this._overlaySave.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
        $this._overlaySave.ImageAlign = [System.Drawing.ContentAlignment]::MiddleLeft
        $this._overlaySave.TextImageRelation = [System.Windows.Forms.TextImageRelation]::ImageBeforeText
        $this._overlaySave.Padding = [System.Windows.Forms.Padding]::new(6, 0, 0, 0)
        $this._overlaySave.Tag = $this
        $this._overlaySave.add_Click({ param($s, $e) $s.Tag.onOverlaySave() })
        [void]$this._overlay.Controls.Add($this._overlaySave)

        $this._overlayDiscard = [System.Windows.Forms.Button]::new()
        $this._overlayDiscard.Text = 'Discard changes'
        $this._overlayDiscard.Font = [System.Drawing.Font]::new('Segoe UI', 9)
        $this._overlayDiscard.Size = [System.Drawing.Size]::new(160, 30)
        $this._overlayDiscard.Location = [System.Drawing.Point]::new(16, 112)
        $this._overlayDiscard.FlatStyle = [System.Windows.Forms.FlatStyle]::System
        $this._overlayDiscard.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
        $this._overlayDiscard.Tag = $this
        $this._overlayDiscard.add_Click({ param($s, $e) $s.Tag.onOverlayDiscard() })
        [void]$this._overlay.Controls.Add($this._overlayDiscard)

        $this._overlayCancel = [System.Windows.Forms.Button]::new()
        $this._overlayCancel.Text = 'Cancel'
        $this._overlayCancel.Font = [System.Drawing.Font]::new('Segoe UI', 9)
        $this._overlayCancel.Size = [System.Drawing.Size]::new(160, 30)
        $this._overlayCancel.Location = [System.Drawing.Point]::new(16, 148)
        $this._overlayCancel.FlatStyle = [System.Windows.Forms.FlatStyle]::System
        $this._overlayCancel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
        $this._overlayCancel.Tag = $this
        $this._overlayCancel.add_Click({ param($s, $e) $s.Tag.onOverlayCancel() })
        [void]$this._overlay.Controls.Add($this._overlayCancel)

        [void]$this._editorPanel.Controls.Add($this._overlay)
    }

    hidden [void] showOverlay($editor) {
        try {
            $this._promptEditor = $editor
            if ($this._activeView -ne [string]$editor.Id) { $this.switchView([string]$editor.Id) }
            $this._overlayMsg.Text = "You have unsaved changes in '$($editor.FileName)'.`r`nWhat would you like to do?"
            if ($this.Icons.ContainsKey('Save') -and $this.Icons['Save']) { $this._overlaySave.Image = $this.Icons['Save'] }
            $this._overlay.Dock = [System.Windows.Forms.DockStyle]::Fill
            $this._overlay.Visible = $true
            $this._overlay.BringToFront()
        } catch { Write-Host "showOverlay - $($_.Exception.Message)" }
    }

    hidden [void] hideOverlay() {
        try { $this._overlay.Visible = $false } catch { }
        $this._promptEditor = $null
    }

    [bool] IsPromptVisible() { return ($null -ne $this._promptEditor) }

    hidden [void] onOverlaySave() {
        $ed = $this._promptEditor
        $this.hideOverlay()
        if ($null -ne $ed) { $this.Editors.ResolvePrompt($ed, 'save') }
    }

    hidden [void] onOverlayDiscard() {
        $ed = $this._promptEditor
        $this.hideOverlay()
        if ($null -ne $ed) { $this.Editors.ResolvePrompt($ed, 'discard') }
    }

    hidden [void] onOverlayCancel() {
        $ed = $this._promptEditor
        $this.hideOverlay()
        if ($null -ne $ed) { $this.Editors.ResolvePrompt($ed, 'cancel') }
    }

    hidden [void] checkVSCode() {
        if ($this._vscodeChecked -or -not $this.EnsureVSCode) { return }
        $this._vscodeChecked = $true
        try {
            $det = Test-SRxVSCodeInstalled
            if ($det.Installed) { return }

            $this._vscodeInstall = Start-SRxVSCodeUserInstall -Detection $det -BuildEdition $this.VSCodeBuildEdition
            if ($null -eq $this._vscodeInstall) { return }
            $this.status("VS Code not found - installing it for the current user in the background (log: $($this._vscodeInstall.LogPath)).")

            $this._vscodeTimer = New-Object System.Windows.Forms.Timer
            $this._vscodeTimer.Interval = 2000
            $this._vscodeTimer.Tag = $this
            $this._vscodeTimer.add_Tick({ param($s, $e) $s.Tag.pollVSCodeInstall() })
            $this._vscodeTimer.Start()
        }
        catch { Write-Host "checkVSCode - $($_.Exception.Message)" }
    }

    hidden [void] pollVSCodeInstall() {
        try {
            $p = $null
            if ($this._vscodeInstall) { $p = $this._vscodeInstall.Process }
            if ($null -ne $p -and -not $p.HasExited) { return }
            if ($this._vscodeTimer) { $this._vscodeTimer.Stop(); $this._vscodeTimer.Dispose(); $this._vscodeTimer = $null }
            $code = $null
            try { if ($p) { $code = $p.ExitCode } } catch { }
            $det = Test-SRxVSCodeInstalled
            if ($det.Installed) {
                $this.status("VS Code installed for the current user ($($det.Path)).")
            }
            else {
                $this.status("VS Code installation did not complete (exit code $code) - see $($this._vscodeInstall.LogPath).")
            }
        } catch { Write-Host "pollVSCodeInstall - $($_.Exception.Message)" }
    }

    [void] Shutdown() {
        try { if ($this._startupTimer) { $this._startupTimer.Stop(); $this._startupTimer.Dispose() } } catch { }
        $this._startupTimer = $null
        try { if ($this._ioTimer) { $this._ioTimer.Stop(); $this._ioTimer.Dispose() } } catch { }
        $this._ioTimer = $null
        try { if ($this._conPty) { $this._conPty.Stop(1500) } } catch { }
        $this._conPty = $null
        try { if ($this._webView) { $this._webView.Dispose() } } catch { }
        $this._webView = $null
        try { if ($this._vscodeTimer) { $this._vscodeTimer.Stop(); $this._vscodeTimer.Dispose() } } catch { }
        $this._vscodeTimer = $null
        try { if ($this.Editors) { $this.Editors.Clear() } } catch { }
        try { if ($this._editorWebView) { $this._editorWebView.Dispose() } } catch { }
        $this._editorWebView = $null
        $this._editorJsReady = $false
        try { if ($this._tabToolTip) { $this._tabToolTip.Dispose() } } catch { }
        $this._tabToolTip = $null
        $this._dbgBitmaps = $null
        try { if ($this._debugWebView) { $this._debugWebView.Dispose() } } catch { }
        $this._debugWebView = $null
        $this._debugJsReady = $false
        $this._debugConsoleOn = $false
        try { $this._debugOutbox.Clear() } catch { }
    }
}

function New-SRxTerminalBoxControl {
    [OutputType([object])]
    param()
    return [SRxTerminalBoxControl]::new()
}

Export-ModuleMember -Function New-SRxTerminalBoxControl, Import-SRxWebView2Assemblies, Get-SRxWebView2Type, Get-SRxLoadedAssembly