src/UI/Screens/BranchManagerScreen.ps1

# BranchManagerScreen — Vista de branches del repo seleccionado.
#
# State machine de modos:
# list — navegando lista (modo default).
# filter — '/' arranca buffer de filtro inline. Lista filtra por substring CI.
# input — 'n' arranca prompt para crear branch.
# confirm — destructivos (delete local/remote, merge) piden y/N inline.
#
# Patrón: BuildLines arma frame como array de líneas (testeable sin tty); Open
# corre el loop con readkey + render a stderr. Cursor + alt buffer son
# responsabilidad del root loop (MainScreen) — esta sub-screen no los toca.
#
# Bindings (modo list):
# ↑↓ / jk navegar (con wrap)
# g / G home / end
# ↵ / s switch a la branch (noop si current)
# r / R reload
# / entrar en modo filter
# n crear branch (modo input)
# p pull (--ff-only en current)
# P push (--set-upstream auto si falta)
# d borrar local (con confirm)
# D borrar remote (con confirm)
# m merge selección en current (con confirm)
# ←/h/Esc back al MainScreen
# Q quit

class BranchManagerScreen {
    [object] $Theme
    [object] $Renderer
    [object] $Primitives
    [object] $Frame
    [object] $AppHeader
    [object] $StatusBar
    [object] $Git
    [object] $I18n = $null   # Inyectable post-construct.

    [object]   $Repo
    [object[]] $AllBranches = @()      # Resultado completo de Git.GetBranches.
    [int]      $SelectedIndex = 0
    [object]   $Viewport = [Viewport]::new()
    [bool]     $QuitRequested = $false
    [bool]     $UseAltBuffer = $true

    # Filtro persistente (sobrevive entre acciones). Vacío = sin filtro.
    [string]   $Filter = ''

    # Modo activo. Cada modo gobierna cómo se interpreta la próxima tecla.
    [string]   $Mode = 'list'   # 'list' | 'filter' | 'input' | 'confirm' | 'header'

    # Barra de chips arriba (launchers de flows v2). Ver $ChipNames para el orden.
    [int]      $ActiveChip = 0  # 0..5
    static [string[]] $ChipNames = @('Integrate', 'Quick', 'Release', 'Compare', 'Cherry-Pick', 'Graph')

    # Buffer mientras el user tipea (modo filter o input).
    [string]   $InputBuffer = ''
    [string]   $InputLabel = ''        # Prompt visible en modo input/filter.

    # Acción pendiente que se ejecuta si el user confirma o termina input.
    # Hashtable @{ Kind='create'|'delete'|'deleteRemote'|'merge'; Branch=$obj }
    [hashtable] $Pending = $null

    # Status del último resultado (vacío al cambiar de modo o en nueva acción).
    [string]   $StatusMessage = ''
    [string]   $StatusKind = ''        # 'ok' | 'error' | ''

    BranchManagerScreen($theme, $renderer, $primitives, $frame, $appHeader, $statusBar, $git) {
        $this.Theme       = $theme
        $this.Renderer    = $renderer
        $this.Primitives  = $primitives
        $this.Frame       = $frame
        $this.AppHeader   = $appHeader
        $this.StatusBar   = $statusBar
        $this.Git         = $git
    }

    [void] Open([object]$repo) {
        if ($null -eq $repo) { throw "BranchManagerScreen.Open: repo no puede ser null." }

        $this.Repo = $repo
        $this.SelectedIndex = 0
        $this.Viewport.Reset()
        $this.StatusMessage = ''
        $this.StatusKind = ''
        $this.Filter = ''
        $this.Mode = 'list'
        $this.ActiveChip = 0
        $this.InputBuffer = ''
        $this.Pending = $null
        # Branches vacío al inicio — se cargan después del primer frame para que la
        # transición desde MainScreen sea instantánea.
        $this.AllBranches = @()

        if ([Console]::IsInputRedirected) {
            $this.LoadBranches()
            $lines = $this.BuildLines('1.0.0')
            foreach ($l in $lines) { Write-Host $l }
            return
        }

        # Pre-frame INSTANTÁNEO que pisa el header del MainScreen antes de hacer
        # el git call. Sin esto, el header de la vista anterior queda visible
        # mientras LoadBranches corre (git for-each-ref puede tomar 50-200ms en
        # repos con muchas branches) — Tino lo reportó como parpadeo.
        $errOut = [Console]::Error
        $this.StatusMessage = 'cargando branches…'
        $this.StatusKind = 'ok'
        $earlyFrame = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + (($this.BuildLines('1.0.0')) -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync()
        $errOut.Write($earlyFrame)
        $errOut.Flush()

        # Ahora sí cargamos las branches reales.
        $this.LoadBranches()
        $this.StatusMessage = ''
        $this.StatusKind = ''

        try {
            while ($true) {
                $this.EnsureViewport()
                $lines = $this.BuildLines('1.0.0')
                $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($lines -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync()
                $errOut.Write($framePayload)

                $key = [Console]::ReadKey($true)

                $exit = $this.HandleKey($key)
                if ($exit -eq 'back') { return }
                if ($exit -eq 'quit') { $this.QuitRequested = $true; return }
            }
        }
        finally {
            # Sub-screen: ni cursor ni alt buffer — eso es del root loop.
        }
    }

    # Maneja una tecla según el modo activo.
    # Retorna 'back', 'quit', o '' para seguir en el loop.
    hidden [string] HandleKey([System.ConsoleKeyInfo]$key) {
        switch ($this.Mode) {
            'list'    { return $this.HandleKeyList($key) }
            'header'  { return $this.HandleKeyHeader($key) }
            'filter'  { return $this.HandleKeyFilter($key) }
            'input'   { return $this.HandleKeyInput($key) }
            'confirm' { return $this.HandleKeyConfirm($key) }
        }
        return ''
    }

    hidden [string] HandleKeyList([System.ConsoleKeyInfo]$key) {
        $k = $key.Key
        $c = $key.KeyChar
        $branches = $this.FilteredBranches()

        if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' }
        if ($k -eq 'LeftArrow' -or $c -eq 'h' -or $k -eq 'Escape') {
            # Si hay filtro activo, primer Esc lo limpia antes de salir.
            if ($this.Filter) {
                $this.Filter = ''
                $this.SelectedIndex = 0
                return ''
            }
            return 'back'
        }

        # Navegación
        if ($k -eq 'UpArrow' -or $c -eq 'k') {
            if ($branches.Count -gt 0) {
                $this.SelectedIndex = if ($this.SelectedIndex -le 0) { $branches.Count - 1 } else { $this.SelectedIndex - 1 }
            }
            return ''
        }
        if ($k -eq 'DownArrow' -or $c -eq 'j') {
            if ($branches.Count -gt 0) {
                $this.SelectedIndex = if ($this.SelectedIndex -ge $branches.Count - 1) { 0 } else { $this.SelectedIndex + 1 }
            }
            return ''
        }
        if ($k -eq 'Home' -or $c -eq 'g') { $this.SelectedIndex = 0; return '' }
        if ($k -eq 'End'  -or $c -eq 'G') { $this.SelectedIndex = [Math]::Max(0, $branches.Count - 1); return '' }

        # Tab: pasar focus a la barra de chips.
        if ($k -eq 'Tab') {
            $this.Mode = 'header'
            return ''
        }

        # Filter
        if ($c -eq '/') {
            $this.Mode = 'filter'
            $this.InputBuffer = $this.Filter
            $this.InputLabel = 'Filtrar'
            return ''
        }

        # Switch
        if ($k -eq 'Enter' -or $c -eq 's') { $this.SwitchSelected(); return '' }

        # Reload
        if ($c -eq 'r' -or $c -eq 'R') {
            $this.LoadBranches()
            $this.SetStatus('lista actualizada', 'ok')
            return ''
        }

        # Crear branch (input)
        if ($c -eq 'n') {
            $this.Mode = 'input'
            $this.InputBuffer = ''
            $this.InputLabel = 'Nueva branch'
            $this.Pending = @{ Kind = 'create' }
            return ''
        }

        # Pull
        if ($c -eq 'p') { $this.DoPull(); return '' }

        # Push
        if ($c -eq 'P') { $this.DoPush($false); return '' }

        # Delete local (confirm)
        if ($c -eq 'd') {
            if ($branches.Count -eq 0) { return '' }
            $b = $branches[$this.SelectedIndex]
            if ($b.IsCurrent) { $this.SetStatus('no podés borrar la branch actual', 'error'); return '' }
            $this.Pending = @{ Kind = 'delete'; Branch = $b }
            $this.Mode = 'confirm'
            return ''
        }

        # Delete remote (confirm)
        if ($c -eq 'D') {
            if ($branches.Count -eq 0) { return '' }
            $b = $branches[$this.SelectedIndex]
            if (-not $b.Remote) { $this.SetStatus("'$($b.Name)' no tiene remote", 'error'); return '' }
            $this.Pending = @{ Kind = 'deleteRemote'; Branch = $b }
            $this.Mode = 'confirm'
            return ''
        }

        # Merge into current
        if ($c -eq 'm') {
            if ($branches.Count -eq 0) { return '' }
            $b = $branches[$this.SelectedIndex]
            if ($b.IsCurrent) { $this.SetStatus('no podés mergear la branch actual en sí misma', 'error'); return '' }
            $this.Pending = @{ Kind = 'merge'; Branch = $b }
            $this.Mode = 'confirm'
            return ''
        }

        return ''
    }

    # Modo Header (focus en la barra de chips arriba). ←→ navega chips, Enter
    # abre el flow correspondiente, ↑/Esc/Tab/k vuelve a list.
    hidden [string] HandleKeyHeader([System.ConsoleKeyInfo]$key) {
        $k = $key.Key
        $c = $key.KeyChar

        if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' }
        if ($k -eq 'Tab' -or $k -eq 'Escape' -or $k -eq 'UpArrow' -or $c -eq 'k') {
            $this.Mode = 'list'
            return ''
        }

        if ($k -eq 'LeftArrow' -or $c -eq 'h') {
            $count = [BranchManagerScreen]::ChipNames.Count
            $this.ActiveChip = ($this.ActiveChip - 1 + $count) % $count
            return ''
        }
        if ($k -eq 'RightArrow' -or $c -eq 'l') {
            $count = [BranchManagerScreen]::ChipNames.Count
            $this.ActiveChip = ($this.ActiveChip + 1) % $count
            return ''
        }
        if ($k -eq 'Enter') {
            $this.LaunchActiveChip()
            return ''
        }
        return ''
    }

    # Lanza la screen del chip activo. Cada flow es una sub-screen completa con
    # su propio loop. Al volver, refrescamos branches por si cambiaron.
    hidden [void] LaunchActiveChip() {
        $name = [BranchManagerScreen]::ChipNames[$this.ActiveChip]
        $this.Mode = 'list'

        if ($name -eq 'Integrate') {
            $screen = [IntegrateScreen]::new(
                $this.Theme, $this.Renderer, $this.Primitives,
                $this.Frame, $this.AppHeader, $this.StatusBar, $this.Git)
            $screen.UseAltBuffer = $this.UseAltBuffer
            $screen.I18n = $this.I18n
            $screen.Open($this.Repo)
            if ($screen.QuitRequested) { $this.QuitRequested = $true; return }
            $this.LoadBranches()
            if ($screen.DoneMessage) {
                $kind = if ($screen.StatusKind) { $screen.StatusKind } else { 'ok' }
                $this.SetStatus($screen.DoneMessage, $kind)
            }
            return
        }

        if ($name -eq 'Quick') {
            $screen = [QuickChangeScreen]::new(
                $this.Theme, $this.Renderer, $this.Primitives,
                $this.Frame, $this.AppHeader, $this.StatusBar, $this.Git)
            $screen.UseAltBuffer = $this.UseAltBuffer
            $screen.I18n = $this.I18n
            $screen.Open($this.Repo)
            if ($screen.QuitRequested) { $this.QuitRequested = $true; return }
            $this.LoadBranches()
            if ($screen.StatusMessage) {
                $kind = if ($screen.StatusKind) { $screen.StatusKind } else { 'ok' }
                $this.SetStatus($screen.StatusMessage, $kind)
            }
            return
        }

        if ($name -eq 'Release') {
            $screen = [ReleaseScreen]::new(
                $this.Theme, $this.Renderer, $this.Primitives,
                $this.Frame, $this.AppHeader, $this.StatusBar, $this.Git)
            $screen.UseAltBuffer = $this.UseAltBuffer
            $screen.I18n = $this.I18n
            $screen.Open($this.Repo)
            if ($screen.QuitRequested) { $this.QuitRequested = $true; return }
            if ($screen.StatusMessage) {
                $kind = if ($screen.StatusKind) { $screen.StatusKind } else { 'ok' }
                $this.SetStatus($screen.StatusMessage, $kind)
            }
            return
        }

        if ($name -eq 'Graph') {
            $screen = [GraphScreen]::new(
                $this.Theme, $this.Renderer, $this.Primitives,
                $this.Frame, $this.AppHeader, $this.StatusBar, $this.Git)
            $screen.UseAltBuffer = $this.UseAltBuffer
            $screen.I18n = $this.I18n
            $screen.Open($this.Repo)
            if ($screen.QuitRequested) { $this.QuitRequested = $true; return }
            return
        }

        if ($name -eq 'Cherry-Pick') {
            $screen = [CherryPickScreen]::new(
                $this.Theme, $this.Renderer, $this.Primitives,
                $this.Frame, $this.AppHeader, $this.StatusBar, $this.Git)
            $screen.UseAltBuffer = $this.UseAltBuffer
            $screen.I18n = $this.I18n
            $screen.Open($this.Repo)
            if ($screen.QuitRequested) { $this.QuitRequested = $true; return }
            if ($screen.PickedCount -gt 0) {
                $this.LoadBranches()
                $this.SetStatus("$($screen.PickedCount) commit(s) cherry-pickeados", 'ok')
            }
            return
        }

        if ($name -eq 'Compare') {
            $screen = [CompareScreen]::new(
                $this.Theme, $this.Renderer, $this.Primitives,
                $this.Frame, $this.AppHeader, $this.StatusBar, $this.Git)
            $screen.UseAltBuffer = $this.UseAltBuffer
            $screen.I18n = $this.I18n
            $screen.Open($this.Repo)
            if ($screen.QuitRequested) { $this.QuitRequested = $true; return }
            return
        }

        # Todos los chips wired. Si llega acá, programmer error.
        $this.SetStatus("chip '$name' sin handler — bug en LaunchActiveChip", 'error')
    }

    hidden [string] HandleKeyFilter([System.ConsoleKeyInfo]$key) {
        $k = $key.Key
        $c = $key.KeyChar

        if ($k -eq 'Escape') {
            # Cancelar: limpia el filtro y vuelve.
            $this.Filter = ''
            $this.InputBuffer = ''
            $this.Mode = 'list'
            $this.SelectedIndex = 0
            return ''
        }
        if ($k -eq 'Enter') {
            $this.Filter = $this.InputBuffer
            $this.InputBuffer = ''
            $this.Mode = 'list'
            $this.SelectedIndex = 0
            return ''
        }
        if ($k -eq 'Backspace') {
            if ($this.InputBuffer.Length -gt 0) {
                $this.InputBuffer = $this.InputBuffer.Substring(0, $this.InputBuffer.Length - 1)
            }
            # Filtro live mientras tipea
            $this.Filter = $this.InputBuffer
            $this.SelectedIndex = 0
            return ''
        }

        # Char printable → append + filter live
        if (-not [char]::IsControl($c)) {
            $this.InputBuffer += $c
            $this.Filter = $this.InputBuffer
            $this.SelectedIndex = 0
        }
        return ''
    }

    hidden [string] HandleKeyInput([System.ConsoleKeyInfo]$key) {
        $k = $key.Key
        $c = $key.KeyChar

        if ($k -eq 'Escape') {
            $this.Mode = 'list'
            $this.InputBuffer = ''
            $this.Pending = $null
            return ''
        }
        if ($k -eq 'Enter') {
            $this.RunPendingFromInput()
            return ''
        }
        if ($k -eq 'Backspace') {
            if ($this.InputBuffer.Length -gt 0) {
                $this.InputBuffer = $this.InputBuffer.Substring(0, $this.InputBuffer.Length - 1)
            }
            return ''
        }
        if (-not [char]::IsControl($c)) {
            $this.InputBuffer += $c
        }
        return ''
    }

    hidden [string] HandleKeyConfirm([System.ConsoleKeyInfo]$key) {
        $c = $key.KeyChar

        if ($c -eq 'y' -or $c -eq 'Y') {
            $this.RunPendingDestructive()
        } else {
            # Cualquier otra tecla cancela (incluido Esc, n, N, espacio).
            $this.Mode = 'list'
            $this.Pending = $null
        }
        return ''
    }

    hidden [void] RunPendingFromInput() {
        if (-not $this.Pending) { $this.Mode = 'list'; return }
        $kind = $this.Pending.Kind
        $name = $this.InputBuffer.Trim()

        if ($kind -eq 'create') {
            if (-not $name) {
                $this.SetStatus('nombre vacío — cancelado', 'error')
                $this.Mode = 'list'
                $this.Pending = $null
                return
            }
            $r = $this.Git.CreateBranch($this.Repo.Path, $name, $true)
            $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' })))
            if ($r.Ok) { $this.LoadBranches() }
        }

        $this.Mode = 'list'
        $this.InputBuffer = ''
        $this.Pending = $null
    }

    hidden [void] RunPendingDestructive() {
        if (-not $this.Pending) { $this.Mode = 'list'; return }
        $kind = $this.Pending.Kind
        $b    = $this.Pending.Branch
        $r    = $null

        switch ($kind) {
            'delete'       { $r = $this.Git.DeleteLocalBranch($this.Repo.Path, $b.Name, $false) }
            'deleteRemote' { $r = $this.Git.DeleteRemoteBranch($this.Repo.Path, $b.Name, $b.Remote) }
            'merge'        { $r = $this.Git.MergeBranch($this.Repo.Path, $b.Name) }
        }

        if ($r) {
            $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' })))
            if ($r.Ok) { $this.LoadBranches() }
        }
        $this.Mode = 'list'
        $this.Pending = $null
    }

    hidden [void] DoPull() {
        $r = $this.Git.Pull($this.Repo.Path)
        $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' })))
        if ($r.Ok) { $this.LoadBranches() }
    }

    hidden [void] DoPush([bool]$noVerify) {
        $r = $this.Git.Push($this.Repo.Path, $noVerify)
        $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' })))
        if ($r.Ok) { $this.LoadBranches() }
    }

    # Re-render inmediato del frame con el status actualizado, para que el user
    # vea el progreso del flow entre pasos. Sin esto, los handlers son
    # sincrónicos: el Write del frame solo ocurre al final del while iteration,
    # entonces el user solo ve el resultado final, no los pasos intermedios.
    hidden [void] FlashStatus([string]$msg, [string]$kind) {
        $this.SetStatus($msg, $kind)
        if ([Console]::IsInputRedirected) { return }
        $errOut = [Console]::Error
        $lines = $this.BuildLines('1.0.0')
        $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($lines -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync()
        $errOut.Write($framePayload)
        $errOut.Flush()
    }

    hidden [void] SwitchSelected() {
        $branches = $this.FilteredBranches()
        if ($branches.Count -eq 0) { return }
        $target = $branches[$this.SelectedIndex]
        if ($target.IsCurrent) { $this.SetStatus("ya estás en $($target.Name)", 'ok'); return }
        $r = $this.Git.CheckoutBranch($this.Repo.Path, $target.Name)
        $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' })))
        if ($r.Ok) { $this.LoadBranches() }
    }

    hidden [void] SetStatus([string]$msg, [string]$kind) {
        $this.StatusMessage = $msg
        $this.StatusKind = $kind
    }

    hidden [void] LoadBranches() {
        if ($null -eq $this.Repo -or -not $this.Repo.Path) {
            $this.AllBranches = @()
            return
        }
        $this.AllBranches = $this.Git.GetBranches($this.Repo.Path)
        # Clamp del SelectedIndex contra la lista filtrada.
        $count = $this.FilteredBranches().Count
        if ($this.SelectedIndex -ge $count) {
            $this.SelectedIndex = [Math]::Max(0, $count - 1)
        }
    }

    # Devuelve la lista filtrada por $this.Filter (substring CI sobre Name).
    # Si filter vacío, devuelve AllBranches.
    [object[]] FilteredBranches() {
        if (-not $this.Filter) { return $this.AllBranches }
        $needle = $this.Filter.ToLowerInvariant()
        return @($this.AllBranches | Where-Object { $_.Name.ToLowerInvariant().Contains($needle) })
    }

    hidden [void] EnsureViewport() {
        $count = $this.FilteredBranches().Count
        if ($count -eq 0) { $this.Viewport.Reset(); return }
        # Chrome:
        # 1 titlebar + 1 appheader + 1 hr + 1 chips bar + 1 hr + 1 prompt/status
        # + 1 section title + 1 hr + 1 hr + 1 statusbar = 10 (sumando el hr interno)
        # En la práctica ajustado a 9 que era 7 antes + 2 por la chips bar y su hr.
        # +1 a chrome porque StatusBar ahora son 2 líneas (hints + counts/right).
        $available = [Console]::WindowHeight - 10
        $this.Viewport.Resize($available, 1)
        $this.Viewport.EnsureVisible($this.SelectedIndex, $count)
    }

    [string[]] BuildLines([string]$version) {
        $reset = [AnsiService]::Reset
        $r = $this.Renderer
        $branches = $this.FilteredBranches()
        $lines = [System.Collections.Generic.List[string]]::new()

        # 1. Title bar
        $repoName = if ($this.Repo) { $this.Repo.Name } else { '—' }
        $lines.Add($this.Frame.TitleBar('repo-nav', "Branches · $repoName", $version))

        # 2. AppHeader: breadcrumb del path + 'Branches' como current.
        $bc = [BreadcrumbBuilder]::Build($this.Repo.Path)
        $segs = @($bc.Segs) + @($bc.Current)
        $lines.Add($this.AppHeader.Render($segs, 'Branches', @()))
        $lines.Add($r.HRule())

        # 2b. Barra de chips (launchers de flows v2). Siempre visible. El chip
        # activo se destaca como pill cuando focus=Header; en focus=List todos
        # se ven dim para indicar que no son interactivos en ese modo.
        $lines.Add($this.RenderChipsBar())
        $lines.Add($r.HRule())

        # 3. Línea de prompt o status (altura fija para que la lista no salte).
        $lines.Add($this.RenderPromptOrStatus())

        # 4. Section title — agrega filter info y conteos.
        $titleText = ' Branches · ' + $branches.Count
        if ($this.Filter -and $branches.Count -ne $this.AllBranches.Count) {
            $titleText += ' de ' + $this.AllBranches.Count + " · /$($this.Filter)/"
        } elseif ($this.Filter) {
            $titleText += " · /$($this.Filter)/"
        }
        $lines.Add($this.Theme.Fg('fg2') + $titleText + $reset)
        $lines.Add($r.HRule())

        # 5. Filas
        if ($null -ne $this.Viewport -and $this.Viewport.Capacity -gt 0 -and -not [Console]::IsInputRedirected) {
            $sliceStart = $this.Viewport.Start
            $sliceEnd   = $this.Viewport.EndExclusive($branches.Count)
        } else {
            $sliceStart = 0
            $sliceEnd   = $branches.Count
        }

        if ($branches.Count -eq 0) {
            $msg = if ($this.Filter) { " (sin matches para /$($this.Filter)/)" } else { ' (no hay branches)' }
            $lines.Add($this.Theme.Fg('fg3') + $msg + $reset)
        } else {
            for ($i = $sliceStart; $i -lt $sliceEnd; $i++) {
                $b = $branches[$i]
                $lines.Add($this.RenderBranchRow($b, ($i -eq $this.SelectedIndex)))
            }
        }

        # 6. StatusBar — hints según el modo.
        $hints = $this.StatusBarHints()
        $lines.Add($r.HRule())
        return @($lines.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $lines.Count)
    }

    # Línea entre el header y la sección de title. Muestra prompt activo,
    # confirmación pendiente, status del último resultado, o vacío.
    # Render de la barra de chips. focus=Header destaca el activo como pill;
    # focus=List atenua todos a fg3 (visualmente "no interactivos ahora").
    hidden [string] RenderChipsBar() {
        $reset = [AnsiService]::Reset
        $names = [BranchManagerScreen]::ChipNames
        $isHeaderFocus = ($this.Mode -eq 'header')
        $sepFg = if ($isHeaderFocus) { $this.Theme.Fg('fg3') } else { $this.Theme.Fg('fg3') }
        $sep = $sepFg + ' · ' + $reset

        $parts = @()
        for ($i = 0; $i -lt $names.Count; $i++) {
            $name = $names[$i]
            if ($isHeaderFocus -and $i -eq $this.ActiveChip) {
                $bg = $this.Theme.Bg('acc')
                $fg = $this.Theme.Fg('bg0')
                $parts += "${bg}${fg} $name ${reset}"
            } elseif ($isHeaderFocus) {
                $parts += $this.Theme.Fg('fg1') + $name + $reset
            } else {
                $parts += $this.Theme.Fg('fg3') + $name + $reset
            }
        }
        return ' ' + ($parts -join $sep)
    }

    hidden [string] RenderPromptOrStatus() {
        $reset = [AnsiService]::Reset

        if ($this.Mode -eq 'filter' -or $this.Mode -eq 'input') {
            # Prompt: 'Label: |buffer_'
            $cursor = $this.Theme.Fg('acc') + '_' + $reset
            $label  = $this.Theme.Fg('fg2') + ($this.InputLabel + ': ') + $reset
            $buf    = $this.Theme.Fg('fg1') + $this.InputBuffer + $reset
            return ' ' + $label + $buf + $cursor
        }

        if ($this.Mode -eq 'confirm' -and $this.Pending) {
            $kind   = $this.Pending.Kind
            $b      = $this.Pending.Branch
            $prompt = switch ($kind) {
                'delete'       { "Borrar local '$($b.Name)'? (y/N)" }
                'deleteRemote' { "Borrar del remote '$($b.Remote)/$($b.Name)'? (y/N)" }
                'merge'        { "Mergear '$($b.Name)' en current? (y/N)" }
                default        { '?' }
            }
            return ' ' + $this.Theme.Fg('gitDirty') + $prompt + $reset
        }

        if ($this.StatusMessage) {
            $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' }
            return ' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset
        }

        return ''
    }

    # Helper i18n con fallback graceful (mismo patrón que MainScreen.T).
    hidden [string] T([string]$key) {
        if ($null -ne $this.I18n) { return $this.I18n.T($key) }
        return ([I18nService]::new('es')).T($key)
    }

    hidden [array] StatusBarHints() {
        switch ($this.Mode) {
            'header' {
                return @(
                    @{ k = '←→';  label = $this.T('hint.tab') }
                    @{ k = '↵';   label = $this.T('hint.open') }
                    @{ k = '↑';   label = $this.T('hint.back') }
                    @{ k = 'Esc'; label = $this.T('hint.back') }
                    @{ k = 'Q';   label = $this.T('hint.quit') }
                )
            }
            'filter' {
                return @(
                    @{ k = $this.T('hint.type'); label = $this.T('hint.search') }
                    @{ k = '↵';                  label = $this.T('hint.accept') }
                    @{ k = 'Esc';                label = $this.T('hint.cancel') }
                )
            }
            'input' {
                return @(
                    @{ k = $this.T('hint.type'); label = $this.T('hint.save') }
                    @{ k = '↵';                  label = $this.T('hint.accept') }
                    @{ k = 'Esc';                label = $this.T('hint.cancel') }
                )
            }
            'confirm' {
                return @(
                    @{ k = 'Y'; label = $this.T('common.yes') }
                    @{ k = '*'; label = $this.T('common.no') }
                )
            }
            default {
                # 'list' — bindings completos.
                return @(
                    @{ k = '↑↓'; label = $this.T('hint.move') }
                    @{ k = '↵';  label = 'Switch' }
                    @{ k = 'Tab';label = 'Flows ↑' }
                    @{ k = '/';  label = $this.T('hint.search') }
                    @{ k = 'n';  label = 'New' }
                    @{ k = 'p';  label = 'Pull' }
                    @{ k = 'P';  label = 'Push' }
                    @{ k = 'd';  label = 'Del' }
                    @{ k = 'D';  label = 'DelRm' }
                    @{ k = 'm';  label = 'Merge' }
                    @{ k = 'R';  label = $this.T('hint.reload') }
                    @{ k = '←';  label = $this.T('hint.back') }
                    @{ k = 'Q';  label = $this.T('hint.quit') }
                )
            }
        }
        return @()
    }

    hidden [string] RenderBranchRow([object]$b, [bool]$selected) {
        $reset = [AnsiService]::Reset

        $marker = if ($selected) { $this.Theme.Fg('acc') + '▎' + $reset } else { ' ' }

        $dot = if ($b.IsCurrent) {
            $this.Theme.Fg('acc') + '●' + $reset
        } else {
            $this.Theme.Fg('fg3') + '·' + $reset
        }

        $nameColor = if ($b.IsCurrent) { 'acc' }
                     elseif ($b.IsProtected) { 'fg2' }
                     else { 'fg1' }
        $name = $this.Theme.Fg($nameColor) + $b.Name + $reset

        $tracking = if (-not $b.Remote) {
            $this.Theme.Fg('fg3') + '(local)' + $reset
        } else {
            $aheadStr  = if ($b.Ahead -gt 0)  { $this.Theme.Fg('gitAhead') + '▲' + $b.Ahead + $reset } else { '' }
            $behindStr = if ($b.Behind -gt 0) { $this.Theme.Fg('gitBehind') + '▼' + $b.Behind + $reset } else { '' }
            $sep = if ($aheadStr -and $behindStr) { ' ' } else { '' }
            if ($aheadStr -or $behindStr) { ($aheadStr + $sep + $behindStr) }
            else { $this.Theme.Fg('fg3') + '·' + $reset }
        }

        $meta = ''
        if ($b.LastCommitAuthor -or $b.LastCommitDate) {
            $author = if ($b.LastCommitAuthor) { $b.LastCommitAuthor } else { '' }
            $date   = if ($b.LastCommitDate)   { $b.LastCommitDate }   else { '' }
            $sep    = if ($author -and $date) { ' · ' } else { '' }
            $meta = $this.Theme.Fg('fg3') + ($author + $sep + $date) + $reset
        }

        $proto = if ($b.IsProtected) { ' ' + $this.Primitives.Pill('protected', 'fg2') } else { '' }

        return "$marker $dot $name $tracking $meta$proto"
    }
}