"use strict";
/**
 * CLI Tool response polling.
 * Periodically checks tmux sessions for CLI tool responses (Claude, Codex, Gemini, Vibe Local, OpenCode).
 *
 * Key responsibilities:
 * - Extract completed responses from tmux output (extractResponse)
 * - Detect interactive prompts and save as prompt messages
 * - Clean tool-specific artifacts from response content
 * - Manage polling lifecycle (start/stop/timeout)
 *
 * Issue #188 improvements:
 * - DR-004: Tail-line windowing for thinking detection in extractResponse
 * - MF-001 fix: Same windowing applied to checkForResponse thinking check
 * - SF-003: RESPONSE_THINKING_TAIL_LINE_COUNT constant tracks STATUS_THINKING_LINE_COUNT
 *
 * Issue #379 additions:
 * - OpenCode completion detection via Build summary line (isOpenCodeComplete)
 * - OpenCode response cleaning (cleanOpenCodeResponse)
 * - OpenCode extraction stop conditions (OPENCODE_PROMPT_PATTERN, OPENCODE_PROMPT_AFTER_RESPONSE)
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractTuiContentLines = extractTuiContentLines;
exports.findOverlapIndex = findOverlapIndex;
exports.initTuiAccumulator = initTuiAccumulator;
exports.accumulateTuiContent = accumulateTuiContent;
exports.getAccumulatedContent = getAccumulatedContent;
exports.clearTuiAccumulator = clearTuiAccumulator;
exports.cleanClaudeResponse = cleanClaudeResponse;
exports.cleanGeminiResponse = cleanGeminiResponse;
exports.isOpenCodeComplete = isOpenCodeComplete;
exports.cleanOpenCodeResponse = cleanOpenCodeResponse;
exports.resolveExtractionStartIndex = resolveExtractionStartIndex;
exports.startPolling = startPolling;
exports.stopPolling = stopPolling;
exports.stopAllPolling = stopAllPolling;
exports.getActivePollers = getActivePollers;
const cli_session_1 = require("./cli-session");
const db_instance_1 = require("./db-instance");
const db_1 = require("./db");
const ws_server_1 = require("./ws-server");
const prompt_detector_1 = require("./prompt-detector");
const conversation_logger_1 = require("./conversation-logger");
const claude_output_1 = require("./claude-output");
const cli_patterns_1 = require("./cli-patterns");
/**
 * Polling interval in milliseconds (default: 2 seconds)
 */
const POLLING_INTERVAL = 2000;
/**
 * Maximum polling duration in milliseconds (default: 30 minutes)
 * Previously 5 minutes, which caused silent polling stops for long-running tasks.
 */
const MAX_POLLING_DURATION = 30 * 60 * 1000;
/**
 * Number of tail lines to check for active thinking indicators in response extraction.
 *
 * SF-003 coupling note: This value must track STATUS_THINKING_LINE_COUNT (5) in
 * status-detector.ts. Both constants exist because they serve separate modules:
 *   - RESPONSE_THINKING_TAIL_LINE_COUNT: response-poller.ts (response extraction)
 *   - STATUS_THINKING_LINE_COUNT: status-detector.ts (UI status display)
 * If STATUS_THINKING_LINE_COUNT changes, update this value accordingly.
 *
 * Why not a shared constant? status-detector.ts has no dependency on response-poller.ts
 * and vice versa. Introducing a shared module would create a coupling that does not
 * currently exist. The test suite validates consistency (SF-003 test).
 *
 * @constant
 */
const RESPONSE_THINKING_TAIL_LINE_COUNT = 5;
/**
 * Gemini auth/loading state indicators that should not be treated as complete responses.
 * Braille spinner characters are shared with CLAUDE_SPINNER_CHARS in cli-patterns.ts.
 * Extracted to module level for clarity and to avoid re-creation on each call.
 */
const GEMINI_LOADING_INDICATORS = [
    'Waiting for auth',
    '\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f',
];
/**
 * Creates an incomplete extraction result with empty response.
 * Centralizes the repeated pattern of returning an in-progress/incomplete state.
 *
 * @param lineCount - Current line count for state tracking
 * @returns ExtractionResult with empty response and isComplete: false
 */
function incompleteResult(lineCount) {
    return { response: '', isComplete: false, lineCount };
}
/**
 * Build a complete ExtractionResult for a detected prompt.
 *
 * Shared between Claude early prompt detection (section 3-4, site 1) and
 * fallback prompt detection (section 3-4, site 2) in extractResponse().
 * Applies resolveExtractionStartIndex() to limit extraction to lastCapturedLine
 * onwards, then strips ANSI codes for safe DB storage (Stage 4 MF-001).
 *
 * @param lines - The trimmed tmux buffer lines array
 * @param lastCapturedLine - Number of lines previously captured
 * @param totalLines - Total line count in the buffer
 * @param bufferReset - External buffer reset flag
 * @param cliToolId - CLI tool identifier
 * @param findRecentUserPromptIndex - Callback to locate the most recent user prompt
 * @returns ExtractionResult with isComplete: true and ANSI-stripped response
 */
function buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection) {
    const startIndex = resolveExtractionStartIndex(lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex);
    const extractedLines = lines.slice(startIndex);
    return {
        response: (0, cli_patterns_1.stripAnsi)(extractedLines.join('\n')),
        isComplete: true,
        lineCount: totalLines,
        promptDetection,
        bufferReset,
    };
}
/**
 * Number of lines from the end of accumulated content to use as
 * fingerprint for overlap detection with the next capture.
 */
const OVERLAP_FINGERPRINT_SIZE = 10;
/**
 * Per-session TUI response accumulator storage.
 * Key: pollerKey ("worktreeId:cliToolId")
 */
const tuiResponseAccumulator = new Map();
/**
 * Normalize a single OpenCode TUI line by removing ANSI codes and border glyphs.
 * Returns an empty string when the line has no meaningful content after cleanup.
 */
function normalizeOpenCodeLine(line) {
    return (0, cli_patterns_1.stripBoxDrawing)((0, cli_patterns_1.stripAnsi)(line))
        .replace(/^\u2503\s?/, '')
        .replace(/\s*\u2503$/, '')
        .trim();
}
/**
 * Extract meaningful content lines from raw TUI output.
 * Strips ANSI codes, box-drawing characters, and lines matching OPENCODE_SKIP_PATTERNS.
 *
 * @param rawOutput - Raw tmux capture-pane output
 * @returns Array of cleaned, non-empty content lines
 *
 * @internal Exported for unit testing
 */
function extractTuiContentLines(rawOutput) {
    const lines = rawOutput.split('\n');
    const contentLines = [];
    for (const line of lines) {
        const cleaned = normalizeOpenCodeLine(line);
        if (!cleaned)
            continue;
        const shouldSkip = cli_patterns_1.OPENCODE_SKIP_PATTERNS.some(pattern => pattern.test(cleaned));
        if (shouldSkip)
            continue;
        // Also skip the Build summary line (completion indicator)
        if (cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleaned))
            continue;
        contentLines.push(cleaned);
    }
    return contentLines;
}
/**
 * Find the overlap index between previously accumulated lines and newly captured lines.
 * Searches for the longest suffix of `previous` that matches a prefix of `current`.
 *
 * @param previous - Previously accumulated lines (fingerprint subset)
 * @param current - Newly captured content lines
 * @returns Number of overlapping lines (0 if no overlap found)
 *
 * @internal Exported for unit testing
 */
function findOverlapIndex(previous, current) {
    if (previous.length === 0 || current.length === 0)
        return 0;
    // Try decreasing overlap sizes: full fingerprint down to 1 line
    const maxOverlap = Math.min(previous.length, current.length);
    for (let overlapSize = maxOverlap; overlapSize >= 1; overlapSize--) {
        // Check if the last `overlapSize` lines of previous match
        // the first `overlapSize` lines of current
        const prevSlice = previous.slice(-overlapSize);
        const currSlice = current.slice(0, overlapSize);
        let matches = true;
        for (let i = 0; i < overlapSize; i++) {
            if (prevSlice[i] !== currSlice[i]) {
                matches = false;
                break;
            }
        }
        if (matches) {
            return overlapSize;
        }
    }
    return 0;
}
/**
 * Initialize the TUI accumulator for a polling session.
 *
 * @param pollerKey - Poller key ("worktreeId:cliToolId")
 *
 * @internal Exported for unit testing
 */
function initTuiAccumulator(pollerKey) {
    tuiResponseAccumulator.set(pollerKey, {
        lines: [],
        lastFingerprint: [],
        pollCount: 0,
    });
}
/**
 * Accumulate TUI content from a new capture into the session buffer.
 * Detects overlap with previous capture and appends only new lines.
 *
 * @param pollerKey - Poller key ("worktreeId:cliToolId")
 * @param rawOutput - Raw tmux capture-pane output
 *
 * @internal Exported for unit testing
 */
function accumulateTuiContent(pollerKey, rawOutput) {
    const state = tuiResponseAccumulator.get(pollerKey);
    if (!state)
        return;
    const contentLines = extractTuiContentLines(rawOutput);
    if (contentLines.length === 0)
        return;
    state.pollCount++;
    if (state.lines.length === 0) {
        // First capture: seed with all content
        state.lines = [...contentLines];
    }
    else {
        // Subsequent captures: find overlap and append new lines
        const overlapCount = findOverlapIndex(state.lastFingerprint, contentLines);
        if (overlapCount > 0) {
            // Append only lines after the overlap
            const newLines = contentLines.slice(overlapCount);
            state.lines.push(...newLines);
        }
        else {
            // No overlap found: append all content (completeness over dedup)
            state.lines.push(...contentLines);
        }
    }
    // Update fingerprint for next poll
    state.lastFingerprint = contentLines.slice(-OVERLAP_FINGERPRINT_SIZE);
}
/**
 * Get the accumulated content as a single string.
 *
 * @param pollerKey - Poller key ("worktreeId:cliToolId")
 * @returns Accumulated content joined by newlines, or empty string if no accumulator
 *
 * @internal Exported for unit testing
 */
function getAccumulatedContent(pollerKey) {
    const state = tuiResponseAccumulator.get(pollerKey);
    if (!state || state.lines.length === 0)
        return '';
    return state.lines.join('\n');
}
/**
 * Clear the TUI accumulator for a polling session.
 *
 * @param pollerKey - Poller key ("worktreeId:cliToolId")
 *
 * @internal Exported for unit testing
 */
function clearTuiAccumulator(pollerKey) {
    tuiResponseAccumulator.delete(pollerKey);
}
// ============================================================================
// Poller State Management
// ============================================================================
/**
 * Active pollers map: "worktreeId:cliToolId" -> NodeJS.Timeout
 */
const activePollers = new Map();
/**
 * Polling start times map: "worktreeId:cliToolId" -> timestamp
 */
const pollingStartTimes = new Map();
/**
 * Generate poller key from worktree ID and CLI tool ID
 */
function getPollerKey(worktreeId, cliToolId) {
    return `${worktreeId}:${cliToolId}`;
}
/**
 * Internal helper: detect prompt with CLI-tool-specific options.
 *
 * Centralizes the stripAnsi() + buildDetectPromptOptions() + detectPrompt() pipeline
 * to avoid repeating this 3-step sequence across extractResponse() and checkForResponse().
 *
 * Design notes:
 * - IA-001: stripAnsi() is applied uniformly inside this helper. It is idempotent,
 *   so double-application on already-stripped input is safe.
 * - SF-003: Uses buildDetectPromptOptions() from cli-patterns.ts for tool-specific
 *   configuration (e.g., Claude's requireDefaultIndicator=false for Issue #193).
 *
 * @param output - Raw or pre-stripped tmux output
 * @param cliToolId - CLI tool identifier for building detection options
 * @returns PromptDetectionResult with isPrompt, promptData, and cleanContent
 */
function detectPromptWithOptions(output, cliToolId) {
    const promptOptions = (0, cli_patterns_1.buildDetectPromptOptions)(cliToolId);
    return (0, prompt_detector_1.detectPrompt)((0, cli_patterns_1.stripBoxDrawing)((0, cli_patterns_1.stripAnsi)(output)), promptOptions);
}
/**
 * Clean up Claude response by removing shell setup commands, environment exports, ANSI codes, and banner
 * Also extracts only the LATEST response to avoid including conversation history
 *
 * @param response - Raw Claude response
 * @returns Cleaned response (only the latest response)
 */
function cleanClaudeResponse(response) {
    // First, strip ANSI escape codes
    const cleanedResponse = (0, cli_patterns_1.stripAnsi)(response);
    // Find the LAST user prompt (❯ followed by content) and extract only the response after it
    // This ensures we only get the latest response, not the entire conversation history
    const lines = cleanedResponse.split('\n');
    // Find the last user prompt line index
    let lastUserPromptIndex = -1;
    for (let i = lines.length - 1; i >= 0; i--) {
        // User prompt line: ❯ followed by actual content (not empty ❯)
        if (/^❯\s+\S/.test(lines[i])) {
            lastUserPromptIndex = i;
            break;
        }
    }
    // Extract lines after the last user prompt
    const startIndex = lastUserPromptIndex >= 0 ? lastUserPromptIndex + 1 : 0;
    const responseLines = lines.slice(startIndex);
    // Patterns to remove (Claude-specific setup commands and UI elements)
    // IMPORTANT: These patterns should NOT match legitimate Claude response content
    // Lines starting with ⏺ (Claude output marker) are typically valid content
    const skipPatterns = [
        /CLAUDE_HOOKS_/, // Any CLAUDE_HOOKS reference
        /\/bin\/claude/, // Claude binary path (any variant)
        /^claude\s*$/, // Just "claude" on a line
        /@.*\s+%\s*$/, // Shell prompt (any user@host followed by % at end of line)
        /^[^⏺]*curl.*POST/, // Curl POST commands (not starting with ⏺)
        /^[^⏺]*Content-Type/, // HTTP headers (not in Claude output)
        /^[^⏺]*export\s+CLAUDE_/, // Claude environment exports only
        /^\s*$/, // Empty lines
        // Claude Code banner patterns (only match pure banner elements)
        /^[╭╮╰╯│─\s]+$/, // Box drawing characters only (with spaces)
        /^[│╭╮╰╯].*[│╭╮╰╯]$/, // Lines with box drawing on both sides (banner rows)
        /Claude Code v[\d.]+/, // Version info
        /^Tips for getting started/, // Tips header (at line start)
        /^Welcome back/, // Welcome message (at line start)
        /Run \/init to create/, // Init instruction
        /^Recent activity/, // Activity header (at line start)
        /^No recent activity/, // No activity message (at line start)
        /▐▛███▜▌|▝▜█████▛▘|▘▘ ▝▝/, // ASCII art logo
        /^\s*Opus \d+\.\d+\s*·\s*Claude Max/, // Model info in banner format
        /\.com's Organization/, // Organization info
        /\?\s*for shortcuts\s*$/, // Shortcuts hint at end of line
        /^─{10,}$/, // Separator lines
        /^❯\s*$/, // Empty prompt lines
        cli_patterns_1.PASTED_TEXT_PATTERN, // [Pasted text #N +XX lines] (Issue #212)
    ];
    // Filter out UI elements and keep only the response content
    const cleanedLines = [];
    for (const line of responseLines) {
        const shouldSkip = skipPatterns.some(pattern => pattern.test(line));
        if (!shouldSkip && line.trim()) {
            cleanedLines.push(line);
        }
    }
    // Return cleaned content
    return cleanedLines.join('\n').trim();
}
/**
 * Clean up Gemini response by removing shell prompts and error messages
 *
 * @param response - Raw Gemini response
 * @returns Cleaned response
 */
function cleanGeminiResponse(response) {
    // Split response into lines
    const lines = response.split('\n');
    const cleanedLines = [];
    // Patterns to remove
    const skipPatterns = [
        /^maenokota@.*%/, // Shell prompt
        /^zsh:/, // Shell error messages
        /^feature-issue-\d+/, // Worktree indicator
        /^\s*$/, // Empty lines at start
    ];
    // Find the ✦ marker (actual Gemini response start)
    let foundMarker = false;
    const afterMarker = [];
    for (const line of lines) {
        if (line.includes('✦')) {
            foundMarker = true;
            // Extract content after ✦ marker
            const markerIndex = line.indexOf('✦');
            const afterMarkerContent = line.substring(markerIndex + 1).trim();
            if (afterMarkerContent) {
                afterMarker.push(afterMarkerContent);
            }
            continue;
        }
        if (foundMarker) {
            // Skip shell prompts and errors after ✦ marker
            if (skipPatterns.some(pattern => pattern.test(line))) {
                continue;
            }
            afterMarker.push(line);
        }
    }
    // If we found content after ✦, use only that
    if (afterMarker.length > 0) {
        return afterMarker.join('\n').trim();
    }
    // Otherwise, filter the original response
    for (const line of lines) {
        if (skipPatterns.some(pattern => pattern.test(line))) {
            continue;
        }
        cleanedLines.push(line);
    }
    return cleanedLines.join('\n').trim();
}
/**
 * Check if OpenCode has completed its response.
 * Detects the Build summary line pattern (e.g., "&#x25A3; Build . model . 2.5s").
 * [D2-002] Independent completion detection for OpenCode.
 *
 * Unlike Claude (prompt + separator) or Codex/Gemini (prompt + not thinking),
 * OpenCode signals completion via the Build summary line, which includes
 * the model name and generation timing.
 *
 * @param output - Cleaned tmux output to check (ANSI-stripped)
 * @returns True if OpenCode response is complete
 *
 * @internal Exported for unit testing (response-poller-opencode.test.ts)
 */
function isOpenCodeComplete(output) {
    // Must have a Build completion marker AND must NOT be actively processing.
    // The "esc interrupt" indicator appears in the TUI footer during model processing.
    // Without this check, old Build markers from previous Q&As cause false completions.
    return cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(output) && !cli_patterns_1.OPENCODE_PROCESSING_INDICATOR.test(output);
}
/**
 * Clean OpenCode TUI response by removing decoration characters and status lines,
 * and trimming to only the latest response.
 * [D2-009] Removes box-drawing characters, Build summary, loading indicators,
 * prompt patterns, and processing indicators.
 *
 * Cleaning pipeline:
 * 1. Split response into lines
 * 2. Trim to latest response: find Build markers (▣ Build · model · time)
 *    and discard all content before the second-to-last marker.
 *    OpenCode TUI accumulates conversation history; each Q&A exchange ends
 *    with a Build marker. Without this trimming, savePendingAssistantResponse
 *    and Layer 2 accumulator would include previous Q&As in the response.
 * 3. Skip empty lines
 * 4. Skip lines matching any OPENCODE_SKIP_PATTERNS (TUI artifacts)
 * 5. Skip Build summary line (OPENCODE_RESPONSE_COMPLETE, the completion indicator)
 * 6. Join remaining lines
 *
 * @param response - Raw OpenCode response (may contain TUI decoration)
 * @returns Cleaned response with TUI artifacts removed
 *
 * @internal Exported for unit testing (response-poller-opencode.test.ts)
 */
function cleanOpenCodeResponse(response) {
    const lines = response.split('\n');
    // Step 2: Trim to latest response by finding Build markers.
    // Each Q&A exchange ends with "▣ Build · model · time".
    // If 2+ markers exist, only include content after the second-to-last marker.
    const buildIndices = [];
    for (let i = 0; i < lines.length; i++) {
        const cleanLine = normalizeOpenCodeLine(lines[i]);
        if (cleanLine && cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanLine)) {
            buildIndices.push(i);
        }
    }
    let startLine = 0;
    if (buildIndices.length >= 2) {
        startLine = buildIndices[buildIndices.length - 2] + 1;
    }
    const cleanedLines = [];
    for (let i = startLine; i < lines.length; i++) {
        // Strip ANSI escape codes and TUI border characters before pattern matching.
        // Without this, embedded ANSI codes and heavy borders can break regex matches.
        const cleanLine = normalizeOpenCodeLine(lines[i]);
        if (!cleanLine)
            continue;
        // Skip lines matching any OpenCode skip pattern
        const shouldSkip = cli_patterns_1.OPENCODE_SKIP_PATTERNS.some(pattern => pattern.test(cleanLine));
        if (shouldSkip)
            continue;
        // Skip the Build summary line (completion indicator)
        if (cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanLine))
            continue;
        cleanedLines.push(cleanLine);
    }
    return cleanedLines.join('\n').trim();
}
/**
 * Determine the start index for response extraction based on buffer state.
 * Shared between normal response extraction and prompt detection paths.
 *
 * Implements a 5-branch decision tree for startIndex determination:
 *   1. bufferWasReset  -> findRecentUserPromptIndex(40) + 1, or 0 if not found
 *   2a. cliToolId === 'opencode' -> findRecentUserPromptIndex(totalLines) + 1, or 0
 *   2b. cliToolId === 'codex' -> Math.max(0, lastCapturedLine)
 *   3. lastCapturedLine >= totalLines - 5 (scroll boundary) ->
 *        findRecentUserPromptIndex(50) + 1, or totalLines - 40 if not found
 *   4. Normal case -> Math.max(0, lastCapturedLine)
 *
 * `bufferWasReset` is computed internally from `lastCapturedLine`, `totalLines`,
 * and `bufferReset`. Callers do NOT need to pre-compute `bufferWasReset`.
 * (Design: MF-001 responsibility boundary)
 *
 * Design references:
 * - Issue #326 design policy section 3-2 (4-branch startIndex table)
 * - Stage 4 SF-001: Defensive validation (negative lastCapturedLine clamped to 0)
 * - Stage 1 SF-001: findRecentUserPromptIndex as callback for SRP/testability
 *
 * @param lastCapturedLine - Number of lines previously captured from the tmux buffer.
 *   Negative values are defensively clamped to 0 (Stage 4 SF-001).
 * @param totalLines - Total number of (non-empty-trailing) lines in the current tmux buffer.
 * @param bufferReset - External flag indicating the buffer was reset (e.g., session restart).
 *   Combined with `lastCapturedLine >= totalLines` to derive internal `bufferWasReset`.
 * @param cliToolId - CLI tool identifier. Affects branch 2 (Codex-specific path).
 *   Note: When called from the Claude early prompt detection path (section 3-4),
 *   cliToolId is always 'claude', making the Codex branch unreachable in that context.
 *   The parameter is retained for the function's generality across all call sites.
 * @param findRecentUserPromptIndex - Callback that searches the tmux buffer backwards
 *   for the most recent user prompt line within a given window size.
 *   Returns the line index (>= 0) if found, or -1 if not found.
 * @returns The 0-based line index from which response extraction should begin.
 *
 * @internal Exported for testing only
 */
function resolveExtractionStartIndex(lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex) {
    // Defensive validation: clamp negative values to 0 (Stage 4 SF-001)
    lastCapturedLine = Math.max(0, lastCapturedLine);
    // Branch 2a (highest priority for OpenCode): OpenCode runs in alternate screen mode
    // (fixed-size buffer, no scrollback). lastCapturedLine is meaningless because the buffer
    // doesn't grow — it's always ~PANE_HEIGHT lines. bufferWasReset is often true because
    // lastCapturedLine ≈ totalLines. Must execute BEFORE Branch 1 to avoid Branch 1's small
    // window (40 lines) which fails to find the second-to-last Build marker in a 200-line pane.
    if (cliToolId === 'opencode') {
        const foundUserPrompt = findRecentUserPromptIndex(totalLines);
        return foundUserPrompt >= 0 ? foundUserPrompt + 1 : 0;
    }
    // Compute bufferWasReset internally (MF-001: responsibility boundary)
    const bufferWasReset = lastCapturedLine >= totalLines || bufferReset;
    // Branch 1: Buffer was reset - find the most recent user prompt as anchor
    if (bufferWasReset) {
        const foundUserPrompt = findRecentUserPromptIndex(40);
        return foundUserPrompt >= 0 ? foundUserPrompt + 1 : 0;
    }
    // Branch 2b: Codex uses lastCapturedLine directly (Codex-specific TUI behavior)
    if (cliToolId === 'codex') {
        return Math.max(0, lastCapturedLine);
    }
    // Branch 3: Near scroll boundary - buffer may have scrolled, search for user prompt
    if (lastCapturedLine >= totalLines - 5) {
        const foundUserPrompt = findRecentUserPromptIndex(50);
        return foundUserPrompt >= 0 ? foundUserPrompt + 1 : Math.max(0, totalLines - 40);
    }
    // Branch 4: Normal case - start from lastCapturedLine
    return Math.max(0, lastCapturedLine);
}
/**
 * Extract CLI tool response from tmux output
 * Detects when a CLI tool has completed a response by looking for tool-specific patterns
 *
 * @param output - Full tmux output
 * @param lastCapturedLine - Number of lines previously captured
 * @param cliToolId - CLI tool ID (claude, codex, gemini)
 * @returns Extracted response or null if incomplete
 */
function extractResponse(output, lastCapturedLine, cliToolId) {
    // Trim trailing empty lines from the output before processing
    // This prevents the "last 20 lines" from being all empty due to tmux buffer padding
    const rawLines = output.split('\n');
    let trimmedLength = rawLines.length;
    while (trimmedLength > 0 && rawLines[trimmedLength - 1].trim() === '') {
        trimmedLength--;
    }
    const lines = rawLines.slice(0, trimmedLength);
    const totalLines = lines.length;
    const BUFFER_RESET_TOLERANCE = 25;
    const bufferShrank = totalLines > 0 && lastCapturedLine > BUFFER_RESET_TOLERANCE && (totalLines + BUFFER_RESET_TOLERANCE) < lastCapturedLine;
    const sessionRestarted = totalLines > 0 && lastCapturedLine > 50 && totalLines < 50;
    const bufferReset = bufferShrank || sessionRestarted;
    // No new output (with buffer to handle newline inconsistencies)
    // BUT: if totalLines is much smaller than lastCapturedLine, the buffer was likely reset (session restart)
    // In that case, don't skip - proceed to check for completion
    if (!bufferReset && totalLines < lastCapturedLine - 5) {
        return null;
    }
    // Check recent lines for completion pattern.
    // OpenCode TUI: content area + many empty padding lines + status bar at bottom.
    // The trailing-empty-line trim above removes trailing newlines but NOT the internal
    // padding between content and status bar. For OpenCode, the Build completion marker
    // can be far above the last 20 lines, so we must check the full buffer.
    const checkLineCount = 20;
    const startLine = Math.max(0, totalLines - checkLineCount);
    const linesToCheck = lines.slice(startLine);
    const outputToCheck = cliToolId === 'opencode'
        ? (0, cli_patterns_1.stripAnsi)(lines.join('\n'))
        : linesToCheck.join('\n');
    // Get tool-specific patterns from shared module
    const { promptPattern, separatorPattern, thinkingPattern, skipPatterns } = (0, cli_patterns_1.getCliToolPatterns)(cliToolId);
    const findRecentUserPromptIndex = (windowSize = 60) => {
        // User prompt pattern: supports legacy '>' and new '❯' for Claude
        let userPromptPattern;
        if (cliToolId === 'codex') {
            userPromptPattern = /^›\s+(?!Implement|Find and fix|Type|Summarize)/;
        }
        else if (cliToolId === 'opencode') {
            // OpenCode TUI accumulates conversation history in a single screen.
            // Each Q&A exchange ends with a "▣ Build · model · time" marker.
            // "Ask anything..." does NOT appear in tmux capture content area.
            // Instead, find the SECOND-TO-LAST Build marker (= end of previous exchange)
            // to use as the extraction boundary. The first Build marker found (searching
            // backwards) is the current response's completion; the second is the boundary.
            let buildCount = 0;
            for (let i = totalLines - 1; i >= Math.max(0, totalLines - windowSize); i--) {
                const cleanLine = (0, cli_patterns_1.stripAnsi)(lines[i]);
                if (cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanLine)) {
                    buildCount++;
                    if (buildCount === 2) {
                        return i;
                    }
                }
            }
            return -1;
        }
        else {
            userPromptPattern = /^[>❯]\s+\S/;
        }
        for (let i = totalLines - 1; i >= Math.max(0, totalLines - windowSize); i--) {
            const cleanLine = (0, cli_patterns_1.stripAnsi)(lines[i]);
            if (userPromptPattern.test(cleanLine)) {
                return i;
            }
        }
        return -1;
    };
    // Early check for interactive prompts (before extraction logic)
    // Permission prompts appear after normal responses and need special handling.
    // Issue #372: Codex command confirmation prompts (› 1. Yes, proceed) match
    // CODEX_PROMPT_PATTERN, causing isCodexOrGeminiComplete to fire prematurely.
    // Early detection ensures prompt options are preserved in the extraction result.
    if (cliToolId === 'claude' || cliToolId === 'codex') {
        const fullOutput = lines.join('\n');
        const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
        if (promptDetection.isPrompt) {
            // Prompt detection uses full buffer for accuracy, but return only lastCapturedLine onwards.
            // Issue #372: Carry promptDetection through ExtractionResult so checkForResponse()
            // can use it directly, avoiding a second detection on the (potentially truncated)
            // extracted portion which may miss the › indicator line.
            return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection);
        }
    }
    // Strip ANSI codes before pattern matching
    const cleanOutputToCheck = (0, cli_patterns_1.stripAnsi)(outputToCheck);
    const hasPrompt = promptPattern.test(cleanOutputToCheck);
    const hasSeparator = separatorPattern.test(cleanOutputToCheck);
    const isThinking = thinkingPattern.test(cleanOutputToCheck);
    // Codex/Gemini/Vibe-Local completion logic: prompt detected and not thinking (separator optional)
    // - Codex: Interactive TUI, detects › prompt
    // - Gemini: Interactive REPL, detects > / ❯ prompt
    // - Vibe-Local: Interactive REPL, detects > prompt
    // Claude: require both prompt and separator
    const isCodexOrGeminiComplete = (cliToolId === 'codex' || cliToolId === 'gemini' || cliToolId === 'vibe-local') && hasPrompt && !isThinking;
    const isClaudeComplete = cliToolId === 'claude' && hasPrompt && hasSeparator && !isThinking;
    // [D2-002] OpenCode completion: detected via Build summary line pattern (independent of prompt/separator)
    const isOpenCodeDone = cliToolId === 'opencode' && isOpenCodeComplete(cleanOutputToCheck);
    if (isCodexOrGeminiComplete || isClaudeComplete || isOpenCodeDone) {
        // CLI tool has completed response
        // Extract the response content from lastCapturedLine to the separator (not just last 20 lines)
        const responseLines = [];
        // Determine start index for response extraction using shared helper
        // Handles buffer reset, Codex-specific logic, scroll boundary, and normal cases
        const startIndex = resolveExtractionStartIndex(lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex);
        let endIndex = totalLines; // Track where extraction actually ended
        for (let i = startIndex; i < totalLines; i++) {
            const line = lines[i];
            const cleanLine = (0, cli_patterns_1.stripAnsi)(line);
            // For Codex: stop at any prompt line (which indicates end of response OR we're already past it)
            if (cliToolId === 'codex' && /^›\s+/.test(cleanLine)) {
                endIndex = i;
                break;
            }
            // For Gemini: stop at shell prompt (indicates command completion)
            if (cliToolId === 'gemini' && /^(%|\$|.*@.*[%$#])\s*$/.test(cleanLine)) {
                endIndex = i;
                break;
            }
            // [D2-003] For OpenCode: stop at prompt or status bar patterns
            if (cliToolId === 'opencode') {
                if (cli_patterns_1.OPENCODE_PROMPT_PATTERN.test(cleanLine) || cli_patterns_1.OPENCODE_PROMPT_AFTER_RESPONSE.test(cleanLine)) {
                    endIndex = i;
                    break;
                }
            }
            // Skip lines matching any skip pattern (check against clean line)
            const shouldSkip = skipPatterns.some(pattern => pattern.test(cleanLine));
            if (shouldSkip) {
                continue;
            }
            responseLines.push(line);
        }
        const response = responseLines.join('\n').trim();
        // DR-004: Check only the tail of the response for thinking indicators.
        // Prevents false blocking when completed thinking summaries appear in the response body.
        const responseTailLines = response.split('\n').slice(-RESPONSE_THINKING_TAIL_LINE_COUNT).join('\n');
        if (thinkingPattern.test(responseTailLines)) {
            return incompleteResult(totalLines);
        }
        // CRITICAL FIX: Detect and skip Claude Code startup banner/screen
        // The startup screen contains: ASCII art logo, version info, prompt, separator
        // But no actual response content from the user's query
        if (cliToolId === 'claude') {
            const cleanResponse = (0, cli_patterns_1.stripAnsi)(response);
            // Check for Claude Code banner patterns
            const hasBannerArt = /[╭╮╰╯│]/.test(cleanResponse) || /░{3,}/.test(cleanResponse) || /▓{3,}/.test(cleanResponse);
            const hasVersionInfo = /Claude Code|claude\/|v\d+\.\d+/.test(cleanResponse);
            const hasStartupTips = /Tip:|for shortcuts|\?\s*for help/.test(cleanResponse);
            const hasProjectInit = /^\s*\/Users\/.*$/m.test(cleanResponse) && cleanResponse.split('\n').length < 30;
            // Check if this looks like just a startup screen (no actual response content)
            // A real response would have content AFTER the user's prompt line (> or ❯ message)
            const userPromptMatch = cleanResponse.match(/^[>❯]\s+(\S.*)$/m);
            if (userPromptMatch) {
                // Found user prompt - check if there's actual response content after it
                const userPromptIndex = cleanResponse.indexOf(userPromptMatch[0]);
                const contentAfterPrompt = cleanResponse.substring(userPromptIndex + userPromptMatch[0].length).trim();
                // Filter out just separators, tips, and empty lines
                const contentLines = contentAfterPrompt.split('\n').filter(line => {
                    const trimmed = line.trim();
                    return trimmed &&
                        !skipPatterns.some(p => p.test(trimmed)) &&
                        !/^─+$/.test(trimmed);
                });
                if (contentLines.length === 0) {
                    return incompleteResult(totalLines);
                }
            }
            else if ((hasBannerArt || hasVersionInfo || hasStartupTips || hasProjectInit) && response.length < 2000) {
                // No user prompt found, but has banner characteristics - likely initial startup
                return incompleteResult(totalLines);
            }
        }
        // Gemini-specific check: ensure response contains actual content (✦ marker)
        if (cliToolId === 'gemini') {
            // Check for banner/UI characters (banner should be filtered by skipPatterns, but double-check)
            const bannerCharCount = (response.match(/[░███]/g) || []).length;
            const totalChars = response.length;
            if (bannerCharCount > totalChars * 0.3) {
                return incompleteResult(totalLines);
            }
            if (GEMINI_LOADING_INDICATORS.some(indicator => response.includes(indicator))) {
                return incompleteResult(totalLines);
            }
            if (!response.includes('✦') && response.length < 10) {
                return incompleteResult(totalLines);
            }
        }
        // OpenCode banner defense: initial startup screen should not be treated as a response
        if (cliToolId === 'opencode') {
            const cleanResponse = (0, cli_patterns_1.stripAnsi)(response);
            // If the output is very short and contains only TUI elements, treat as startup banner
            if (cleanResponse.length < 50 || !cli_patterns_1.OPENCODE_RESPONSE_COMPLETE.test(cleanOutputToCheck)) {
                // Check if there's actual content (not just TUI decoration)
                const contentLines = cleanResponse.split('\n').filter(line => {
                    const trimmed = line.trim();
                    return trimmed && !cli_patterns_1.OPENCODE_SKIP_PATTERNS.some(p => p.test(trimmed));
                });
                if (contentLines.length === 0) {
                    return incompleteResult(totalLines);
                }
            }
        }
        return {
            response,
            isComplete: true,
            lineCount: endIndex, // Use endIndex instead of totalLines to track where we actually stopped
            bufferReset,
        };
    }
    // Check if this is an interactive prompt (yes/no or multiple choice)
    // Interactive prompts don't have the ">" prompt and separator, so we need to detect them separately
    // [Issue #379] Skip general prompt detection for OpenCode when response is incomplete.
    // OpenCode's "Ask anything..." prompt pattern can cause false positive prompt detection
    // when combined with user input text visible in the TUI buffer, leading to duplicate
    // message creation. OpenCode prompts are only relevant when completion is detected above.
    if (cliToolId !== 'opencode') {
        const fullOutput = lines.join('\n');
        const promptDetection = detectPromptWithOptions(fullOutput, cliToolId);
        if (promptDetection.isPrompt) {
            // Prompt detection uses full buffer for accuracy, but return only lastCapturedLine onwards
            // stripAnsi is applied inside buildPromptExtractionResult (Stage 4 MF-001: XSS risk mitigation)
            return buildPromptExtractionResult(lines, lastCapturedLine, totalLines, bufferReset, cliToolId, findRecentUserPromptIndex, promptDetection);
        }
    }
    // Not a prompt, but we may have a partial response in progress (even if Claude shows a spinner)
    const responseLines = [];
    const endIndex = totalLines;
    const partialBufferReset = bufferReset || lastCapturedLine >= endIndex - 5;
    const recentPromptIndex = partialBufferReset ? findRecentUserPromptIndex(80) : -1;
    const startIndex = partialBufferReset
        ? (recentPromptIndex >= 0 ? recentPromptIndex + 1 : Math.max(0, endIndex - 80))
        : Math.max(0, lastCapturedLine);
    for (let i = startIndex; i < endIndex; i++) {
        const line = lines[i];
        const cleanLine = (0, cli_patterns_1.stripAnsi)(line);
        // Skip lines matching any skip pattern
        const shouldSkip = skipPatterns.some(pattern => pattern.test(cleanLine));
        if (shouldSkip) {
            continue;
        }
        responseLines.push(line);
    }
    const partialResponse = responseLines.join('\n').trim();
    if (partialResponse) {
        return {
            response: partialResponse,
            isComplete: false,
            lineCount: endIndex,
        };
    }
    // Response not yet complete (or is in thinking state)
    return incompleteResult(totalLines);
}
/**
 * Check for CLI tool response once
 *
 * @param worktreeId - Worktree ID
 * @returns True if response was found and processed
 */
async function checkForResponse(worktreeId, cliToolId) {
    const db = (0, db_instance_1.getDbInstance)();
    try {
        // Get worktree to verify it exists
        const worktree = (0, db_1.getWorktreeById)(db, worktreeId);
        if (!worktree) {
            console.error(`Worktree ${worktreeId} not found, stopping poller`);
            stopPolling(worktreeId, cliToolId);
            return false;
        }
        // Check if CLI tool session is running
        const running = await (0, cli_session_1.isSessionRunning)(worktreeId, cliToolId);
        if (!running) {
            console.log(`[checkForResponse] Session not running for ${worktreeId} (${cliToolId}), stopping poller`);
            stopPolling(worktreeId, cliToolId);
            return false;
        }
        // Get session state (last captured line count)
        const sessionState = (0, db_1.getSessionState)(db, worktreeId, cliToolId);
        const lastCapturedLine = sessionState?.lastCapturedLine || 0;
        // Capture current output
        const output = await (0, cli_session_1.captureSessionOutput)(worktreeId, cliToolId, 10000);
        // Layer 2: Accumulate TUI content for OpenCode (for overlap tracking only).
        if (cliToolId === 'opencode') {
            const pollerKey = getPollerKey(worktreeId, cliToolId);
            accumulateTuiContent(pollerKey, output);
        }
        // Extract response
        const result = extractResponse(output, lastCapturedLine, cliToolId);
        if (!result || !result.isComplete) {
            // No new output or response not yet complete.
            // If CLI tool is actively thinking, mark any pending prompts as answered.
            // This handles cases where user responded to prompts directly via terminal.
            //
            // DR-004 windowing: Only check tail lines (same as extractResponse thinking check)
            // to avoid false matches on completed thinking summaries in scrollback.
            // Previously (MF-001), full-text check caused false positives.
            const { thinkingPattern } = (0, cli_patterns_1.getCliToolPatterns)(cliToolId);
            const cleanOutput = (0, cli_patterns_1.stripAnsi)(output);
            const tailLines = cleanOutput.split('\n').slice(-RESPONSE_THINKING_TAIL_LINE_COUNT).join('\n');
            if (thinkingPattern.test(tailLines)) {
                const answeredCount = (0, db_1.markPendingPromptsAsAnswered)(db, worktreeId, cliToolId);
                if (answeredCount > 0) {
                    console.log(`Marked ${answeredCount} pending prompt(s) as answered (thinking detected) for ${worktreeId}`);
                }
            }
            return false;
        }
        // Issue #379: OpenCode uses a full-screen TUI with fixed buffer size (~200 lines).
        // The tmux pane doesn't grow (no scrollback); each response overwrites the same pane,
        // so lineCount is always approximately equal to lastCapturedLine. Skip line-based
        // duplicate detection entirely for full-screen TUIs.
        const isFullScreenTui = cliToolId === 'opencode';
        // CRITICAL FIX: If lineCount == lastCapturedLine AND there's no in-progress message,
        // this response has already been saved. Skip to prevent duplicates.
        // Issue #372: Skip when buffer reset detected (TUI redraw may coincidentally match lineCount).
        if (!isFullScreenTui && !result.bufferReset && result.lineCount === lastCapturedLine && !sessionState?.inProgressMessageId) {
            return false;
        }
        // Additional duplicate prevention: check if savePendingAssistantResponse
        // already saved this content by comparing line counts.
        // Issue #372: Skip this check when buffer reset is detected (TUI redraw, screen clear).
        // Codex TUI redraws cause totalLines to shrink, making lineCount < lastCapturedLine.
        if (!result.bufferReset && !isFullScreenTui && result.lineCount <= lastCapturedLine) {
            console.log(`[checkForResponse] Already saved up to line ${lastCapturedLine}, skipping (result: ${result.lineCount})`);
            return false;
        }
        // Response is complete! Check if it's a prompt.
        // Issue #372: Prefer the prompt detection carried from extractResponse() early check,
        // which uses the full tmux output for accuracy. The extracted portion (result.response)
        // may be truncated and miss the › indicator line when lastCapturedLine falls just before it.
        const promptDetection = result.promptDetection ?? detectPromptWithOptions(result.response, cliToolId);
        if (promptDetection.isPrompt) {
            // This is a prompt - save as prompt message
            (0, db_1.clearInProgressMessageId)(db, worktreeId, cliToolId);
            const message = (0, db_1.createMessage)(db, {
                worktreeId,
                role: 'assistant',
                // Issue #235: rawContent優先でDB保存 (rawContent contains complete prompt output)
                content: promptDetection.rawContent || promptDetection.cleanContent,
                messageType: 'prompt',
                promptData: promptDetection.promptData,
                timestamp: new Date(),
                cliToolId,
            });
            (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount);
            (0, ws_server_1.broadcastMessage)('message', { worktreeId, message });
            stopPolling(worktreeId, cliToolId);
            return true;
        }
        // Validate response content is not empty
        if (!result.response || result.response.trim() === '') {
            (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount);
            return false;
        }
        // Parse Claude-specific metadata (summary, log filename, request id)
        const claudeMetadata = cliToolId === 'claude'
            ? (0, claude_output_1.parseClaudeOutput)(result.response)
            : undefined;
        // Clean up responses (remove shell prompts, setup commands, and errors)
        // [D2-009] Each tool has its own clean function for tool-specific artifacts
        let cleanedResponse = result.response;
        if (cliToolId === 'gemini') {
            cleanedResponse = cleanGeminiResponse(result.response);
        }
        else if (cliToolId === 'claude') {
            cleanedResponse = cleanClaudeResponse(result.response);
        }
        else if (cliToolId === 'opencode') {
            cleanedResponse = cleanOpenCodeResponse(result.response);
            // Clear accumulator for next response cycle (Layer 2 data not used for final content;
            // accumulatedContent includes all past Q&A history from the fixed-size TUI, causing
            // old responses to leak into the saved message even after cleanOpenCodeResponse trimming).
            const pollerKey = getPollerKey(worktreeId, cliToolId);
            clearTuiAccumulator(pollerKey);
        }
        // If cleaned response is empty or just "[No content]", skip saving
        // This prevents creating messages for shell setup commands that get filtered out
        if (!cleanedResponse || cleanedResponse.trim() === '' || cleanedResponse === '[No content]') {
            (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount);
            (0, db_1.clearInProgressMessageId)(db, worktreeId, cliToolId);
            return false;
        }
        // Create Markdown log file for the conversation pair
        if (cleanedResponse) {
            await (0, conversation_logger_1.recordClaudeConversation)(db, worktreeId, cleanedResponse, cliToolId);
        }
        // Mark any pending prompts as answered since Claude has started processing
        // This handles cases where user responded to prompts directly via terminal
        const answeredCount = (0, db_1.markPendingPromptsAsAnswered)(db, worktreeId, cliToolId);
        if (answeredCount > 0) {
            console.log(`Marked ${answeredCount} pending prompt(s) as answered for ${worktreeId}`);
        }
        // Race condition prevention: re-check session state before saving
        // savePendingAssistantResponse may have already saved this content concurrently
        // Issue #379: Skip for OpenCode full-screen TUI (fixed buffer size, lineCount never advances)
        const currentSessionState = (0, db_1.getSessionState)(db, worktreeId, cliToolId);
        if (!isFullScreenTui && currentSessionState && result.lineCount <= currentSessionState.lastCapturedLine) {
            console.log(`[checkForResponse] Race condition detected, skipping save (result: ${result.lineCount}, current: ${currentSessionState.lastCapturedLine})`);
            return false;
        }
        // Create new CLI tool message in database
        // (No longer using in-progress messages - frontend shows realtime output instead)
        const message = (0, db_1.createMessage)(db, {
            worktreeId,
            role: 'assistant',
            content: cleanedResponse,
            messageType: 'normal',
            timestamp: new Date(),
            cliToolId,
            summary: claudeMetadata?.summary,
            logFileName: claudeMetadata?.logFileName,
            requestId: claudeMetadata?.requestId,
        });
        // Broadcast message to WebSocket clients
        (0, ws_server_1.broadcastMessage)('message', { worktreeId, message });
        // Update session state
        (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount);
        // For full-screen TUIs (OpenCode), stop polling after saving the response.
        // Line-count based duplicate prevention doesn't work because the pane size is fixed,
        // so lineCount never advances. Polling restarts when the user sends the next message.
        if (isFullScreenTui) {
            stopPolling(worktreeId, cliToolId);
        }
        return true;
    }
    catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        console.error(`Error checking for response (${worktreeId}):`, errorMessage);
        return false;
    }
}
/**
 * Start polling for CLI tool response
 *
 * @param worktreeId - Worktree ID
 * @param cliToolId - CLI tool ID (claude, codex, gemini)
 *
 * @example
 * ```typescript
 * startPolling('feature-foo', 'claude');
 * ```
 */
function startPolling(worktreeId, cliToolId) {
    const pollerKey = getPollerKey(worktreeId, cliToolId);
    // Stop existing poller if any
    stopPolling(worktreeId, cliToolId);
    // Record start time
    pollingStartTimes.set(pollerKey, Date.now());
    // Initialize TUI accumulator for OpenCode (Layer 2 safety net)
    if (cliToolId === 'opencode') {
        initTuiAccumulator(pollerKey);
    }
    // Start polling with setTimeout chain to prevent race conditions
    scheduleNextResponsePoll(worktreeId, cliToolId);
}
/** Schedule next checkForResponse() after current one completes (setTimeout chain) */
function scheduleNextResponsePoll(worktreeId, cliToolId) {
    const pollerKey = getPollerKey(worktreeId, cliToolId);
    const timerId = setTimeout(async () => {
        // Check if max duration exceeded
        const startTime = pollingStartTimes.get(pollerKey);
        if (startTime && Date.now() - startTime > MAX_POLLING_DURATION) {
            stopPolling(worktreeId, cliToolId);
            return;
        }
        // Check for response
        try {
            await checkForResponse(worktreeId, cliToolId);
        }
        catch (error) {
            console.error(`[Poller] Error:`, error);
        }
        // Schedule next poll ONLY after current one completes
        // Guard: only if poller is still active (not stopped during checkForResponse)
        if (activePollers.has(pollerKey)) {
            scheduleNextResponsePoll(worktreeId, cliToolId);
        }
    }, POLLING_INTERVAL);
    activePollers.set(pollerKey, timerId);
}
/**
 * Stop polling for a worktree and CLI tool combination
 *
 * @param worktreeId - Worktree ID
 * @param cliToolId - CLI tool ID (claude, codex, gemini)
 *
 * @example
 * ```typescript
 * stopPolling('feature-foo', 'claude');
 * ```
 */
function stopPolling(worktreeId, cliToolId) {
    const pollerKey = getPollerKey(worktreeId, cliToolId);
    const timerId = activePollers.get(pollerKey);
    if (timerId) {
        clearTimeout(timerId);
        activePollers.delete(pollerKey);
        pollingStartTimes.delete(pollerKey);
    }
    // Clean up TUI accumulator if present
    clearTuiAccumulator(pollerKey);
}
/**
 * Stop all active pollers
 * Used for cleanup on server shutdown
 */
function stopAllPolling() {
    for (const pollerKey of activePollers.keys()) {
        const [worktreeId, cliToolId] = pollerKey.split(':');
        stopPolling(worktreeId, cliToolId);
    }
}
/**
 * Get list of active pollers
 *
 * @returns Array of worktree IDs currently being polled
 */
function getActivePollers() {
    return Array.from(activePollers.keys());
}
