public/Invoke-Octa.ps1

function Wait-OctaKeypress {
    <#
        Shared "press any key to return to the menu" step after running a category/tool -
        factored out so every action site (categories, tools, Quick Clean, Undo, Help) uses the
        exact same prompt instead of repeating it.
    #>

    param([Parameter(Mandatory)]$Strings)
    Write-Host ''
    Write-Host -NoNewline $Strings.pressKeyToContinue
    [Console]::ReadKey($true) | Out-Null
}

function Invoke-Octa {
    <#
        .SYNOPSIS
        Interactive entry point: language menu (first run) -> session-level restore point gate
        (010, every run) -> banner + main menu -> dispatch, looping back to the menu after each
        action so the whole session (every category, the built-in tools, undo) happens from this
        one running terminal - the user never needs to re-invoke bin/octa.ps1 with a different
        flag. FR-018, FR-022, FR-023.
 
        010: the main menu is Quick Assist (today's Quick Clean bundle, unchanged) and Quick
        Custom (wraps the Categorías/Herramientas pickers that used to sit at this top level) -
        see specs/010-quick-assist-restore-point. The restore-point decision is made exactly once
        per session, before this menu ever renders (Invoke-OctaSessionRestorePointGate), so every
        apply call below passes -NoRestorePoint - re-attempting per action was pure repeated
        friction given Windows' own 24h creation-frequency throttle.
 
        009/010: categories and built-in tools are grouped (main menu -> group picker -> leaf
        picker) instead of one flat 32-item list, following real user feedback that a flat list
        got overwhelming as more categories/tools were added. Running a leaf item returns to the
        group screen it was picked from, not all the way back to the top-level main menu.
 
        010: every screen (a menu, or a running action's own output) is drawn through
        Show-OctaFrame, which redraws in place relative to the current cursor position (not a
        stored absolute row - see Show-OctaFrame's own comment for why) so the whole session
        feels like one fixed, updating screen instead of each action's output piling up in the
        terminal's ordinary scrollback history.
 
        ponytail: direct single-keypress shortcuts are guaranteed unique for items 1-9 within
        any one screen (main menu, a group picker, or a leaf picker never has more than 9 items
        at once); items 10+ are still always reachable via Up/Down + Enter, just without a
        guaranteed single-digit direct key.
    #>

    [CmdletBinding()]
    param()

    $strings = Get-OctaStrings

    # 010: restore point moved to once per session, right after language resolves and before the
    # main menu ever renders - see private/RestorePoint.ps1's Invoke-OctaSessionRestorePointGate
    # for why. Declining here exits before anything else in the session happens.
    if (-not (Invoke-OctaSessionRestorePointGate -Strings $strings)) {
        return
    }

    # -SkipRiskScan: this menu never displays HighestRiskLevel (only DisplayName/Description),
    # so the ~20s it took to live-scan all 20 categories just to compute a badge nothing read
    # was pure waste - see Get-OctaCategory's own comment for how this was found.
    $categories = @(Get-OctaCategory -SkipRiskScan)
    Reset-OctaFrameAnchor

    $categoryGroups = @(
        [pscustomobject]@{ Key = '1'; Label = $strings.groupPrivacyAi; Description = $strings.groupPrivacyAiDesc; Ids = @('telemetry', 'background-activity', 'copilot', 'recall', 'ai-features') }
        [pscustomobject]@{ Key = '2'; Label = $strings.groupBloatware; Description = $strings.groupBloatwareDesc; Ids = @('appx', 'oem-suites', 'onedrive', 'widgets') }
        [pscustomobject]@{ Key = '3'; Label = $strings.groupPerformance; Description = $strings.groupPerformanceDesc; Ids = @('performance', 'power', 'gpu', 'services') }
        [pscustomobject]@{ Key = '4'; Label = $strings.groupDesktop; Description = $strings.groupDesktopDesc; Ids = @('explorer-start', 'desktop', 'preferences', 'user-experience', 'gaming') }
        [pscustomobject]@{ Key = '5'; Label = $strings.groupAdvanced; Description = $strings.groupAdvancedDesc; Ids = @('windows-features', 'registry-orphans') }
    )

    $toolCatalog = @(
        [pscustomobject]@{ Id = 'disk-cleanup'; Label = $strings.menuDiskCleanup; Description = $strings.descDiskCleanup; Action = { Invoke-OctaDiskCleanup -NoRestorePoint:$true | Out-Null } }
        [pscustomobject]@{ Id = 'disk-usage'; Label = $strings.menuDiskUsage; Description = $strings.descDiskUsage; Action = { Show-OctaDiskUsage | Out-Null } }
        [pscustomobject]@{ Id = 'disk-health'; Label = $strings.menuDiskHealth; Description = $strings.descDiskHealth; Action = { Show-OctaDiskHealth | Out-Null } }
        [pscustomobject]@{ Id = 'uninstaller'; Label = $strings.menuUninstaller; Description = $strings.descUninstaller; Action = { Invoke-OctaUninstaller | Out-Null } }
        [pscustomobject]@{ Id = 'startup-manager'; Label = $strings.menuStartupManager; Description = $strings.descStartupManager; Action = { Invoke-OctaStartupManager | Out-Null } }
        [pscustomobject]@{ Id = 'tasks'; Label = $strings.menuTasks; Description = $strings.descTasks; Action = { Invoke-OctaTaskBrowser | Out-Null } }
        [pscustomobject]@{ Id = 'scheduler'; Label = $strings.menuScheduler; Description = $strings.descScheduler; Action = { Invoke-OctaScheduler | Out-Null } }
        [pscustomobject]@{ Id = 'software-updater'; Label = $strings.menuSoftwareUpdater; Description = $strings.descSoftwareUpdater; Action = { Invoke-OctaSoftwareUpdater | Out-Null } }
        [pscustomobject]@{ Id = 'dashboard'; Label = $strings.menuDashboard; Description = $strings.descDashboard; Action = { Show-OctaDashboard | Out-Null } }
        [pscustomobject]@{ Id = 'history'; Label = $strings.menuHistory; Description = $strings.descHistory; Action = { Show-OctaHistory | Out-Null } }
        [pscustomobject]@{ Id = 'network-cleanup'; Label = $strings.menuNetworkCleanup; Description = $strings.descNetworkCleanup; Action = { Invoke-OctaNetworkCleanup | Out-Null } }
        [pscustomobject]@{ Id = 'malware-scan'; Label = $strings.menuMalwareScan; Description = $strings.descMalwareScan; Action = { Invoke-OctaMalwareScan | Format-List } }
    )

    $toolGroups = @(
        [pscustomobject]@{ Key = '1'; Label = $strings.groupDiskStorage; Description = $strings.groupDiskStorageDesc; ToolIds = @('disk-cleanup', 'disk-usage', 'disk-health', 'uninstaller') }
        [pscustomobject]@{ Key = '2'; Label = $strings.groupSystemMgmt; Description = $strings.groupSystemMgmtDesc; ToolIds = @('startup-manager', 'tasks', 'scheduler', 'software-updater') }
        [pscustomobject]@{ Key = '3'; Label = $strings.groupInfoHistory; Description = $strings.groupInfoHistoryDesc; ToolIds = @('dashboard', 'history') }
        [pscustomobject]@{ Key = '4'; Label = $strings.groupNetworkSecurity; Description = $strings.groupNetworkSecurityDesc; ToolIds = @('network-cleanup', 'malware-scan') }
    )

    while ($true) {
        # 010: two primary paths instead of a flat list - Quick Assist (today's Quick Clean,
        # unchanged) and Quick Custom (a new screen wrapping the same Categorías/Herramientas
        # pickers that used to sit at this top level - see the 'c' case below).
        $mainItems = @(
            [pscustomobject]@{ Key = 'q'; Label = $strings.menuQuickAssist; Description = $strings.menuQuickAssistDesc }
            [pscustomobject]@{ Key = 'c'; Label = $strings.menuQuickCustom; Description = $strings.menuQuickCustomDesc }
            [pscustomobject]@{ Key = 'u'; Label = $strings.menuUndo; Description = $strings.menuUndoDesc }
            [pscustomobject]@{ Key = 'l'; Label = $strings.menuLanguage; Description = $strings.menuLanguageDesc }
            [pscustomobject]@{ Key = 'h'; Label = $strings.menuHelp; Description = $strings.menuHelpDesc }
            [pscustomobject]@{ Key = 'x'; Label = $strings.menuExit; Description = $strings.menuExitDesc }
        )

        $choice = Show-OctaFrame -Draw {
            Show-OctaMenu -Items $mainItems -Header $strings.mainMenuHeader -NavHint $strings.menuNavHint
        }

        switch ($choice) {
            'q' {
                Show-OctaFrame -Draw {
                    Show-OctaBanner
                    Write-Host ($strings.runningHeader -f $strings.menuQuickAssist)
                    Invoke-OctaQuickClean -Apply -NoRestorePoint:$true | Format-List
                    Wait-OctaKeypress -Strings $strings
                } | Out-Null
            }
            'c' {
                while ($true) {
                    $customItems = @(
                        [pscustomobject]@{ Key = '1'; Label = $strings.menuCategories; Description = $strings.menuCategoriesDesc }
                        [pscustomobject]@{ Key = '2'; Label = $strings.menuTools; Description = $strings.menuToolsDesc }
                        [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }
                    )
                    $customChoice = Show-OctaFrame -Draw {
                        Show-OctaMenu -Items $customItems -Header $strings.menuQuickCustom -NavHint $strings.menuNavHint
                    }
                    if ($customChoice -eq 'b') { break }

                    switch ($customChoice) {
                        '1' {
                            while ($true) {
                                $groupItems = @($categoryGroups | ForEach-Object { [pscustomobject]@{ Key = $_.Key; Label = $_.Label; Description = $_.Description } })
                                $groupItems += [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }
                                $groupChoice = Show-OctaFrame -Draw {
                                    Show-OctaMenu -Items $groupItems -Header $strings.categoryGroupHeader -NavHint $strings.menuNavHint
                                }
                                if ($groupChoice -eq 'b') { break }
                                $group = $categoryGroups | Where-Object { $_.Key -eq $groupChoice }

                                while ($true) {
                                    $leafCategories = @($categories | Where-Object { $_.Id -in $group.Ids })
                                    $leafItems = @()
                                    for ($i = 0; $i -lt $leafCategories.Count; $i++) {
                                        $leafItems += [pscustomobject]@{ Key = ($i + 1).ToString(); Label = $leafCategories[$i].DisplayName; Description = $leafCategories[$i].Description }
                                    }
                                    $leafItems += [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }

                                    $leafChoice = Show-OctaFrame -Draw {
                                        Show-OctaMenu -Items $leafItems -Header $group.Label -NavHint $strings.menuNavHint
                                    }
                                    if ($leafChoice -eq 'b') { break }

                                    $index = [int]$leafChoice - 1
                                    if ($index -ge 0 -and $index -lt $leafCategories.Count) {
                                        Show-OctaFrame -Draw {
                                            Show-OctaBanner
                                            Write-Host ($strings.runningHeader -f $leafCategories[$index].DisplayName)
                                            Invoke-OctaCategory -CategoryId $leafCategories[$index].Id -Apply -NoRestorePoint:$true | Format-List
                                            Wait-OctaKeypress -Strings $strings
                                        } | Out-Null
                                    }
                                }
                            }
                        }
                        '2' {
                            while ($true) {
                                $groupItems = @($toolGroups | ForEach-Object { [pscustomobject]@{ Key = $_.Key; Label = $_.Label; Description = $_.Description } })
                                $groupItems += [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }
                                $groupChoice = Show-OctaFrame -Draw {
                                    Show-OctaMenu -Items $groupItems -Header $strings.toolGroupHeader -NavHint $strings.menuNavHint
                                }
                                if ($groupChoice -eq 'b') { break }
                                $group = $toolGroups | Where-Object { $_.Key -eq $groupChoice }

                                while ($true) {
                                    $leafTools = @($toolCatalog | Where-Object { $_.Id -in $group.ToolIds })
                                    $leafItems = @()
                                    for ($i = 0; $i -lt $leafTools.Count; $i++) {
                                        $leafItems += [pscustomobject]@{ Key = ($i + 1).ToString(); Label = $leafTools[$i].Label; Description = $leafTools[$i].Description }
                                    }
                                    $leafItems += [pscustomobject]@{ Key = 'b'; Label = $strings.menuBack; Description = '' }

                                    $leafChoice = Show-OctaFrame -Draw {
                                        Show-OctaMenu -Items $leafItems -Header $group.Label -NavHint $strings.menuNavHint
                                    }
                                    if ($leafChoice -eq 'b') { break }

                                    $index = [int]$leafChoice - 1
                                    if ($index -ge 0 -and $index -lt $leafTools.Count) {
                                        Show-OctaFrame -Draw {
                                            Show-OctaBanner
                                            Write-Host ($strings.runningHeader -f $leafTools[$index].Label)
                                            & $leafTools[$index].Action
                                            Wait-OctaKeypress -Strings $strings
                                        } | Out-Null
                                    }
                                }
                            }
                        }
                    }
                }
            }
            'u' {
                Show-OctaFrame -Draw {
                    Show-OctaBanner
                    Write-Host ($strings.runningHeader -f $strings.menuUndo)
                    Undo-OctaRun -RunId 'latest' | Format-List
                    Wait-OctaKeypress -Strings $strings
                } | Out-Null
            }
            'l' {
                # Was not wrapped in Show-OctaFrame at all before - its own banner+prompt just
                # printed wherever the cursor happened to be after the previous menu, instead of
                # replacing it in place, which is exactly the "everything stacks up" symptom a
                # real user reported.
                $culture = Show-OctaFrame -Draw {
                    Show-OctaLanguageMenu
                }
                Set-OctaLanguage -Culture $culture
                $strings = Get-OctaStrings
            }
            'h' {
                Show-OctaFrame -Draw {
                    Show-OctaBanner
                    Write-Host 'https://github.com/jpiribe/octa (see README for full reference)'
                    Wait-OctaKeypress -Strings $strings
                } | Out-Null
            }
            'x' { return }
        }
    }
}