private/Invoke-WtwHerdrProject.ps1

# Herdr integration for wtw.
#
# Herdr (https://herdr.dev) is a terminal multiplexer for coding agents: a
# workspace sidebar that shows which agent is working, blocked or done. Its CLI
# reaches the running server over a socket from any shell and prints JSON:
# herdr workspace list -> {result:{workspaces:[{workspace_id,label,...}]}}
# herdr pane list -> {result:{panes:[{workspace_id,cwd,...}]}}
# herdr workspace create --cwd <dir> --label <text> --focus
# herdr workspace rename <id> <label>
# herdr workspace focus <id>
# Failures are {error:{code,message}}. With no server up (server_not_running),
# wtw starts herdr in a new Ghostty window rooted at the target.

function Get-WtwHerdrExe {
    [CmdletBinding()]
    param()

    if ($env:WTW_HERDR -and (Test-Path $env:WTW_HERDR)) { return $env:WTW_HERDR }

    $cmd = Get-Command herdr -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($cmd -and $cmd.Source) { return $cmd.Source }

    $exeName = if ($IsWindows) { 'herdr.exe' } else { 'herdr' }
    $homeDir = if ($HOME) { $HOME } else { $env:USERPROFILE }
    if ($homeDir) {
        # install.sh and the release binaries land in ~/.local/bin; cargo in ~/.cargo/bin.
        foreach ($dir in @('.local/bin', '.cargo/bin')) {
            $candidate = Join-Path (Join-Path $homeDir $dir) $exeName
            if (Test-Path $candidate) { return $candidate }
        }
    }

    return $null
}

function Invoke-WtwHerdrCommand {
    <#
    .SYNOPSIS
        Run a herdr CLI command and parse its JSON reply. Result has ExitCode,
        Result (the reply's .result), ErrorCode and Output.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]] $ArgumentList
    )

    $exe = Get-WtwHerdrExe
    if (-not $exe) {
        return [PSCustomObject]@{ ExitCode = 127; Result = $null; ErrorCode = 'not_found'; Output = 'herdr not found' }
    }

    Write-Verbose "herdr command: $exe $($ArgumentList -join ' ')"
    $output = & $exe @ArgumentList 2>&1
    $exitCode = $LASTEXITCODE
    if ($null -eq $exitCode) { $exitCode = 0 }
    $text = ((@($output) | ForEach-Object { "$_" }) -join [Environment]::NewLine).Trim()

    $reply = $null
    $jsonLine = @($text -split '\r?\n' | Where-Object { $_.TrimStart().StartsWith('{') }) | Select-Object -Last 1
    if ($jsonLine) {
        try { $reply = $jsonLine | ConvertFrom-Json -Depth 20 -ErrorAction Stop } catch { $reply = $null }
    }

    $errorCode = $null
    if ($reply -and $reply.PSObject.Properties['error'] -and $reply.error) {
        $errorCode = "$($reply.error.code)"
        if ($exitCode -eq 0) { $exitCode = 1 }
        if ($reply.error.message) { $text = "$($reply.error.message)" }
    }
    $result = if ($reply -and $reply.PSObject.Properties['result']) { $reply.result } else { $null }

    return [PSCustomObject]@{ ExitCode = $exitCode; Result = $result; ErrorCode = $errorCode; Output = $text }
}

function Find-WtwHerdrWorkspace {
    <#
    .SYNOPSIS
        Find the herdr workspace with a pane rooted at the target directory,
        falling back to a workspace labelled with the pretty name.
    #>

    [CmdletBinding()]
    param(
        [string] $PrettyName,
        [string] $ProjectPath
    )

    if ($ProjectPath) {
        $panes = Invoke-WtwHerdrCommand -ArgumentList @('pane', 'list')
        if ($panes.ExitCode -eq 0 -and $panes.Result) {
            $fullPath = [System.IO.Path]::GetFullPath($ProjectPath).TrimEnd('/', '\')
            $comparison = if ($IsWindows) { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal }
            foreach ($pane in @($panes.Result.panes)) {
                if (-not $pane.cwd) { continue }
                $panePath = [System.IO.Path]::GetFullPath("$($pane.cwd)").TrimEnd('/', '\')
                if ([string]::Equals($panePath, $fullPath, $comparison)) { return "$($pane.workspace_id)" }
            }
        }
    }

    if (-not $PrettyName) { return $null }
    $list = Invoke-WtwHerdrCommand -ArgumentList @('workspace', 'list')
    if ($list.ExitCode -ne 0 -or -not $list.Result) { return $null }
    foreach ($workspace in @($list.Result.workspaces)) {
        if ([string]::Equals("$($workspace.label)", $PrettyName, [StringComparison]::Ordinal)) {
            return "$($workspace.workspace_id)"
        }
    }

    return $null
}

function Start-WtwHerdrServer {
    <#
    .SYNOPSIS
        Launch herdr in a new Ghostty window at the target and wait for its
        server socket. Returns $true once `herdr workspace list` answers.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ProjectPath,
        [int] $TimeoutSeconds = 10
    )

    if (-not (Start-WtwGhosttyWindow -WorkingDirectory $ProjectPath -Command @(Get-WtwHerdrExe))) { return $false }

    $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
    while ([DateTime]::UtcNow -lt $deadline) {
        Start-Sleep -Milliseconds 250
        if ((Invoke-WtwHerdrCommand -ArgumentList @('workspace', 'list')).ExitCode -eq 0) { return $true }
    }
    return $false
}

function Open-WtwHerdrProject {
    <#
    .SYNOPSIS
        Open a wtw target as a herdr workspace: focus (and relabel) the one
        already rooted at the target, otherwise create one there.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ProjectPath,
        [Parameter(Mandatory)][string] $PrettyName
    )

    if (-not (Get-WtwHerdrExe)) {
        return [PSCustomObject]@{ Success = $false; Created = $false; Started = $false; Reason = 'herdr not found' }
    }

    $fullPath = [System.IO.Path]::GetFullPath($ProjectPath)
    $started = $false
    $probe = Invoke-WtwHerdrCommand -ArgumentList @('workspace', 'list')
    if ($probe.ErrorCode -eq 'server_not_running') {
        if (-not (Start-WtwHerdrServer -ProjectPath $fullPath)) {
            return [PSCustomObject]@{
                Success = $false; Created = $false; Started = $false; Path = $fullPath
                Reason  = 'no herdr server is running and Ghostty could not start one. Run `herdr` in a terminal, then retry.'
            }
        }
        $started = $true
    } elseif ($probe.ExitCode -ne 0) {
        return [PSCustomObject]@{ Success = $false; Created = $false; Started = $false; Path = $fullPath; Reason = $probe.Output }
    }

    # A fresh server opens its first workspace at the launch dir, so this also
    # picks that one up and gives it the wtw label.
    $workspaceId = Find-WtwHerdrWorkspace -PrettyName $PrettyName -ProjectPath $fullPath
    if ($workspaceId) {
        Invoke-WtwHerdrCommand -ArgumentList @('workspace', 'rename', $workspaceId, $PrettyName) | Out-Null
        Invoke-WtwHerdrCommand -ArgumentList @('workspace', 'focus', $workspaceId) | Out-Null
        return [PSCustomObject]@{
            Success = $true; Created = $started; Started = $started; Path = $fullPath; Id = $workspaceId; Reason = $null
        }
    }

    $result = Invoke-WtwHerdrCommand -ArgumentList @('workspace', 'create', '--cwd', $fullPath, '--label', $PrettyName, '--focus')
    if ($result.ExitCode -ne 0) {
        return [PSCustomObject]@{ Success = $false; Created = $false; Started = $started; Path = $fullPath; Reason = $result.Output }
    }

    $workspaceId = if ($result.Result -and $result.Result.workspace) { "$($result.Result.workspace.workspace_id)" } else { $null }
    return [PSCustomObject]@{
        Success = $true; Created = $true; Started = $started; Path = $fullPath; Id = $workspaceId; Reason = $null
    }
}