workflows/default/systems/ui/modules/FileWatcher.psm1

<#
.SYNOPSIS
FileSystemWatcher-based change notification system

.DESCRIPTION
Provides event-driven file change notifications instead of polling.
Maintains in-memory state that is updated on file changes.
#>


# Script-scoped state
$script:WatcherState = @{
    Watchers = @{}
    LastChanges = @{}
    StateCache = $null
    StateCacheTime = [DateTime]::MinValue
    ActivityPosition = 0
    Initialized = $false
}

function Initialize-FileWatchers {
    param(
        [Parameter(Mandatory = $true)]
        [string]$BotRoot
    )

    if ($script:WatcherState.Initialized) {
        return
    }

    Write-BotLog -Level Debug -Message "[FileWatcher] Initializing file watchers for: $BotRoot"

    # Watch tasks directories
    $tasksDirs = @(
        (Join-Path $BotRoot "workspace\tasks\todo"),
        (Join-Path $BotRoot "workspace\tasks\in-progress"),
        (Join-Path $BotRoot "workspace\tasks\done")
    )

    foreach ($dir in $tasksDirs) {
        if (-not (Test-Path $dir)) {
            Write-BotLog -Level Debug -Message "[FileWatcher] Creating directory: $dir"
            New-Item -Path $dir -ItemType Directory -Force | Out-Null
        }

        try {
            $watcher = New-Object System.IO.FileSystemWatcher
            $watcher.Path = $dir
            $watcher.Filter = "*.json"
            $watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor
                                    [System.IO.NotifyFilters]::FileName -bor
                                    [System.IO.NotifyFilters]::CreationTime
            $watcher.InternalBufferSize = 65536  # 64KB for high-activity directories
            $watcher.EnableRaisingEvents = $true

            # Register event handlers
            Register-ObjectEvent -InputObject $watcher -EventName Changed -Action {
                $script:WatcherState.LastChanges['tasks'] = [DateTime]::UtcNow
                $script:WatcherState.StateCache = $null  # Invalidate cache
            } | Out-Null

            Register-ObjectEvent -InputObject $watcher -EventName Created -Action {
                $script:WatcherState.LastChanges['tasks'] = [DateTime]::UtcNow
                $script:WatcherState.StateCache = $null
            } | Out-Null

            Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action {
                $script:WatcherState.LastChanges['tasks'] = [DateTime]::UtcNow
                $script:WatcherState.StateCache = $null
            } | Out-Null

            $script:WatcherState.Watchers[$dir] = $watcher
            Write-BotLog -Level Debug -Message "[FileWatcher] Watching tasks directory: $dir"
        } catch {
            Write-BotLog -Level Warn -Message "[FileWatcher] Failed to create watcher for $dir" -Exception $_
        }
    }

    # Watch product docs directory
    $productDir = Join-Path $BotRoot "workspace\product"
    if (-not (Test-Path $productDir)) {
        New-Item -Path $productDir -ItemType Directory -Force | Out-Null
    }

    try {
        $productWatcher = New-Object System.IO.FileSystemWatcher
        $productWatcher.Path = $productDir
        $productWatcher.Filter = "*.md"
        $productWatcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor
                                       [System.IO.NotifyFilters]::FileName -bor
                                       [System.IO.NotifyFilters]::CreationTime
        $productWatcher.InternalBufferSize = 32768
        $productWatcher.EnableRaisingEvents = $true

        Register-ObjectEvent -InputObject $productWatcher -EventName Changed -Action {
            $script:WatcherState.LastChanges['product'] = [DateTime]::UtcNow
            $script:WatcherState.StateCache = $null
        } | Out-Null

        Register-ObjectEvent -InputObject $productWatcher -EventName Created -Action {
            $script:WatcherState.LastChanges['product'] = [DateTime]::UtcNow
            $script:WatcherState.StateCache = $null
        } | Out-Null

        Register-ObjectEvent -InputObject $productWatcher -EventName Deleted -Action {
            $script:WatcherState.LastChanges['product'] = [DateTime]::UtcNow
            $script:WatcherState.StateCache = $null
        } | Out-Null

        $script:WatcherState.Watchers[$productDir] = $productWatcher
        Write-BotLog -Level Debug -Message "[FileWatcher] Watching product directory: $productDir"
    } catch {
        Write-BotLog -Level Warn -Message "[FileWatcher] Failed to create product watcher" -Exception $_
    }

    # Watch session state file
    $sessionsDir = Join-Path $BotRoot "workspace\sessions\runs"
    if (-not (Test-Path $sessionsDir)) {
        New-Item -Path $sessionsDir -ItemType Directory -Force | Out-Null
    }

    try {
        $sessionWatcher = New-Object System.IO.FileSystemWatcher
        $sessionWatcher.Path = $sessionsDir
        $sessionWatcher.Filter = "session-state.json"
        $sessionWatcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite
        $sessionWatcher.InternalBufferSize = 32768
        $sessionWatcher.EnableRaisingEvents = $true

        Register-ObjectEvent -InputObject $sessionWatcher -EventName Changed -Action {
            $script:WatcherState.LastChanges['session'] = [DateTime]::UtcNow
            $script:WatcherState.StateCache = $null
        } | Out-Null

        $script:WatcherState.Watchers[$sessionsDir] = $sessionWatcher
        Write-BotLog -Level Debug -Message "[FileWatcher] Watching session directory: $sessionsDir"
    } catch {
        Write-BotLog -Level Warn -Message "[FileWatcher] Failed to create session watcher" -Exception $_
    }

    # Watch control signals directory
    $controlDir = Join-Path $BotRoot ".control"
    if (-not (Test-Path $controlDir)) {
        New-Item -Path $controlDir -ItemType Directory -Force | Out-Null
    }

    try {
        $controlWatcher = New-Object System.IO.FileSystemWatcher
        $controlWatcher.Path = $controlDir
        $controlWatcher.Filter = "*.signal"
        $controlWatcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor
                                       [System.IO.NotifyFilters]::CreationTime
        $controlWatcher.InternalBufferSize = 32768
        $controlWatcher.EnableRaisingEvents = $true

        Register-ObjectEvent -InputObject $controlWatcher -EventName Created -Action {
            $script:WatcherState.LastChanges['control'] = [DateTime]::UtcNow
            $script:WatcherState.StateCache = $null
        } | Out-Null

        Register-ObjectEvent -InputObject $controlWatcher -EventName Deleted -Action {
            $script:WatcherState.LastChanges['control'] = [DateTime]::UtcNow
            $script:WatcherState.StateCache = $null
        } | Out-Null

        $script:WatcherState.Watchers["$controlDir-signals"] = $controlWatcher
        Write-BotLog -Level Debug -Message "[FileWatcher] Watching control signals: $controlDir"
    } catch {
        Write-BotLog -Level Warn -Message "[FileWatcher] Failed to create control signal watcher" -Exception $_
    }

    # Watch activity log for appends
    $activityLog = Join-Path $controlDir "activity.jsonl"
    try {
        $activityWatcher = New-Object System.IO.FileSystemWatcher
        $activityWatcher.Path = $controlDir
        $activityWatcher.Filter = "activity.jsonl"
        $activityWatcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor
                                        [System.IO.NotifyFilters]::Size
        $activityWatcher.InternalBufferSize = 32768
        $activityWatcher.EnableRaisingEvents = $true

        Register-ObjectEvent -InputObject $activityWatcher -EventName Changed -Action {
            $script:WatcherState.LastChanges['activity'] = [DateTime]::UtcNow
        } | Out-Null

        $script:WatcherState.Watchers["$controlDir-activity"] = $activityWatcher
        Write-BotLog -Level Debug -Message "[FileWatcher] Watching activity log: $activityLog"
    } catch {
        Write-BotLog -Level Warn -Message "[FileWatcher] Failed to create activity watcher" -Exception $_
    }

    # Initialize change timestamps
    $script:WatcherState.LastChanges['tasks'] = [DateTime]::UtcNow
    $script:WatcherState.LastChanges['session'] = [DateTime]::UtcNow
    $script:WatcherState.LastChanges['control'] = [DateTime]::UtcNow
    $script:WatcherState.LastChanges['activity'] = [DateTime]::UtcNow
    $script:WatcherState.LastChanges['product'] = [DateTime]::UtcNow

    $script:WatcherState.Initialized = $true
    Write-BotLog -Level Debug -Message "[FileWatcher] Initialization complete"
}

function Get-LastChangeTime {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Category
    )

    if ($script:WatcherState.LastChanges.ContainsKey($Category)) {
        return $script:WatcherState.LastChanges[$Category]
    }
    return [DateTime]::MinValue
}

function Test-StateChanged {
    param(
        [Parameter(Mandatory = $true)]
        [DateTime]$Since
    )

    foreach ($change in $script:WatcherState.LastChanges.Values) {
        if ($change -gt $Since) {
            return $true
        }
    }
    return $false
}

function Get-CachedState {
    return $script:WatcherState.StateCache
}

function Set-CachedState {
    param(
        [Parameter(Mandatory = $true)]
        $State
    )

    $script:WatcherState.StateCache = $State
    $script:WatcherState.StateCacheTime = [DateTime]::UtcNow
}

function Get-StateCacheTime {
    return $script:WatcherState.StateCacheTime
}

function Clear-StateCache {
    $script:WatcherState.StateCache = $null
    $script:WatcherState.StateCacheTime = [DateTime]::MinValue
}

function Stop-FileWatchers {
    Write-BotLog -Level Debug -Message "[FileWatcher] Stopping all file watchers"

    foreach ($watcher in $script:WatcherState.Watchers.Values) {
        try {
            $watcher.EnableRaisingEvents = $false
            $watcher.Dispose()
        } catch {
            Write-BotLog -Level Warn -Message "[FileWatcher] Error disposing watcher" -Exception $_
        }
    }
    $script:WatcherState.Watchers.Clear()
    $script:WatcherState.Initialized = $false

    Write-BotLog -Level Debug -Message "[FileWatcher] All watchers stopped"
}

Export-ModuleMember -Function @(
    'Initialize-FileWatchers',
    'Get-LastChangeTime',
    'Test-StateChanged',
    'Get-CachedState',
    'Set-CachedState',
    'Get-StateCacheTime',
    'Clear-StateCache',
    'Stop-FileWatchers'
)