Format-CopilotEvent.ps1

function Measure-CopilotEvent {
    <#
    .SYNOPSIS
        Summarizes Copilot session events — counts, tokens, cost, tool results.
 
    .DESCRIPTION
        Accepts Copilot session events from the pipeline (via Send-CopilotMessage -Stream
        or Get-CopilotSessionMessages) and produces an aggregate summary object.
 
    .PARAMETER InputObject
        Copilot session event objects from the pipeline.
 
    .EXAMPLE
        # Summarize a streaming session
        Send-CopilotMessage $session "hello" -Stream | Measure-CopilotEvent
 
    .EXAMPLE
        # Combine with Format-CopilotEvent using -PassThru
        Send-CopilotMessage $session "hello" -Stream | Format-CopilotEvent -PassThru | Measure-CopilotEvent
 
    .EXAMPLE
        # Summarize history
        Get-CopilotSessionMessages $session | Measure-CopilotEvent
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )

    begin {
        $summary = [ordered]@{
            Events           = 0
            UserMessages     = 0
            AssistantMessages = 0
            ToolCalls        = 0
            ToolSuccesses    = 0
            ToolFailures     = 0
            ToolErrors       = [System.Collections.Generic.List[object]]::new()
            ToolNames        = [System.Collections.Generic.List[string]]::new()
            InputTokens      = 0
            OutputTokens     = 0
            CacheReadTokens  = 0
            CacheWriteTokens = 0
            TotalCost        = [double]0
            Models           = [System.Collections.Generic.List[string]]::new()
            SessionTokens    = $null
            SessionTokenLimit = $null
            SessionMessages  = $null
            Duration         = [double]0
        }
    }

    process {
        if ($null -eq $InputObject) { return }
        $summary.Events++
        $type = $InputObject.GetType().Name

        switch -Wildcard ($type) {
            "*UserMessage*" {
                $summary.UserMessages++
            }
            "*AssistantMessage*" {
                if ($type -notmatch 'Delta') {
                    $summary.AssistantMessages++
                }
            }
            "*ToolExecutionStart*" {
                $summary.ToolCalls++
                $name = if ($InputObject.Data.McpServerName) {
                    "$($InputObject.Data.McpServerName)/$($InputObject.Data.McpToolName)"
                } else {
                    $InputObject.Data.ToolName
                }
                if ($name -and -not $summary.ToolNames.Contains($name)) {
                    $summary.ToolNames.Add($name)
                }
            }
            "*ToolExecutionComplete*" {
                if ($InputObject.Data.Success) {
                    $summary.ToolSuccesses++
                } else {
                    $summary.ToolFailures++
                    if ($InputObject.Data.Error) {
                        $summary.ToolErrors.Add([PSCustomObject]@{
                            ToolCallId = $InputObject.Data.ToolCallId
                            Code       = $InputObject.Data.Error.Code
                            Message    = $InputObject.Data.Error.Message
                        })
                    }
                }
            }
            "*AssistantUsage*" {
                $d = $InputObject.Data
                if ($d.InputTokens)      { $summary.InputTokens      += $d.InputTokens }
                if ($d.OutputTokens)     { $summary.OutputTokens     += $d.OutputTokens }
                if ($d.CacheReadTokens)  { $summary.CacheReadTokens  += $d.CacheReadTokens }
                if ($d.CacheWriteTokens) { $summary.CacheWriteTokens += $d.CacheWriteTokens }
                if ($d.Cost)             { $summary.TotalCost        += $d.Cost }
                if ($d.Duration)         { $summary.Duration         += $d.Duration }
                if ($d.Model -and -not $summary.Models.Contains($d.Model)) {
                    $summary.Models.Add($d.Model)
                }
            }
            "*SessionUsageInfo*" {
                $d = $InputObject.Data
                $summary.SessionTokens    = $d.CurrentTokens
                $summary.SessionTokenLimit = $d.TokenLimit
                $summary.SessionMessages  = $d.MessagesLength
            }
        }
    }

    end {
        # Convert lists to arrays for clean output
        $summary.ToolNames  = @($summary.ToolNames)
        $summary.ToolErrors = @($summary.ToolErrors)
        $summary.Models     = @($summary.Models)

        [PSCustomObject]$summary
    }
}

function Format-CopilotEvent {
    <#
    .SYNOPSIS
        Formats and logs Copilot session events with colors to console and optionally to file.
     
    .DESCRIPTION
        A filter function that processes Copilot session events in the pipeline,
        displaying them with colors in the console and optionally saving plain
        text to a log file.
     
    .PARAMETER LogFile
        Optional path to save the log as plain text (no color codes).
     
    .PARAMETER PassThru
        If specified, passes the original event objects through the pipeline
        for further processing.
     
    .PARAMETER Append
        If specified with -LogFile, appends to an existing log file instead of
        overwriting it.
     
    .EXAMPLE
        # Console output only
        Send-CopilotMessage $session -Prompt "test" -Stream | Format-CopilotEvent
     
    .EXAMPLE
        # Console + log file (overwrites existing)
        Send-CopilotMessage $session -Prompt "test" -Stream | Format-CopilotEvent -LogFile "session.log"
     
    .EXAMPLE
        # Append to existing log file
        Send-CopilotMessage $session -Prompt "test" -Stream | Format-CopilotEvent -LogFile "session.log" -Append
     
    .EXAMPLE
        # Save to file and capture events
        $events = Send-CopilotMessage $session -Prompt "test" -Stream | Format-CopilotEvent -LogFile "log.txt" -PassThru
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,
        
        [string]$LogFile,
        
        [switch]$PassThru,
        
        [switch]$Append
    )
    
    begin {
        # Track toolCallId -> display name for correlating COMPLETE with START
        $toolNames = @{}

        function Write-ColorLog {
            param(
                [string]$Message,
                [ConsoleColor]$ForegroundColor = 'White',
                [switch]$NoNewline
            )
            
            # Write to console with color
            Write-Host $Message -ForegroundColor $ForegroundColor -NoNewline:$NoNewline
            
            # Write to log file as plain text if LogFile is specified
            if ($LogFile) {
                if ($NoNewline) {
                    [System.IO.File]::AppendAllText($LogFile, $Message)
                } else {
                    [System.IO.File]::AppendAllText($LogFile, $Message + "`n")
                }
            }
        }
        
        # Initialize log file if specified
        if ($LogFile) {
            # Ensure parent directory exists
            $logDir = Split-Path $LogFile -Parent
            if ($logDir -and -not (Test-Path $logDir)) {
                New-Item -ItemType Directory -Path $logDir -Force | Out-Null
            }
            if (-not $Append -and (Test-Path $LogFile)) {
                Remove-Item $LogFile -Force
            }
        }
    }
    
    process {
        if ($null -eq $InputObject) { return }
        
        $type = $InputObject.GetType().Name
        
        switch -Wildcard ($type) {
            "*UserMessage*" {
                Write-ColorLog "`n[📝 USER MESSAGE]" -ForegroundColor Blue
            }
            "*AssistantMessageDelta*" {
                # Stream assistant text as it arrives
                Write-ColorLog $InputObject.Data.Content -ForegroundColor Green -NoNewline
            }
            "*AssistantMessage*" {
                # Final complete message
                if ($InputObject.Data.Content) {
                    Write-ColorLog "`n[✓ ASSISTANT COMPLETE]" -ForegroundColor DarkGreen
                    Write-ColorLog $InputObject.Data.Content -ForegroundColor White
                }
            }
            "*ToolExecutionStart*" {
                $toolName = $InputObject.Data.ToolName
                $mcpServer = $InputObject.Data.McpServerName
                $mcpTool = $InputObject.Data.McpToolName
                $toolCallId = $InputObject.Data.ToolCallId

                $displayName = if ($mcpServer) { "$mcpServer/$mcpTool" } else { $toolName }
                if ($toolCallId) { $toolNames[$toolCallId] = $displayName }

                Write-ColorLog "`n[🔧 TOOL START] " -ForegroundColor Yellow -NoNewline
                Write-ColorLog $displayName -ForegroundColor Cyan
                if ($toolCallId) {
                    Write-ColorLog " ID: $toolCallId" -ForegroundColor DarkGray
                }

                if ($InputObject.Data.Arguments) {
                    $argsRaw = $InputObject.Data.Arguments
                    $argsJson = if ($argsRaw -is [System.Text.Json.JsonElement]) {
                        $argsRaw.GetRawText()
                    } else {
                        $argsRaw | ConvertTo-Json -Compress -Depth 3
                    }
                    if ($argsJson.Length -gt 300) {
                        Write-ColorLog " Args: $($argsJson.Substring(0, 300))..." -ForegroundColor DarkGray
                    } else {
                        Write-ColorLog " Args: $argsJson" -ForegroundColor DarkGray
                    }
                }
            }
            "*ToolExecutionComplete*" {
                $success = $InputObject.Data.Success
                $icon = if ($success) { "✓" } else { "✗" }
                $color = if ($success) { "Green" } else { "Red" }
                $toolCallId = $InputObject.Data.ToolCallId
                $resolvedName = if ($toolCallId -and $toolNames.ContainsKey($toolCallId)) { $toolNames[$toolCallId] } else { $null }

                Write-ColorLog "`n[$icon TOOL COMPLETE" -ForegroundColor $color -NoNewline
                if ($resolvedName) {
                    Write-ColorLog " $resolvedName" -ForegroundColor Cyan -NoNewline
                }
                if ($toolCallId) {
                    Write-ColorLog " ($toolCallId)" -ForegroundColor DarkGray -NoNewline
                }
                Write-ColorLog "] " -ForegroundColor $color -NoNewline
                
                if ($InputObject.Data.Result) {
                    $content = $InputObject.Data.Result.Content
                    $detailed = $InputObject.Data.Result.DetailedContent
                    
                    if ($content) {
                        if ($content.Length -gt 500) {
                            Write-ColorLog "$($content.Substring(0, 500))..." -ForegroundColor Gray
                        } else {
                            Write-ColorLog $content -ForegroundColor Gray
                        }
                    }
                    
                    if ($detailed -and $detailed -ne $content) {
                        if ($detailed.Length -gt 1000) {
                            Write-ColorLog " Detail: $($detailed.Substring(0, 1000))..." -ForegroundColor DarkGray
                        } else {
                            Write-ColorLog " Detail: $detailed" -ForegroundColor DarkGray
                        }
                    }
                }
                
                if ($InputObject.Data.Error) {
                    $errMsg = $InputObject.Data.Error.Message
                    $errCode = $InputObject.Data.Error.Code
                    if ($errCode) {
                        Write-ColorLog " ERROR [$errCode]: $errMsg" -ForegroundColor Red
                    } else {
                        Write-ColorLog " ERROR: $errMsg" -ForegroundColor Red
                    }
                }
            }
            "*AssistantReasoning*" {
                if ($InputObject.Data.Content) {
                    Write-ColorLog "`n[💭 REASONING] " -ForegroundColor Magenta -NoNewline
                    Write-ColorLog $InputObject.Data.Content -ForegroundColor DarkGray
                }
            }
            "*SessionIdle*" {
                Write-ColorLog "`n[✓ SESSION IDLE]" -ForegroundColor DarkGreen
            }
            "*SessionError*" {
                Write-ColorLog "`n[⚠ ERROR] $($InputObject.Data.Message)" -ForegroundColor Red
            }
            "*AssistantUsage*" {
                $d = $InputObject.Data
                $parts = @()
                if ($d.Model)        { $parts += $d.Model }
                if ($d.InputTokens)  { $parts += "in:$($d.InputTokens)" }
                if ($d.OutputTokens) { $parts += "out:$($d.OutputTokens)" }
                if ($d.CacheReadTokens)  { $parts += "cache-read:$($d.CacheReadTokens)" }
                if ($d.CacheWriteTokens) { $parts += "cache-write:$($d.CacheWriteTokens)" }
                if ($d.Cost)     { $parts += "cost:$($d.Cost)" }
                if ($d.Duration) { $parts += "$($d.Duration)ms" }
                Write-ColorLog "[📊 USAGE] $($parts -join ' | ')" -ForegroundColor DarkCyan
            }
            "*SessionUsageInfo*" {
                $d = $InputObject.Data
                Write-ColorLog "[📊 SESSION] Tokens: $($d.CurrentTokens)/$($d.TokenLimit) | Messages: $($d.MessagesLength)" -ForegroundColor DarkCyan
            }
            "*PendingMessages*" {
                # Suppress these verbose events
            }
            "*TurnStart*" {
                # Suppress these verbose events
            }
            "*TurnEnd*" {
                # Suppress these verbose events
            }
            default {
                # Uncomment to see all event types:
                # Write-ColorLog "[DEBUG: $type]" -ForegroundColor DarkGray
            }
        }
        
        # Pass through the original object if requested
        if ($PassThru) {
            Write-Output $InputObject
        }
    }
    
    end {
    }
}