Select-CopilotSession.ps1

function Invoke-CopilotSessionPicker {
    param(
        [Parameter(Mandatory)]
        [object[]]$Sessions
    )

    Assert-CopilotSessionPickerInteractive
    $choices = @($Sessions | ForEach-Object {
        [PSCustomObject]@{
            Id         = $_.Id
            Name       = $_.Name
            Summary    = $_.Summary
            Repository = $_.Repository
            Branch     = $_.Branch
            Cwd        = $_.Cwd
            UpdatedAt  = $_.UpdatedAt
            EventCount = $_.EventCount
            Session    = $_
        }
    })

    if (Get-Command Out-ConsoleGridView -ErrorAction SilentlyContinue) {
        $selected = $choices | Out-ConsoleGridView -OutputMode Single
        return $selected.Session
    }

    if (Get-Command Out-GridView -ErrorAction SilentlyContinue) {
        $selected = $choices | Out-GridView -Title 'Select Copilot session to resume' -PassThru
        return $selected.Session
    }

    Write-Host 'Select a Copilot session to resume:' -ForegroundColor Yellow
    Write-Host ''
    for ($i = 0; $i -lt $choices.Count; $i++) {
        $choice = $choices[$i]
        $updatedAt = if ($choice.UpdatedAt) { $choice.UpdatedAt.LocalDateTime } else { '(unknown)' }
        Write-Host (" [{0}] {1}" -f ($i + 1), $choice.Summary) -ForegroundColor ($(if ($i -eq 0) { 'Cyan' } else { 'Gray' }))
        Write-Host (" Repo: {0} Branch: {1}" -f ($choice.Repository ?? '(unknown)'), ($choice.Branch ?? '(unknown)')) -ForegroundColor DarkGray
        Write-Host (" Cwd: {0}" -f ($choice.Cwd ?? '(unknown)')) -ForegroundColor DarkGray
        Write-Host (" Updated: {0} Events: {1}" -f $updatedAt, $choice.EventCount) -ForegroundColor DarkGray
    }
    Write-Host ' [Q] Cancel' -ForegroundColor Green
    Write-Host ''

    do {
        Write-Host "Select session [1-$($choices.Count)/Q]: " -NoNewline -ForegroundColor Yellow
        $selection = Read-Host -ErrorAction Stop
        if ($selection -in @('Q', 'q')) { return $null }
        $number = $selection -as [int]
    } while ($null -eq $number -or $number -lt 1 -or $number -gt $choices.Count)

    return $choices[$number - 1].Session
}

function Select-CopilotSession {
    <#
    .SYNOPSIS
        Selects and resumes a Copilot CLI session from all sessions on the machine.
 
    .DESCRIPTION
        Lists sessions from Get-CopilotSession -All, optionally filters them by
        ID, repository, branch, cwd, summary, or age using the same matching logic
        as Get-CopilotSession. Discovery remains global, unlike Get-CopilotSession's
        default current-directory scope. All supplied filters must match.
        Resumes the selected session by delegating to Resume-CopilotSession.
        When filters and -First resolve to exactly one session, the picker is
        skipped. Otherwise, the command prefers
        Out-ConsoleGridView, then Out-GridView, then a numbered console prompt.
 
        By default the resume runs from the session's recorded Cwd so sessions
        from other directories restore their original workspace context. Use
        -StayInDirectory to resume from the current directory instead.
 
    .PARAMETER Id
        Select a session by ID. Wildcards are supported.
 
    .PARAMETER Repository
        Filter sessions by repository. Case-insensitive wildcards are supported.
        Missing or empty Repository does not match even '*'.
 
    .PARAMETER Branch
        Filter sessions by branch. Case-insensitive wildcards are supported.
        Missing or empty Branch does not match even '*'.
 
    .PARAMETER Cwd
        Filter recorded working directories using case-insensitive wildcards.
        Paths are not normalized or resolved. Missing Cwd does not match '*'.
 
    .PARAMETER Summary
        Filter displayed Summary using case-insensitive wildcards (name, then
        legacy summary, then '(no summary)').
 
    .PARAMETER UpdatedBefore
        Match UpdatedAt strictly before this DateTimeOffset instant. Use ISO 8601
        with Z or an explicit offset. Input without an offset uses local time;
        a date-only input means local midnight. Missing UpdatedAt is excluded.
 
    .PARAMETER OlderThan
        Match UpdatedAt strictly more than this positive TimeSpan ago, measured
        against one UTC clock sample per invocation. Use New-TimeSpan -Days 30;
        a day means 24 elapsed hours. With UpdatedBefore, both limits must match.
        Missing UpdatedAt is excluded; there is no timestamp fallback.
 
    .PARAMETER First
        Take the first N matching sessions after sorting by UpdatedAt descending.
        Use -First 1 to resume the most recent matching session without a picker.
 
    .PARAMETER StayInDirectory
        Resume the chosen session from the current directory instead of changing
        to the session's recorded Cwd.
 
    .PARAMETER SessionSelector
        Optional scriptblock replacing the picker when multiple sessions match.
        Receives one object[] of CopilotSession candidates with Id, Name, Summary,
        Branch, UpdatedAt, and Cwd. Return one candidate or $null/no output to
        cancel without launching. Only a candidate's exact Id is accepted;
        returned metadata changes are ignored. Multiple objects, other output,
        errors, and noncandidate results terminate without launching or opening
        a fallback picker. Use Write-Host or Write-Verbose for diagnostics.
        A single match still skips selection. No matches retain the existing
        error, and -WhatIf never invokes a selector. Custom selectors work without
        an interactive console and are responsible for their own UI requirements.
        The default picker requires interactive input; unavailable input or host
        prompt errors terminate instead of choosing a session automatically.
 
    .PARAMETER Prompt
        Optional prompt to execute in autopilot mode within the resumed session.
 
    .PARAMETER RemainingArgs
        Any additional arguments passed through to Resume-CopilotSession.
 
    .EXAMPLE
        Select-CopilotSession
        # Shows a picker over all Copilot sessions and resumes the selected one.
 
    .EXAMPLE
        Select-CopilotSession -Repository 'shmuelie/powershell-modules' -Branch 'main' -First 1
        # Resumes the most recent matching session without opening the picker.
 
    .EXAMPLE
        Select-CopilotSession -Id 'abc-*' -WhatIf
        # Shows which matching session would be resumed without launching Copilot.
 
    .EXAMPLE
        Select-CopilotSession -SessionSelector {
            param([object[]]$Sessions)
            $Sessions | Sort-Object UpdatedAt -Descending | Select-Object -First 1
        }
        # A portable selector; returning $null instead cancels the resume.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [SupportsWildcards()]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [SupportsWildcards()]
        [ValidateNotNullOrEmpty()]
        [string]$Repository,

        [SupportsWildcards()]
        [ValidateNotNullOrEmpty()]
        [string]$Branch,

        [SupportsWildcards()]
        [ValidateNotNullOrEmpty()]
        [string]$Cwd,

        [SupportsWildcards()]
        [ValidateNotNullOrEmpty()]
        [string]$Summary,

        [datetimeoffset]$UpdatedBefore,

        [ValidateScript({ $_ -gt [timespan]::Zero }, ErrorMessage = 'OlderThan must be a positive TimeSpan.')]
        [timespan]$OlderThan,

        [ValidateRange(1, [int]::MaxValue)]
        [int]$First,

        [switch]$StayInDirectory,

        [ValidateNotNull()]
        [scriptblock]$SessionSelector,

        [string]$Prompt,

        [Parameter(ValueFromRemainingArguments)]
        [string[]]$RemainingArgs
    )

    $filters = @{}
    foreach ($filter in 'Id', 'Repository', 'Branch', 'Cwd', 'Summary', 'UpdatedBefore', 'OlderThan') {
        if ($PSBoundParameters.ContainsKey($filter)) {
            $filters[$filter] = $PSBoundParameters[$filter]
        }
    }
    $sessions = @(Get-CopilotSession -All | Select-CopilotSessionMatch @filters | Sort-Object UpdatedAt -Descending)

    if ($First) {
        $sessions = @($sessions | Select-Object -First $First)
    }

    if ($sessions.Count -eq 0) {
        Write-Error 'No Copilot sessions matched the specified criteria.'
        return
    }

    $session = if ($sessions.Count -eq 1) {
        $sessions[0]
    } else {
        if ($WhatIfPreference) {
            Write-Error 'Multiple Copilot sessions matched. Refine the filters or use -First 1 to preview without opening the picker.'
            return
        }

        if ($SessionSelector) {
            Invoke-CopilotSessionSelector -Sessions $sessions -SessionSelector $SessionSelector
        } else {
            Invoke-CopilotSessionPicker -Sessions $sessions
        }
    }

    if ($null -eq $session) {
        return
    }

    $target = "$($session.Summary) ($($session.Id))"
    $action = if ($StayInDirectory) {
        'Resume Copilot session from current directory'
    } else {
        "Resume Copilot session from $($session.Cwd)"
    }

    if (-not $PSCmdlet.ShouldProcess($target, $action)) {
        return
    }

    $resumeParams = @{ Id = $session.Id }
    if ($Prompt) {
        $resumeParams['Prompt'] = $Prompt
    }

    if ($StayInDirectory) {
        if ($RemainingArgs) {
            Resume-CopilotSession @resumeParams -RemainingArgs $RemainingArgs
        } else {
            Resume-CopilotSession @resumeParams
        }
        return
    }

    if (-not $session.Cwd -or -not (Test-Path -LiteralPath $session.Cwd -PathType Container)) {
        Write-Error "Session '$($session.Id)' has no existing Cwd to resume from: $($session.Cwd)"
        return
    }

    Push-Location -LiteralPath $session.Cwd
    try {
        if ($RemainingArgs) {
            Resume-CopilotSession @resumeParams -RemainingArgs $RemainingArgs
        } else {
            Resume-CopilotSession @resumeParams
        }
    } finally {
        Pop-Location
    }
}