Public/Session.ps1

# ---------------------------------------------------------------------------
# Session tracking
# ---------------------------------------------------------------------------

function Update-TTSession {
    <#
    .SYNOPSIS
        Called from the prompt hook to update the current session's state.
    .DESCRIPTION
        Records PID, working directory, window title, shell type, and timestamp
        for the current PowerShell process. Creates a new session record if one
        does not exist, or updates the existing record with current state.
    .PARAMETER Note
        Optional note to attach to the session.
    .EXAMPLE
        Update-TTSession
        Updates the current session's tracking data.
    .EXAMPLE
        Update-TTSession -Note 'Working on feature branch'
        Updates session and attaches a note.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string]$Note
    )
    Initialize-TTDataStore
    if (-not $PSCmdlet.ShouldProcess("Current session (PID $PID)", "Update")) { return }

    $pid_current = $PID
    $cwd = (Get-Location).Path
    $title = $Host.UI.RawUI.WindowTitle
    $shell = if ($PSVersionTable.PSEdition -eq 'Core') { "PowerShell $($PSVersionTable.PSVersion)" }
             elseif ($PSVersionTable.PSVersion.Major -le 5) { "Windows PowerShell $($PSVersionTable.PSVersion)" }
             else { "PowerShell" }
    $procName = (Get-Process -Id $pid_current -ErrorAction SilentlyContinue).ProcessName
    $now = [datetime]::UtcNow.ToString('o')

    $sessions = @(Read-JsonFile $script:SessionsFile)
    $existing = $sessions | Where-Object { $_.Pid -eq $pid_current }

    if ($existing) {
        # Keep last directory in history before updating
        if ($existing.WorkingDirectory -ne $cwd) {
            if (-not $existing.DirectoryHistory) {
                $existing | Add-Member -NotePropertyName DirectoryHistory -NotePropertyValue @() -Force
            }
            $histEntry = [PSCustomObject]@{
                Directory = $existing.WorkingDirectory
                Timestamp = $existing.LastUpdated
            }
            $history = @($existing.DirectoryHistory) + @($histEntry)
            # Keep last 50 entries
            if ($history.Count -gt 50) { $history = $history[-50..-1] }
            $existing.DirectoryHistory = $history
        }
        $existing.WorkingDirectory = $cwd
        $existing.WindowTitle = $title
        $existing.LastUpdated = $now
        $existing.LastSeen = $now
        if ($Note) { $existing.Notes = $Note }
    }
    else {
        $newSession = [PSCustomObject]@{
            Id               = [guid]::NewGuid().ToString('N').Substring(0, 8)
            Pid              = $pid_current
            ProcessName      = $procName
            Shell            = $shell
            WindowTitle      = $title
            WorkingDirectory = $cwd
            StartTime        = $now
            LastUpdated      = $now
            LastSeen         = $now
            Notes            = if ($Note) { $Note } else { '' }
            Hidden           = $false
            Tags             = @()
            DirectoryHistory = @()
        }
        $sessions = @($sessions) + @($newSession)
    }

    Write-JsonFile $script:SessionsFile $sessions
}

# ---------------------------------------------------------------------------
# Session queries
# ---------------------------------------------------------------------------

function Get-TTSession {
    <#
    .SYNOPSIS
        List tracked terminal sessions.
    .DESCRIPTION
        Returns terminal sessions currently tracked by TerminalTracker. Supports
        filtering by active/hidden status and retrieving specific sessions by ID.
    .PARAMETER Id
        Filter by session ID (prefix match supported).
    .PARAMETER Active
        Only show sessions whose process is still running.
    .PARAMETER IncludeHidden
        Include hidden sessions in output.
    .PARAMETER All
        Include all sessions regardless of hidden status.
    .PARAMETER Clean
        Archive and remove dead sessions before returning (default: true).
    .EXAMPLE
        Get-TTSession
        Returns all tracked sessions.
    .EXAMPLE
        Get-TTSession -Id 'abc12345'
        Returns the session with ID starting with abc12345.
    .EXAMPLE
        Get-TTSession -Active
        Returns only sessions whose process is still running.
    #>

    [CmdletBinding()]
    param(
        [string]$Id,
        [switch]$Active,
        [switch]$IncludeHidden,
        [switch]$All,
        [bool]$Clean = $true
    )
    Initialize-TTDataStore
    $sessions = @(Read-JsonFile $script:SessionsFile)

    # Deduplicate by Id (not PID — PID reuse can suppress new sessions)
    $idSeen = @{}
    $unique = [System.Collections.Generic.List[object]]::new()
    foreach ($s in $sessions) {
        $idKey = $s.Id
        if (-not $idKey) { $unique.Add($s); continue }
        if ($idSeen.ContainsKey($idKey)) {
            # Keep the one with the more recent LastUpdated
            $existingIdx = $idSeen[$idKey]
            $existing = $unique[$existingIdx]
            if ($s.LastUpdated -gt $existing.LastUpdated) {
                $unique[$existingIdx] = $s
            }
            continue
        }
        $idSeen[$idKey] = $unique.Count
        $unique.Add($s)
    }
    $sessions = @($unique.ToArray())

    # Auto-clean dead sessions on every read
    if ($Clean) {
        $alive = [System.Collections.Generic.List[object]]::new()
        $dirty = $false
        foreach ($s in $sessions) {
            $running = try { $null -ne (Get-Process -Id $s.Pid -ErrorAction Stop); $true } catch { $false }
            if ($running) {
                $alive.Add($s)
            }
            else {
                Add-TTArchiveEntry $s 'closed'
                $dirty = $true
            }
        }
        if ($dirty -or ($unique.Count -ne @($sessions).Count)) {
            Write-JsonFile $script:SessionsFile @($alive.ToArray())
            $sessions = @($alive.ToArray())
        }
    }

    if ($Id) {
        $sessions = @($sessions | Where-Object { $_.Id -like "$Id*" })
    }

    if ($Active) {
        $sessions = @($sessions | Where-Object {
            try { $null -ne (Get-Process -Id $_.Pid -ErrorAction Stop); $true }
            catch { $false }
        })
    }

    if (-not $IncludeHidden -and -not $All) {
        $sessions = @($sessions | Where-Object { $_.Hidden -ne $true })
    }

    foreach ($s in $sessions) {
        $s | Add-Member -NotePropertyName 'IsAlive' -NotePropertyValue $true -Force
    }

    $sessions
}

function Set-TTNote {
    <#
    .SYNOPSIS
        Add or update resume notes for a session.
    .DESCRIPTION
        Sets or appends a text note on a tracked session. Notes are preserved
        through suspend/resume cycles and displayed in the dashboard.
    .PARAMETER Id
        Session ID (prefix match supported).
    .PARAMETER Note
        The note text. Use -Append to add to existing notes.
    .PARAMETER Append
        Append the note to existing notes instead of replacing.
    .EXAMPLE
        Set-TTNote -Id 'abc12345' -Note 'Debugging auth issue'
        Sets a note on the session.
    .EXAMPLE
        Set-TTNote -Id 'abc12345' -Note 'Also check logs' -Append
        Appends additional text to the existing note.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Id,
        [Parameter(Mandatory)][string]$Note,
        [switch]$Append
    )
    Initialize-TTDataStore
    $sessions = @(Read-JsonFile $script:SessionsFile)
    $target = $sessions | Where-Object { $_.Id -like "$Id*" }
    if (-not $target) { Write-Warning "Session '$Id' not found."; return }
    if (@($target).Count -gt 1) { Write-Warning "Ambiguous ID '$Id' matches multiple sessions."; return }
    $target = @($target)[0]

    if (-not $PSCmdlet.ShouldProcess("Session $($target.Id)", "Set note")) { return }

    if ($Append -and $target.Notes) {
        $target.Notes = "$($target.Notes)`n$Note"
    } else {
        $target.Notes = $Note
    }
    $target.LastUpdated = [datetime]::UtcNow.ToString('o')

    Write-JsonFile $script:SessionsFile $sessions
    Write-Verbose "Note updated for session $($target.Id)."
    $target
}

function Set-TTTag {
    <#
    .SYNOPSIS
        Add or remove tags on a session.
    .DESCRIPTION
        Manages tags on a tracked session. Tags can be used for categorization
        and filtering. Multiple tags can be added or removed in a single call.
    .PARAMETER Id
        Session ID (prefix match supported).
    .PARAMETER Add
        Tags to add to the session.
    .PARAMETER Remove
        Tags to remove from the session.
    .EXAMPLE
        Set-TTTag -Id 'abc12345' -Add 'project-x', 'frontend'
        Adds two tags to the session.
    .EXAMPLE
        Set-TTTag -Id 'abc12345' -Remove 'temp'
        Removes the 'temp' tag from the session.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Id,
        [string[]]$Add,
        [string[]]$Remove
    )
    Initialize-TTDataStore
    $sessions = @(Read-JsonFile $script:SessionsFile)
    $target = $sessions | Where-Object { $_.Id -like "$Id*" }
    if (-not $target -or @($target).Count -ne 1) { Write-Warning "Session not found or ambiguous."; return }
    $target = @($target)[0]

    if (-not $PSCmdlet.ShouldProcess("Session $($target.Id)", "Set tags")) { return }

    if (-not $target.Tags) { $target | Add-Member -NotePropertyName Tags -NotePropertyValue @() -Force }
    $tagArray = @($target.Tags | ForEach-Object { [string]$_ })
    $tags = [System.Collections.Generic.List[string]]::new([string[]]$tagArray)
    foreach ($t in $Add) { if ($t -notin $tags) { $tags.Add($t) } }
    foreach ($t in $Remove) { $tags.Remove($t) | Out-Null }
    $target.Tags = @($tags)
    $target.LastUpdated = [datetime]::UtcNow.ToString('o')

    Write-JsonFile $script:SessionsFile $sessions
    $target
}

# ---------------------------------------------------------------------------
# Session removal
# ---------------------------------------------------------------------------

function Remove-TTSession {
    <#
    .SYNOPSIS
        Remove a session from tracking (archives it first by default).
    .DESCRIPTION
        Removes a tracked session. By default, the session is archived before
        removal so it appears in the session history. Use -NoArchive to skip.
    .PARAMETER Id
        Session ID (prefix match supported).
    .PARAMETER NoArchive
        Skip archiving before removal.
    .EXAMPLE
        Remove-TTSession -Id 'abc12345'
        Archives and removes the session from active tracking.
    .EXAMPLE
        Remove-TTSession -Id 'abc12345' -NoArchive
        Removes the session without archiving.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string]$Id,
        [switch]$NoArchive
    )
    Initialize-TTDataStore
    $sessions = @(Read-JsonFile $script:SessionsFile)
    $target = $sessions | Where-Object { $_.Id -like "$Id*" }
    if (-not $target) { Write-Warning "Session '$Id' not found."; return }
    if (@($target).Count -gt 1) { Write-Warning "Ambiguous ID '$Id'."; return }
    $target = @($target)[0]

    if ($PSCmdlet.ShouldProcess("Session $($target.Id) at $($target.WorkingDirectory)", "Remove")) {
        if (-not $NoArchive) {
            Add-TTArchiveEntry $target 'manual-remove'
        }
        $sessions = @($sessions | Where-Object { $_.Id -ne $target.Id })
        Write-JsonFile $script:SessionsFile $sessions
        Write-Verbose "Session $($target.Id) removed."
    }
}