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) -> 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.

        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
    # -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 | 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) {
        $mainItems = @(
            [pscustomobject]@{ Key = '1'; Label = $strings.menuCategories; Description = $strings.menuCategoriesDesc }
            [pscustomobject]@{ Key = '2'; Label = $strings.menuTools; Description = $strings.menuToolsDesc }
            [pscustomobject]@{ Key = 'q'; Label = $strings.menuQuickClean; Description = $strings.menuQuickCleanDesc }
            [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) {
            '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 | 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
                        }
                    }
                }
            }
            'q' {
                Show-OctaFrame -Draw {
                    Show-OctaBanner
                    Write-Host ($strings.runningHeader -f $strings.menuQuickClean)
                    Invoke-OctaQuickClean -Apply | Format-List
                    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 }
        }
    }
}