/**
 * @fileoverview Subagent Watcher - Real-time monitoring of Claude Code background agents.
 *
 * Watches `~/.claude/projects/{project}/{session}/subagents/agent-{id}.jsonl` files
 * and emits structured events for tool calls, progress, messages, and tool results.
 * Also detects Agent Teams teammates (distinguished by `<teammate-message>` in description).
 *
 * Key exports:
 * - `SubagentWatcher` class — singleton watcher, extends EventEmitter
 * - `subagentWatcher` — pre-instantiated singleton instance
 * - `SubagentInfo`, `SubagentToolCall`, `SubagentProgress`, `SubagentMessage`,
 *   `SubagentToolResult`, `SubagentTranscriptEntry` — data interfaces
 * - `SubagentEvents` — typed event map
 *
 * Watched patterns: `~/.claude/projects/{project}/{session}/subagents/agent-{id}.jsonl`
 * Parses JSONL entries: user/assistant messages, tool_use/tool_result blocks, progress events.
 * Tracks per-agent: status, token counts, model, description, tool call count, liveness (PID).
 *
 * @dependencies config/map-limits (MAX_TRACKED_AGENTS, PENDING_TOOL_CALL_TTL_MS),
 *   config/buffer-limits (FILE_PEEK_BYTES), utils (CleanupManager, KeyedDebouncer)
 * @consumedby web/server (SSE broadcast), session (subagent-session correlation)
 * @emits subagent:discovered, subagent:updated, subagent:tool_call, subagent:tool_result,
 *   subagent:progress, subagent:message, subagent:completed
 *
 * @module subagent-watcher
 */
import { EventEmitter } from 'node:events';
import { watch, existsSync } from 'node:fs';
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
import { homedir } from 'node:os';
import { join, basename } from 'node:path';
import { execFile } from 'node:child_process';
import { readFile, readdir, stat as statAsync } from 'node:fs/promises';
import { PENDING_TOOL_CALL_TTL_MS, MAX_PENDING_TOOL_CALLS, MAX_TRACKED_AGENTS } from './config/map-limits.js';
import { STALE_DATA_MAX_AGE_MS } from './config/server-timing.js';
import { FILE_PEEK_BYTES } from './config/buffer-limits.js';
import { CleanupManager, KeyedDebouncer } from './utils/index.js';
// ========== Constants ==========
const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude/projects');
const IDLE_TIMEOUT_MS = 30000; // Consider agent idle after 30s of no activity
const POLL_INTERVAL_MS = 1000; // Base poll interval (lightweight checks)
const FULL_SCAN_EVERY_N_POLLS = 5; // Full directory traversal every 5th poll (5s)
const LIVENESS_CHECK_MS = 10000; // Check if subagent processes are still alive every 10s
const FILE_ALIVE_THRESHOLD_MS = 30000; // File mtime within 30s = agent alive (primary check)
const STALE_COMPLETED_MAX_AGE_MS = STALE_DATA_MAX_AGE_MS; // Remove completed agents older than 1 hour
const STALE_IDLE_MAX_AGE_MS = 4 * 60 * 60 * 1000; // Remove idle agents older than 4 hours
const STARTUP_MAX_FILE_AGE_MS = 4 * 60 * 60 * 1000; // Only load files modified in last 4 hours on startup
// Internal Claude Code agent patterns to filter out (not real user-initiated subagents)
const INTERNAL_AGENT_PATTERNS = [
    /^\[?SUGGESTION MODE/i, // Claude Code's internal suggestion mode
    /^Suggest what user might/i, // Suggestion mode prompt variant
    /aprompt/i, // Internal prompt agent (anywhere in string)
    /^a\s?prompt/i, // Variants of internal prompt agent
    /^prompt$/i, // Just "prompt"
];
// Minimum description length - very short descriptions are likely internal or malformed
const MIN_DESCRIPTION_LENGTH = 5;
// Display/preview length constants
const TEXT_PREVIEW_LENGTH = 200; // Length for text previews in tool results
const USER_TEXT_PREVIEW_LENGTH = 80; // Length for user message previews
const SMART_TITLE_MAX_LENGTH = 45; // Max length for smart title extraction
const MESSAGE_TEXT_LIMIT = 500; // Max length for message text content
const COMMAND_DISPLAY_LENGTH = 60; // Max length for command display
const INPUT_TRUNCATE_LENGTH = 100; // Max length for input value truncation
const FILE_CONTENT_DEBOUNCE_MS = 100; // Debounce delay for file content updates
// ========== SubagentWatcher Class ==========
export class SubagentWatcher extends EventEmitter {
    filePositions = new Map();
    dirWatchers = new Map();
    // Per-file debouncer for directory watcher (replaces per-file FSWatchers)
    fileDeb = new KeyedDebouncer(FILE_CONTENT_DEBOUNCE_MS);
    agentInfo = new Map();
    idleDeb = new KeyedDebouncer(IDLE_TIMEOUT_MS);
    cleanup = new CleanupManager();
    _isRunning = false;
    knownSubagentDirs = new Set();
    // Map of agentId -> Map of toolUseId -> { toolName, timestamp } (for linking tool_result to tool_call)
    // Includes timestamp for TTL-based cleanup of orphaned entries
    pendingToolCalls = new Map();
    // Guard to prevent concurrent liveness checks (prevents duplicate completed events)
    _isCheckingLiveness = false;
    // Counter for throttling full directory scans (only scan every FULL_SCAN_EVERY_N_POLLS)
    _pollCount = 0;
    // Short-lived cache for parsed parent transcript descriptions (TTL: 5s)
    // Key: "{projectHash}/{sessionId}", Value: { descriptions: Map<agentId, description>, timestamp }
    parentDescriptionCache = new Map();
    // Store error handlers for FSWatchers to enable proper cleanup (prevent memory leaks)
    dirWatcherErrorHandlers = new Map();
    // Map filePath → { projectHash, sessionId } for directory watcher file-change handling
    fileAgentContext = new Map();
    constructor() {
        super();
        this.setMaxListeners(50);
    }
    /**
     * Check if a description matches internal Claude Code agent patterns.
     * These are not real user-initiated subagents and should be filtered out.
     * Also filters out very short descriptions that are likely internal or malformed.
     */
    isInternalAgent(description) {
        if (!description)
            return false;
        // Filter out very short descriptions (likely internal or malformed)
        if (description.length < MIN_DESCRIPTION_LENGTH)
            return true;
        return INTERNAL_AGENT_PATTERNS.some((pattern) => pattern.test(description));
    }
    /**
     * Mark a subagent as completed: clear PID, set status, clean up pending tool calls, emit event.
     */
    markSubagentAsCompleted(info) {
        info.pid = undefined;
        info.status = 'completed';
        this.pendingToolCalls.delete(info.agentId);
        this.emit('subagent:completed', info);
    }
    /**
     * Extract text from message content, handling both string and array formats.
     * For array content, returns the text from the first 'text' block.
     */
    extractFirstTextContent(content) {
        if (!content)
            return undefined;
        if (typeof content === 'string') {
            const trimmed = content.trim();
            return trimmed.length > 0 ? trimmed : undefined;
        }
        if (Array.isArray(content)) {
            const firstContent = content[0];
            if (firstContent?.type === 'text' && firstContent.text) {
                const trimmed = firstContent.text.trim();
                return trimmed.length > 0 ? trimmed : undefined;
            }
        }
        return undefined;
    }
    /**
     * Process a tool_result content block: look up pending tool call, emit tool_result event.
     */
    emitToolResult(content, agentId, sessionId, timestamp) {
        const resultContent = this.extractToolResultContent(content.content);
        const agentPendingCalls = this.pendingToolCalls.get(agentId);
        const pendingCall = agentPendingCalls?.get(content.tool_use_id);
        const toolName = pendingCall?.toolName;
        // Delete after lookup to prevent memory leak
        agentPendingCalls?.delete(content.tool_use_id);
        const toolResult = {
            agentId,
            sessionId,
            timestamp,
            toolUseId: content.tool_use_id,
            tool: toolName,
            preview: resultContent.substring(0, MESSAGE_TEXT_LIMIT),
            contentLength: resultContent.length,
            isError: content.is_error || false,
        };
        this.emit('subagent:tool_result', toolResult);
    }
    /**
     * Find the oldest inactive (non-active) agent for LRU eviction.
     * Returns the agent ID of the oldest inactive agent, or null if all are active.
     */
    findOldestInactiveAgent() {
        let oldestId = null;
        let oldestTime = Infinity;
        for (const [id, existing] of this.agentInfo) {
            if (existing.status !== 'active' && existing.lastActivityAt < oldestTime) {
                oldestTime = existing.lastActivityAt;
                oldestId = id;
            }
        }
        return oldestId;
    }
    /**
     * Extract short model identifier from full model name
     */
    extractModelShort(model) {
        const lower = model.toLowerCase();
        if (lower.includes('haiku'))
            return 'haiku';
        if (lower.includes('sonnet'))
            return 'sonnet';
        if (lower.includes('opus'))
            return 'opus';
        return undefined;
    }
    // ========== Public API ==========
    /**
     * Start watching for subagent activity
     */
    start() {
        if (this._isRunning)
            return;
        this._isRunning = true;
        // Initial scan (always runs immediately)
        this._pollCount = 0;
        this.scanForSubagents().catch((err) => this.emit('subagent:error', err));
        // Periodic scan for new subagent directories
        // Full directory traversal only every FULL_SCAN_EVERY_N_POLLS polls (~5s)
        // FSWatchers handle known directories between full scans
        this.cleanup.setInterval(() => {
            this._pollCount++;
            if (this._pollCount % FULL_SCAN_EVERY_N_POLLS === 0) {
                this.scanForSubagents().catch((err) => this.emit('subagent:error', err));
            }
        }, POLL_INTERVAL_MS, { description: 'subagent directory poll' });
        // Periodic liveness check for active subagents
        this.startLivenessChecker();
    }
    /**
     * Start periodic liveness checker
     * Detects when subagent processes have exited but status is still active/idle.
     *
     * Uses a 3-tier check to minimize cost:
     *   1. File mtime (stat ~0.3ms/agent) — if transcript modified recently, agent is alive
     *   2. Cached PID (/proc/{pid}/stat ~0.1ms) — if stored PID still exists, alive
     *   3. Full pgrep scan (expensive, ~500ms) — only for agents that fail tiers 1+2
     */
    startLivenessChecker() {
        this.cleanup.setInterval(async () => {
            // Guard: prevent concurrent liveness checks (avoids duplicate completed events)
            if (this._isCheckingLiveness)
                return;
            this._isCheckingLiveness = true;
            try {
                // Collect agents that need the expensive pgrep scan
                const needsFullScan = [];
                for (const [_agentId, info] of this.agentInfo) {
                    if (info.status !== 'active' && info.status !== 'idle')
                        continue;
                    // Tier 1: File mtime check (~0.3ms per agent)
                    if (await this.checkSubagentFileAlive(info))
                        continue;
                    // Tier 2: Cached PID check (~0.1ms per agent)
                    if (info.pid && (await this.checkPidAlive(info.pid)))
                        continue;
                    // Tiers 1+2 failed — need expensive scan for this agent
                    needsFullScan.push(info);
                }
                // Tier 3: Full pgrep scan — only if any agents failed cheap checks
                if (needsFullScan.length > 0) {
                    const pidMap = await this.getClaudePids();
                    for (const info of needsFullScan) {
                        // Re-check status in case another check completed this agent
                        if (info.status !== 'active' && info.status !== 'idle')
                            continue;
                        const alive = this.checkSubagentAliveFromPidMap(info, pidMap);
                        if (!alive) {
                            this.markSubagentAsCompleted(info);
                        }
                    }
                }
                // Periodically clean up stale completed agents (older than 24 hours)
                this.cleanupStaleAgents();
            }
            finally {
                this._isCheckingLiveness = false;
            }
        }, LIVENESS_CHECK_MS, { description: 'subagent liveness check' });
    }
    /**
     * Check if a PID is still alive via /proc/{pid}/stat (single file read, ~0.1ms).
     */
    async checkPidAlive(pid) {
        try {
            await statAsync(`/proc/${pid}`);
            return true;
        }
        catch {
            return false;
        }
    }
    /**
     * Run pgrep once and read /proc info for all Claude PIDs in parallel.
     * Returns a Map of pid -> { environ, cmdline } for subagent processes only.
     * Excludes main Codeman-managed Claude processes (CODEMAN_MUX=1).
     * Also updates cached PIDs on tracked agents when a match is found.
     */
    async getClaudePids() {
        const result = new Map();
        try {
            const pgrepOutput = await new Promise((resolve, reject) => {
                execFile('pgrep', ['-f', 'claude'], { encoding: 'utf8' }, (err, stdout) => {
                    if (err)
                        return reject(err);
                    resolve(stdout);
                });
            });
            const pids = pgrepOutput
                .trim()
                .split('\n')
                .filter(Boolean)
                .map((s) => parseInt(s, 10))
                .filter((n) => !Number.isNaN(n));
            // Read /proc for all PIDs in parallel
            await Promise.all(pids.map(async (pid) => {
                let environ = '';
                let cmdline = '';
                try {
                    environ = await readFile(`/proc/${pid}/environ`, 'utf8');
                }
                catch {
                    /* skip */
                }
                try {
                    cmdline = await readFile(`/proc/${pid}/cmdline`, 'utf8');
                }
                catch {
                    /* skip */
                }
                // Skip main Codeman-managed Claude processes — only track subagents
                if (environ.includes('CODEMAN_MUX=1'))
                    return;
                if (environ || cmdline) {
                    result.set(pid, { environ, cmdline });
                }
            }));
            // Update cached PIDs on tracked agents
            for (const [pid, procInfo] of result) {
                for (const [_agentId, info] of this.agentInfo) {
                    if (info.status !== 'active' && info.status !== 'idle')
                        continue;
                    if (procInfo.environ.includes(info.sessionId) || procInfo.cmdline.includes(info.sessionId)) {
                        info.pid = pid;
                        break; // Each PID belongs to at most one agent
                    }
                }
            }
        }
        catch {
            // pgrep returns non-zero if no matches
        }
        return result;
    }
    /**
     * Check if a subagent is alive using the pre-fetched pid map (no process spawning).
     */
    checkSubagentAliveFromPidMap(info, pidMap) {
        for (const [_pid, procInfo] of pidMap) {
            if (procInfo.environ.includes(info.sessionId) || procInfo.cmdline.includes(info.sessionId)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Check if a subagent's transcript file was recently modified.
     * Primary liveness signal — transcript files are written to continuously while agent is active.
     */
    async checkSubagentFileAlive(info) {
        try {
            const fileStat = await statAsync(info.filePath);
            const mtime = fileStat.mtime.getTime();
            const now = Date.now();
            if (now - mtime < FILE_ALIVE_THRESHOLD_MS) {
                return true;
            }
        }
        catch {
            // File doesn't exist or can't be read
        }
        return false;
    }
    /**
     * Check if the watcher is currently running
     */
    isRunning() {
        return this._isRunning;
    }
    /**
     * Stop watching and clean up all state
     */
    stop() {
        this._isRunning = false;
        // Dispose poll and liveness intervals, then re-create for potential restart
        this.cleanup.dispose();
        this.cleanup = new CleanupManager();
        // Clear file debouncers
        this.fileDeb.dispose();
        this.fileAgentContext.clear();
        // Remove error handlers before closing watchers to prevent memory leak
        for (const [dir, handler] of this.dirWatcherErrorHandlers) {
            const watcher = this.dirWatchers.get(dir);
            if (watcher)
                watcher.off('error', handler);
        }
        this.dirWatcherErrorHandlers.clear();
        for (const watcher of this.dirWatchers.values()) {
            watcher.close();
        }
        this.dirWatchers.clear();
        this.idleDeb.dispose();
        // Clear all state for clean restart
        this.filePositions.clear();
        this.agentInfo.clear();
        this.knownSubagentDirs.clear();
        this.pendingToolCalls.clear();
        this.parentDescriptionCache.clear();
        this._pollCount = 0;
    }
    /**
     * Clean up stale agents to prevent unbounded memory growth.
     * - Completed agents: removed after STALE_COMPLETED_MAX_AGE_MS (1 hour)
     * - Idle agents: removed after STALE_IDLE_MAX_AGE_MS (4 hours)
     * Also enforces MAX_TRACKED_AGENTS limit with LRU eviction.
     * Also cleans up orphaned pending tool calls older than PENDING_TOOL_CALL_TTL_MS.
     */
    cleanupStaleAgents() {
        const now = Date.now();
        const agentsToDelete = new Set();
        for (const [agentId, info] of this.agentInfo) {
            const age = now - info.lastActivityAt;
            // Clean up based on status and age
            if (info.status === 'completed' && age > STALE_COMPLETED_MAX_AGE_MS) {
                agentsToDelete.add(agentId);
            }
            else if (info.status === 'idle' && age > STALE_IDLE_MAX_AGE_MS) {
                agentsToDelete.add(agentId);
            }
        }
        // Enforce max tracked agents limit (LRU eviction)
        const currentCount = this.agentInfo.size - agentsToDelete.size;
        if (currentCount > MAX_TRACKED_AGENTS) {
            // Sort by lastActivityAt (oldest first) and evict oldest completed/idle agents
            const sortedAgents = Array.from(this.agentInfo.entries())
                .filter(([id]) => !agentsToDelete.has(id))
                .filter(([, info]) => info.status !== 'active') // Keep active agents
                .sort((a, b) => a[1].lastActivityAt - b[1].lastActivityAt);
            const toEvict = currentCount - MAX_TRACKED_AGENTS;
            for (let i = 0; i < toEvict && i < sortedAgents.length; i++) {
                agentsToDelete.add(sortedAgents[i][0]);
            }
        }
        // Perform cleanup
        for (const agentId of agentsToDelete) {
            this.removeAgent(agentId);
        }
        // Clean up orphaned pending tool calls (older than TTL)
        // These can accumulate if tool_result is never received (e.g., agent crashed)
        for (const [agentId, agentCalls] of this.pendingToolCalls) {
            const idsToDelete = [];
            for (const [toolUseId, callInfo] of agentCalls) {
                if (now - callInfo.timestamp > PENDING_TOOL_CALL_TTL_MS) {
                    idsToDelete.push(toolUseId);
                }
            }
            for (const id of idsToDelete) {
                agentCalls.delete(id);
            }
            // If agent has no more pending calls, remove the agent entry from the map
            if (agentCalls.size === 0) {
                this.pendingToolCalls.delete(agentId);
            }
        }
    }
    /**
     * Remove an agent and all its associated resources.
     */
    removeAgent(agentId) {
        const info = this.agentInfo.get(agentId);
        if (info) {
            this.agentInfo.delete(agentId);
            this.pendingToolCalls.delete(agentId);
            this.filePositions.delete(info.filePath);
            this.fileAgentContext.delete(info.filePath);
            this.fileDeb.cancelKey(info.filePath);
            this.idleDeb.cancelKey(agentId);
        }
    }
    /**
     * Manually trigger cleanup of stale agents.
     * Returns number of agents removed.
     */
    cleanupNow() {
        const beforeCount = this.agentInfo.size;
        this.cleanupStaleAgents();
        return beforeCount - this.agentInfo.size;
    }
    /**
     * Clear all tracked agents (for manual reset).
     * Returns number of agents cleared.
     */
    clearAll() {
        const count = this.agentInfo.size;
        const agentIds = Array.from(this.agentInfo.keys());
        for (const agentId of agentIds) {
            this.removeAgent(agentId);
        }
        return count;
    }
    /**
     * Get all known subagents
     */
    getSubagents() {
        return Array.from(this.agentInfo.values());
    }
    /**
     * Get subagents for a specific Codeman session
     * Maps Codeman working directory to Claude's project hash
     */
    getSubagentsForSession(workingDir) {
        const projectHash = this.getProjectHash(workingDir);
        return Array.from(this.agentInfo.values()).filter((info) => info.projectHash === projectHash);
    }
    /**
     * Get a specific subagent's info
     */
    getSubagent(agentId) {
        return this.agentInfo.get(agentId);
    }
    /**
     * Update a subagent's description.
     * Used to set the short description from TaskTracker when available.
     */
    updateDescription(agentId, description) {
        const info = this.agentInfo.get(agentId);
        if (info) {
            info.description = description;
            this.emit('subagent:updated', info);
            return true;
        }
        return false;
    }
    /**
     * Find sessions whose working directory matches a project hash.
     * Returns the project hash for a given working directory.
     */
    getProjectHashForDir(workingDir) {
        return this.getProjectHash(workingDir);
    }
    /**
     * Get internal statistics for memory monitoring.
     * Returns counts of internal Maps and resources.
     */
    getStats() {
        // Count pending tool calls across all agents
        let pendingToolCallsCount = 0;
        for (const agentCalls of this.pendingToolCalls.values()) {
            pendingToolCallsCount += agentCalls.size;
        }
        return {
            agentCount: this.agentInfo.size,
            fileDebouncerCount: this.fileDeb.size,
            dirWatcherCount: this.dirWatchers.size,
            idleTimerCount: this.idleDeb.size,
            pendingToolCallsCount,
            knownDirsCount: this.knownSubagentDirs.size,
            filePositionsCount: this.filePositions.size,
        };
    }
    /**
     * Get recent subagents (modified within specified minutes)
     */
    getRecentSubagents(minutes = 60) {
        const cutoff = Date.now() - minutes * 60 * 1000;
        return Array.from(this.agentInfo.values())
            .filter((info) => info.lastActivityAt > cutoff)
            .sort((a, b) => b.lastActivityAt - a.lastActivityAt);
    }
    /**
     * Kill a subagent by its agent ID
     * Uses cached PID first, falls back to findSubagentProcess if needed.
     */
    async killSubagent(agentId) {
        const info = this.agentInfo.get(agentId);
        if (!info)
            return false;
        // Already completed, nothing to kill
        if (info.status === 'completed')
            return false;
        try {
            // Always use findSubagentProcess for kill — it verifies environ/cmdline,
            // preventing PID reuse attacks (cached PID may have been recycled by OS)
            const pid = await this.findSubagentProcess(info.sessionId);
            if (pid) {
                process.kill(pid, 'SIGTERM');
                this.markSubagentAsCompleted(info);
                return true;
            }
        }
        catch {
            // Process may have already exited
        }
        // Mark as completed even if we couldn't find the process
        this.markSubagentAsCompleted(info);
        return true;
    }
    /**
     * Kill all subagents for a specific Codeman session.
     * IMPORTANT: Must scope to sessionId to avoid cross-session kills.
     * All sessions in the same workingDir share a projectHash, so filtering
     * by workingDir alone would kill subagents belonging to OTHER sessions.
     */
    async killSubagentsForSession(workingDir, sessionId) {
        const subagents = this.getSubagentsForSession(workingDir);
        for (const agent of subagents) {
            if (agent.status === 'active' || agent.status === 'idle') {
                // Only kill subagents belonging to this specific session
                if (sessionId && agent.sessionId !== sessionId)
                    continue;
                await this.killSubagent(agent.agentId);
            }
        }
    }
    /**
     * Find the process ID of a Claude subagent by its session ID.
     * Searches /proc for claude processes with matching session ID in environment.
     * Skips main Codeman-managed Claude processes (identified by CODEMAN_MUX=1).
     * Caches the discovered PID on the matching agent info for future fast checks.
     */
    async findSubagentProcess(sessionId) {
        try {
            // Find all claude processes (async to avoid blocking event loop)
            const pgrepOutput = await new Promise((resolve, reject) => {
                execFile('pgrep', ['-f', 'claude'], { encoding: 'utf8' }, (err, stdout) => {
                    if (err)
                        return reject(err);
                    resolve(stdout);
                });
            });
            const pids = pgrepOutput.trim().split('\n').filter(Boolean);
            for (const pidStr of pids) {
                const pid = parseInt(pidStr, 10);
                if (Number.isNaN(pid))
                    continue;
                let environ = '';
                try {
                    environ = await readFile(`/proc/${pid}/environ`, 'utf8');
                }
                catch {
                    // Can't read this process's environ - skip
                }
                // Defense-in-depth: never kill a main Codeman-managed Claude process.
                // Main processes have CODEMAN_MUX=1 in their environment; subagents don't.
                if (environ.includes('CODEMAN_MUX=1'))
                    continue;
                if (environ.includes(sessionId)) {
                    // Cache PID on the matching agent
                    this.cacheAgentPid(sessionId, pid);
                    return pid;
                }
                try {
                    const cmdline = await readFile(`/proc/${pid}/cmdline`, 'utf8');
                    if (cmdline.includes(sessionId)) {
                        this.cacheAgentPid(sessionId, pid);
                        return pid;
                    }
                }
                catch {
                    // Can't read this process's cmdline - skip
                }
            }
        }
        catch {
            // pgrep returns non-zero if no matches
        }
        return null;
    }
    /**
     * Store a discovered PID on the agent info with matching sessionId.
     */
    cacheAgentPid(sessionId, pid) {
        for (const [_agentId, info] of this.agentInfo) {
            if (info.sessionId === sessionId && (info.status === 'active' || info.status === 'idle')) {
                info.pid = pid;
                return;
            }
        }
    }
    /**
     * Get transcript for a subagent (optionally limited to last N entries)
     */
    async getTranscript(agentId, limit) {
        const info = this.agentInfo.get(agentId);
        if (!info)
            return [];
        const entries = [];
        try {
            const content = await readFile(info.filePath, 'utf8');
            const lines = content.split('\n').filter((l) => l.trim());
            for (const line of lines) {
                try {
                    const entry = JSON.parse(line);
                    entries.push(entry);
                }
                catch {
                    // Skip malformed lines
                }
            }
        }
        catch {
            // File read error
        }
        if (limit && limit > 0) {
            return entries.slice(-limit);
        }
        return entries;
    }
    /**
     * Format transcript entries for display
     */
    formatTranscript(entries) {
        const lines = [];
        for (const entry of entries) {
            if (entry.type === 'progress' && entry.data) {
                lines.push(this.formatProgress(entry));
            }
            else if (entry.type === 'assistant' && entry.message?.content) {
                // Handle both string and array content formats
                if (typeof entry.message.content === 'string') {
                    const text = entry.message.content.trim();
                    if (text.length > 0) {
                        const preview = text.length > TEXT_PREVIEW_LENGTH ? text.substring(0, TEXT_PREVIEW_LENGTH) + '...' : text;
                        lines.push(`${this.formatTime(entry.timestamp)} 💬 ${preview.replace(/\n/g, ' ')}`);
                    }
                }
                else {
                    for (const content of entry.message.content) {
                        if (content.type === 'tool_use' && content.name) {
                            lines.push(this.formatToolCall(entry.timestamp, content.name, content.input || {}));
                        }
                        else if (content.type === 'text' && content.text) {
                            const text = content.text.trim();
                            if (text.length > 0) {
                                const preview = text.length > TEXT_PREVIEW_LENGTH ? text.substring(0, TEXT_PREVIEW_LENGTH) + '...' : text;
                                lines.push(`${this.formatTime(entry.timestamp)} 💬 ${preview.replace(/\n/g, ' ')}`);
                            }
                        }
                    }
                }
            }
            else if (entry.type === 'user' && entry.message?.content) {
                // Handle both string and array content formats
                const text = this.extractFirstTextContent(entry.message.content);
                if (text && text.length < 100 && !text.includes('{')) {
                    lines.push(`${this.formatTime(entry.timestamp)} 📥 User: ${text.substring(0, USER_TEXT_PREVIEW_LENGTH)}`);
                }
            }
        }
        return lines;
    }
    // ========== Private Methods ==========
    /**
     * Convert working directory to Claude's project hash format
     */
    getProjectHash(workingDir) {
        return workingDir.replace(/\//g, '-');
    }
    /**
     * Extract a smart, concise title from a task prompt
     * Aims for ~40-50 chars that convey what the agent is doing
     */
    extractSmartTitle(text) {
        const MAX_LEN = SMART_TITLE_MAX_LENGTH;
        // Get first line/sentence
        const title = text.split('\n')[0].trim();
        // If already short enough, use it
        if (title.length <= MAX_LEN) {
            return title.replace(/[.!?,\s]+$/, '');
        }
        // Remove common filler phrases to condense
        const fillers = [
            /^(please |i need you to |i want you to |can you |could you )/i,
            / (the|a|an) /gi,
            / (in|at|on|to|for|of|with|from|by) the /gi,
            / (including|related to|regarding|about) /gi,
            /[""]/g,
            / +/g, // multiple spaces to single
        ];
        let condensed = title;
        for (const filler of fillers) {
            condensed = condensed.replace(filler, (match) => {
                // Keep single space for word boundaries
                if (match.trim() === '')
                    return ' ';
                if (/^(the|a|an)$/i.test(match.trim()))
                    return ' ';
                if (/including|related to|regarding|about/i.test(match))
                    return ': ';
                return ' ';
            });
        }
        condensed = condensed.replace(/ +/g, ' ').trim();
        // If condensed version is short enough, use it
        if (condensed.length <= MAX_LEN) {
            return condensed.replace(/[.!?,:\s]+$/, '');
        }
        // Try to cut at a natural boundary (colon, dash, comma)
        const boundaryMatch = condensed.substring(0, MAX_LEN + 5).match(/^(.{20,}?)[:\-,]/);
        if (boundaryMatch && boundaryMatch[1].length <= MAX_LEN) {
            return boundaryMatch[1].trim();
        }
        // Last resort: truncate at word boundary
        const truncated = condensed.substring(0, MAX_LEN);
        const lastSpace = truncated.lastIndexOf(' ');
        if (lastSpace > 20) {
            return truncated.substring(0, lastSpace).replace(/[.!?,:\s]+$/, '');
        }
        return truncated.replace(/[.!?,:\s]+$/, '');
    }
    async _resolveDescription(projectHash, sessionId, agentId, filePath, fallbackText) {
        // First try parent transcript (most reliable)
        const fromParent = await this.extractDescriptionFromParentTranscript(projectHash, sessionId, agentId);
        if (fromParent)
            return fromParent;
        // Fallback: inline text (from processEntry) or file extraction
        if (fallbackText) {
            return this.extractSmartTitle(fallbackText);
        }
        return this.extractDescriptionFromFile(filePath);
    }
    /**
     * Extract the short description from the parent session's transcript.
     * This is the most reliable method because it reads the actual Task tool result
     * that spawned this agent, which contains the description directly.
     *
     * The parent transcript contains a 'user' entry with toolUseResult that has:
     * - agentId: the spawned agent's ID
     * - description: the Task description parameter
     *
     * We look for this format:
     * { "type": "user", "toolUseResult": { "agentId": "xxx", "description": "..." } }
     */
    async extractDescriptionFromParentTranscript(projectHash, sessionId, agentId) {
        const cacheKey = `${projectHash}/${sessionId}`;
        const CACHE_TTL_MS = 5000;
        // Check cache first (covers burst of simultaneous agent discoveries)
        const cached = this.parentDescriptionCache.get(cacheKey);
        if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
            return cached.descriptions.get(agentId);
        }
        try {
            // The parent session's transcript is at: ~/.claude/projects/{projectHash}/{sessionId}.jsonl
            const transcriptPath = join(CLAUDE_PROJECTS_DIR, projectHash, `${sessionId}.jsonl`);
            let fileSize;
            try {
                const fileStat = await statAsync(transcriptPath);
                fileSize = fileStat.size;
            }
            catch {
                return undefined;
            }
            // Only read last 16KB — toolUseResult entries are near the end of the transcript
            const TAIL_BYTES = 16384;
            const startOffset = Math.max(0, fileSize - TAIL_BYTES);
            const content = await new Promise((resolve, reject) => {
                const chunks = [];
                const stream = createReadStream(transcriptPath, { start: startOffset, encoding: 'utf8' });
                stream.on('data', (chunk) => chunks.push(String(chunk)));
                stream.on('end', () => resolve(chunks.join('')));
                stream.on('error', reject);
            });
            let lines = content.split('\n').filter((l) => l.trim());
            // If we started mid-file, drop the first partial line
            if (startOffset > 0 && lines.length > 0) {
                lines = lines.slice(1);
            }
            // Parse ALL toolUseResult entries into a Map and cache them
            const descriptions = new Map();
            for (const line of lines) {
                try {
                    const entry = JSON.parse(line);
                    if (entry.type === 'user' && entry.toolUseResult?.agentId && entry.toolUseResult?.description) {
                        descriptions.set(entry.toolUseResult.agentId, entry.toolUseResult.description);
                    }
                }
                catch {
                    // Skip malformed lines
                }
            }
            this.parentDescriptionCache.set(cacheKey, { descriptions, timestamp: Date.now() });
            return descriptions.get(agentId);
        }
        catch {
            // Failed to read transcript
        }
        return undefined;
    }
    /**
     * Extract description from agent file by finding first user message
     */
    async extractDescriptionFromFile(filePath) {
        try {
            // Only read the first 8KB — more than enough for 5 JSONL lines
            const stream = createReadStream(filePath, { end: FILE_PEEK_BYTES });
            const rl = createInterface({ input: stream });
            return await new Promise((resolve) => {
                let lineCount = 0;
                let resolved = false;
                rl.on('line', (line) => {
                    if (resolved || lineCount >= 5) {
                        rl.close();
                        stream.destroy();
                        return;
                    }
                    lineCount++;
                    if (!line.trim())
                        return;
                    try {
                        const entry = JSON.parse(line);
                        if (entry.type === 'user' && entry.message?.content) {
                            const text = this.extractFirstTextContent(entry.message.content);
                            if (text) {
                                resolved = true;
                                rl.close();
                                stream.destroy();
                                resolve(this.extractSmartTitle(text));
                            }
                        }
                    }
                    catch {
                        // Skip malformed lines
                    }
                });
                rl.on('close', () => {
                    if (!resolved)
                        resolve(undefined);
                });
                rl.on('error', () => {
                    if (!resolved)
                        resolve(undefined);
                });
            });
        }
        catch {
            // Failed to read file
        }
        return undefined;
    }
    /**
     * Scan for all subagent directories (async to avoid blocking event loop)
     */
    async scanForSubagents() {
        try {
            await statAsync(CLAUDE_PROJECTS_DIR);
        }
        catch {
            return;
        }
        try {
            const projects = await readdir(CLAUDE_PROJECTS_DIR);
            for (const project of projects) {
                const projectPath = join(CLAUDE_PROJECTS_DIR, project);
                try {
                    const st = await statAsync(projectPath);
                    if (!st.isDirectory())
                        continue;
                    const sessions = await readdir(projectPath);
                    for (const session of sessions) {
                        const sessionPath = join(projectPath, session);
                        try {
                            const sessionStat = await statAsync(sessionPath);
                            if (!sessionStat.isDirectory())
                                continue;
                            const subagentDir = join(sessionPath, 'subagents');
                            try {
                                await statAsync(subagentDir);
                                await this.watchSubagentDir(subagentDir, project, session);
                            }
                            catch {
                                // subagent dir doesn't exist - skip
                            }
                        }
                        catch {
                            // Skip inaccessible session directories
                        }
                    }
                }
                catch {
                    // Skip inaccessible project directories
                }
            }
        }
        catch (error) {
            this.emit('subagent:error', error);
        }
    }
    /**
     * Watch a subagent directory for new and changed files.
     * Uses a single directory-level fs.watch() instead of per-file watchers.
     * On Linux, inotify IN_MODIFY fires for content changes within the directory.
     */
    async watchSubagentDir(dir, projectHash, sessionId) {
        if (this.knownSubagentDirs.has(dir))
            return;
        this.knownSubagentDirs.add(dir);
        // Register existing files (initial scan - skip old files)
        try {
            const files = await readdir(dir);
            for (const file of files) {
                if (file.endsWith('.jsonl')) {
                    await this.registerAgentFile(join(dir, file), projectHash, sessionId, true);
                }
            }
        }
        catch {
            return;
        }
        // Single directory watcher handles both new files and file content changes
        try {
            const watcher = watch(dir, (_eventType, filename) => {
                if (!filename?.endsWith('.jsonl'))
                    return;
                const filePath = join(dir, filename);
                // Debounce 100ms to batch rapid writes
                this.fileDeb.schedule(filePath, () => {
                    if (!existsSync(filePath))
                        return;
                    if (this.fileAgentContext.has(filePath)) {
                        // Known file — handle content change
                        this.handleFileChange(filePath).catch(() => { }); // Ignore - errors logged internally, don't crash watcher callback
                    }
                    else {
                        // New file — register it
                        this.registerAgentFile(filePath, projectHash, sessionId).catch(() => { }); // Ignore - errors logged internally, don't crash watcher callback
                    }
                });
            });
            // Handle watcher errors to prevent unhandled exceptions
            // Store handler reference for proper cleanup
            const errorHandler = (error) => {
                this.emit('subagent:error', error instanceof Error ? error : new Error(String(error)));
                watcher.close();
                this.dirWatcherErrorHandlers.delete(dir);
                this.dirWatchers.delete(dir);
                this.knownSubagentDirs.delete(dir);
            };
            watcher.on('error', errorHandler);
            this.dirWatcherErrorHandlers.set(dir, errorHandler);
            this.dirWatchers.set(dir, watcher);
        }
        catch {
            // Watch failed
        }
    }
    /**
     * Handle a file content change for an already-registered agent file.
     * Tails from last known position, updates info, retries description if missing.
     */
    async handleFileChange(filePath) {
        const context = this.fileAgentContext.get(filePath);
        if (!context)
            return;
        const agentId = basename(filePath).replace('agent-', '').replace('.jsonl', '');
        const currentPos = this.filePositions.get(filePath) || 0;
        const newPos = await this.tailFile(filePath, agentId, context.sessionId, currentPos);
        this.filePositions.set(filePath, newPos);
        // Update info
        const existingInfo = this.agentInfo.get(agentId);
        if (existingInfo) {
            try {
                const newStat = await statAsync(filePath);
                existingInfo.lastActivityAt = Date.now();
                existingInfo.fileSize = newStat.size;
                existingInfo.status = 'active';
            }
            catch {
                // Stat failed
            }
            // Retry description extraction if missing (race condition fix)
            if (!existingInfo.description) {
                const extractedDescription = await this._resolveDescription(existingInfo.projectHash, existingInfo.sessionId, agentId, filePath);
                if (extractedDescription) {
                    if (this.isInternalAgent(extractedDescription)) {
                        this.removeAgent(agentId);
                        return;
                    }
                    existingInfo.description = extractedDescription;
                    this.emit('subagent:updated', existingInfo);
                }
            }
            // Reset idle timer
            this.resetIdleTimer(agentId);
        }
    }
    /**
     * Register a specific agent transcript file (discovery + initial read).
     * Does NOT create a per-file watcher — the directory watcher handles changes.
     * @param filePath Path to the agent transcript file
     * @param projectHash Claude project hash
     * @param sessionId Claude session ID
     * @param isInitialScan If true, skip files older than STARTUP_MAX_FILE_AGE_MS
     */
    async registerAgentFile(filePath, projectHash, sessionId, isInitialScan = false) {
        if (this.fileAgentContext.has(filePath))
            return;
        const agentId = basename(filePath).replace('agent-', '').replace('.jsonl', '');
        // Initial info - handle race condition where file may be deleted between discovery and stat
        let fileStat;
        try {
            fileStat = await statAsync(filePath);
        }
        catch {
            // File was deleted between discovery and stat - skip this agent
            return;
        }
        // On initial scan, skip old files to avoid loading stale historical data
        if (isInitialScan) {
            const fileAge = Date.now() - fileStat.mtime.getTime();
            if (fileAge > STARTUP_MAX_FILE_AGE_MS) {
                return; // Skip old files on startup
            }
        }
        // Extract description - prefer reading from parent transcript (most reliable)
        const description = await this._resolveDescription(projectHash, sessionId, agentId, filePath);
        // Skip internal Claude Code agents (e.g., suggestion mode) - not real subagents
        if (this.isInternalAgent(description)) {
            return;
        }
        const info = {
            agentId,
            sessionId,
            projectHash,
            filePath,
            startedAt: fileStat.birthtime.toISOString(),
            lastActivityAt: fileStat.mtime.getTime(),
            status: 'active',
            toolCallCount: 0,
            entryCount: 0,
            fileSize: fileStat.size,
            description,
        };
        // Enforce MAX_TRACKED_AGENTS during insertion — evict oldest inactive agent
        if (this.agentInfo.size >= MAX_TRACKED_AGENTS) {
            const oldestId = this.findOldestInactiveAgent();
            if (oldestId) {
                this.removeAgent(oldestId);
            }
        }
        // Track file context for directory watcher change handling
        this.fileAgentContext.set(filePath, { projectHash, sessionId });
        this.agentInfo.set(agentId, info);
        this.emit('subagent:discovered', info);
        // Read existing content
        this.tailFile(filePath, agentId, sessionId, 0)
            .then((position) => {
            this.filePositions.set(filePath, position);
        })
            .catch((err) => {
            // Log but don't throw - non-critical background operation
            console.warn(`[SubagentWatcher] Failed to read initial content for ${agentId}:`, err);
        });
        this.resetIdleTimer(agentId);
    }
    /**
     * Tail a file from a specific position
     */
    async tailFile(filePath, agentId, sessionId, fromPosition) {
        return new Promise((resolve) => {
            let position = fromPosition;
            const stream = createReadStream(filePath, { start: fromPosition });
            const rl = createInterface({ input: stream });
            rl.on('line', (line) => {
                const lineBytes = Buffer.byteLength(line, 'utf8') + 1;
                position += lineBytes; // Always advance past the line
                try {
                    const entry = JSON.parse(line);
                    this.processEntry(entry, agentId, sessionId).catch(() => {
                        // processEntry failure is non-critical
                    });
                    // Update entry count
                    const info = this.agentInfo.get(agentId);
                    if (info) {
                        info.entryCount++;
                    }
                }
                catch {
                    // Malformed JSON line — skip it
                }
            });
            rl.on('close', () => {
                resolve(position);
            });
            rl.on('error', () => {
                resolve(position);
            });
        });
    }
    /**
     * Process a transcript entry and emit appropriate events
     */
    async processEntry(entry, agentId, sessionId) {
        const info = this.agentInfo.get(agentId);
        if (info) {
            this._processModelInfo(entry, info);
            this._processTokenInfo(entry, info);
            if (await this._processDescription(entry, agentId, info))
                return;
        }
        if (entry.type === 'progress' && entry.data) {
            const progress = {
                agentId,
                sessionId,
                timestamp: entry.timestamp,
                progressType: entry.data.type,
                query: entry.data.query,
                resultCount: entry.data.resultCount,
                hookEvent: entry.data.hookEvent,
                hookName: entry.data.hookName ||
                    (entry.data.hookEvent && entry.data.tool_name
                        ? `${entry.data.hookEvent}:${entry.data.tool_name}`
                        : undefined),
            };
            this.emit('subagent:progress', progress);
        }
        else if (entry.type === 'assistant' && entry.message?.content) {
            this._processAssistantContent(entry, agentId, sessionId);
        }
        else if (entry.type === 'user' && entry.message?.content) {
            this._processUserContent(entry, agentId, sessionId);
        }
    }
    _processModelInfo(entry, agent) {
        if (entry.type === 'assistant' && entry.message?.model && !agent.model) {
            agent.model = entry.message.model;
            agent.modelShort = this.extractModelShort(entry.message.model);
            this.emit('subagent:updated', agent);
        }
    }
    _processTokenInfo(entry, agent) {
        if (!entry.message?.usage)
            return;
        if (entry.message.usage.input_tokens) {
            agent.totalInputTokens = (agent.totalInputTokens || 0) + entry.message.usage.input_tokens;
        }
        if (entry.message.usage.output_tokens) {
            agent.totalOutputTokens = (agent.totalOutputTokens || 0) + entry.message.usage.output_tokens;
        }
    }
    async _processDescription(entry, agentId, agent) {
        if (agent.description || entry.type !== 'user' || !entry.message?.content)
            return false;
        const fallbackText = this.extractFirstTextContent(entry.message.content);
        const description = await this._resolveDescription(agent.projectHash, agent.sessionId, agentId, agent.filePath, fallbackText);
        if (description) {
            if (this.isInternalAgent(description)) {
                this.removeAgent(agentId);
                return true;
            }
            agent.description = description;
            this.emit('subagent:updated', agent);
        }
        return false;
    }
    _processAssistantContent(entry, agentId, sessionId) {
        const messageContent = entry.message.content;
        if (typeof messageContent === 'string') {
            const text = messageContent.trim();
            if (text.length > 0) {
                const message = {
                    agentId,
                    sessionId,
                    timestamp: entry.timestamp,
                    role: 'assistant',
                    text: text.substring(0, MESSAGE_TEXT_LIMIT),
                };
                this.emit('subagent:message', message);
            }
        }
        else {
            for (const content of messageContent) {
                if (content.type === 'tool_use' && content.name) {
                    // Store toolUseId for linking to results, with timestamp for TTL cleanup
                    if (content.id) {
                        if (!this.pendingToolCalls.has(agentId)) {
                            this.pendingToolCalls.set(agentId, new Map());
                        }
                        const agentCalls = this.pendingToolCalls.get(agentId);
                        // Enforce size limit to prevent memory leak from rapid tool calls
                        if (agentCalls.size >= MAX_PENDING_TOOL_CALLS) {
                            // FIFO eviction: delete first (oldest) entry using Map insertion order
                            const firstKey = agentCalls.keys().next().value;
                            if (firstKey !== undefined)
                                agentCalls.delete(firstKey);
                        }
                        agentCalls.set(content.id, {
                            toolName: content.name,
                            timestamp: Date.now(),
                        });
                    }
                    const toolCall = {
                        agentId,
                        sessionId,
                        timestamp: entry.timestamp,
                        tool: content.name,
                        input: this.getTruncatedInput(content.name, content.input || {}),
                        toolUseId: content.id,
                        fullInput: content.input || {},
                    };
                    this.emit('subagent:tool_call', toolCall);
                    // Update tool call count
                    const agentInfo = this.agentInfo.get(agentId);
                    if (agentInfo) {
                        agentInfo.toolCallCount++;
                    }
                }
                else if (content.type === 'tool_result' && content.tool_use_id) {
                    this.emitToolResult({ tool_use_id: content.tool_use_id, content: content.content, is_error: content.is_error }, agentId, sessionId, entry.timestamp);
                }
                else if (content.type === 'text' && content.text) {
                    const text = content.text.trim();
                    if (text.length > 0) {
                        const message = {
                            agentId,
                            sessionId,
                            timestamp: entry.timestamp,
                            role: 'assistant',
                            text: text.substring(0, MESSAGE_TEXT_LIMIT),
                        };
                        this.emit('subagent:message', message);
                    }
                }
            }
        }
    }
    _processUserContent(entry, agentId, sessionId) {
        const messageContent = entry.message.content;
        if (typeof messageContent === 'string') {
            const userText = messageContent.trim();
            if (userText.length > 0 && userText.length < 500) {
                const message = {
                    agentId,
                    sessionId,
                    timestamp: entry.timestamp,
                    role: 'user',
                    text: userText,
                };
                this.emit('subagent:message', message);
            }
        }
        else {
            // Check for tool_result blocks in user messages (common pattern)
            for (const content of messageContent) {
                if (content.type === 'tool_result' && content.tool_use_id) {
                    this.emitToolResult({ tool_use_id: content.tool_use_id, content: content.content, is_error: content.is_error }, agentId, sessionId, entry.timestamp);
                }
                else if (content.type === 'text' && content.text) {
                    const userText = content.text.trim();
                    if (userText.length > 0 && userText.length < 500) {
                        const message = {
                            agentId,
                            sessionId,
                            timestamp: entry.timestamp,
                            role: 'user',
                            text: userText,
                        };
                        this.emit('subagent:message', message);
                    }
                }
            }
        }
    }
    /**
     * Extract text content from tool_result content field
     */
    extractToolResultContent(content) {
        if (!content)
            return '';
        if (typeof content === 'string')
            return content;
        if (Array.isArray(content)) {
            return content
                .filter((c) => c.type === 'text' && c.text)
                .map((c) => c.text)
                .join('\n');
        }
        return '';
    }
    /**
     * Get truncated input for display (keeps primary param, truncates large content)
     */
    getTruncatedInput(_tool, input) {
        const truncated = {};
        for (const [key, value] of Object.entries(input)) {
            if (typeof value === 'string' && value.length > INPUT_TRUNCATE_LENGTH) {
                // Keep short preview of long strings
                truncated[key] = value.substring(0, INPUT_TRUNCATE_LENGTH) + '...';
            }
            else {
                truncated[key] = value;
            }
        }
        return truncated;
    }
    /**
     * Reset idle timer for an agent
     */
    resetIdleTimer(agentId) {
        this.idleDeb.schedule(agentId, () => {
            const info = this.agentInfo.get(agentId);
            if (!info)
                return;
            if (info.status === 'active') {
                info.status = 'idle';
            }
        });
    }
    /**
     * Format a tool call for display
     */
    formatToolCall(timestamp, name, input) {
        const icons = {
            WebSearch: '🔍',
            WebFetch: '🌐',
            Read: '📖',
            Write: '📝',
            Edit: '✏️',
            Bash: '💻',
            Glob: '📁',
            Grep: '🔎',
            Task: '🤖',
        };
        const icon = icons[name] || '🔧';
        let details = '';
        if (name === 'WebSearch' && input.query) {
            details = `"${input.query}"`;
        }
        else if (name === 'WebFetch' && input.url) {
            details = input.url;
        }
        else if (name === 'Read' && input.file_path) {
            details = input.file_path;
        }
        else if ((name === 'Write' || name === 'Edit') && input.file_path) {
            details = input.file_path;
        }
        else if (name === 'Bash' && input.command) {
            const cmd = input.command;
            details = cmd.length > COMMAND_DISPLAY_LENGTH ? cmd.substring(0, COMMAND_DISPLAY_LENGTH) + '...' : cmd;
        }
        else if (name === 'Glob' && input.pattern) {
            details = input.pattern;
        }
        else if (name === 'Grep' && input.pattern) {
            details = input.pattern;
        }
        else if (name === 'Task' && input.description) {
            details = input.description;
        }
        return `${this.formatTime(timestamp)} ${icon} ${name}: ${details}`;
    }
    /**
     * Format a progress event for display
     */
    formatProgress(entry) {
        const data = entry.data;
        if (data.type === 'query_update') {
            return `${this.formatTime(entry.timestamp)} ⟳ Searching: "${data.query}"`;
        }
        else if (data.type === 'search_results_received') {
            return `${this.formatTime(entry.timestamp)} ✓ Got ${data.resultCount} results`;
        }
        return `${this.formatTime(entry.timestamp)} Progress: ${data.type}`;
    }
    /**
     * Format timestamp for display
     */
    formatTime(timestamp) {
        return new Date(timestamp).toLocaleTimeString();
    }
}
// Export singleton instance
export const subagentWatcher = new SubagentWatcher();
//# sourceMappingURL=subagent-watcher.js.map