import { jsx as _jsx } from "react/jsx-runtime";
import { PureComponent } from 'react';
import { updateLastInteractionTime } from '../../bootstrap/state.js';
import { logForDebugging } from '../../utils/debug.js';
import { stopCapturingEarlyInput } from '../../utils/earlyInput.js';
import { isEnvTruthy } from '../../utils/envUtils.js';
import { isMouseClicksDisabled } from '../../utils/fullscreen.js';
import { logError } from '../../utils/log.js';
import { EventEmitter } from '../events/emitter.js';
import { InputEvent } from '../events/input-event.js';
import { traceInputLatencyInputEvent, traceInputLatencyParsedKeys, traceInputLatencyStdinChunk } from '../input-latency-trace.js';
import { TerminalFocusEvent } from '../events/terminal-focus-event.js';
import { INITIAL_STATE, parseMultipleKeypresses } from '../parse-keypress.js';
import reconciler from '../reconciler.js';
import { finishSelection, hasSelection, startSelection } from '../selection.js';
import { isXtermJs, setXtversionName, supportsExtendedKeys, usesLowChurnTerminalMode } from '../terminal.js';
import { getTerminalFocused, setTerminalFocused } from '../terminal-focus-state.js';
import { TerminalQuerier, xtversion } from '../terminal-querier.js';
import { DISABLE_KITTY_KEYBOARD, DISABLE_MODIFY_OTHER_KEYS, ENABLE_KITTY_KEYBOARD, ENABLE_MODIFY_OTHER_KEYS, FOCUS_IN, FOCUS_OUT } from '../termio/csi.js';
import { DBP, DFE, DISABLE_MOUSE_TRACKING, EBP, EFE, HIDE_CURSOR, SHOW_CURSOR } from '../termio/dec.js';
import AppContext from './AppContext.js';
import { ClockProvider } from './ClockContext.js';
import CursorDeclarationContext from './CursorDeclarationContext.js';
import ErrorOverview from './ErrorOverview.js';
import StdinContext from './StdinContext.js';
import { TerminalFocusProvider } from './TerminalFocusContext.js';
import { TerminalSizeContext } from './TerminalSizeContext.js';
// Platforms that support Unix-style process suspension (SIGSTOP/SIGCONT)
const SUPPORTS_SUSPEND = process.platform !== 'win32';
// After this many milliseconds of stdin silence, the next chunk triggers
// a terminal mode re-assert (mouse tracking). Catches tmux detach→attach,
// ssh reconnect, and laptop wake — the terminal resets DEC private modes
// but no signal reaches us. 5s is well above normal inter-keystroke gaps
// but short enough that the first scroll after reattach works.
const STDIN_RESUME_GAP_MS = 5000;
// Multi-click detection thresholds. 500ms is the macOS default; a small
// position tolerance allows for trackpad jitter between clicks.
const MULTI_CLICK_TIMEOUT_MS = 500;
const MULTI_CLICK_DISTANCE = 1;
// Root component for all Ink apps
// It renders stdin and stdout contexts, so that children can access them if needed
// It also handles Ctrl+C exiting and cursor visibility
export default class App extends PureComponent {
    static displayName = 'InternalApp';
    static getDerivedStateFromError(error) {
        return {
            error
        };
    }
    state = {
        error: undefined
    };
    // Count how many components enabled raw mode to avoid disabling
    // raw mode until all components don't need it anymore
    rawModeEnabledCount = 0;
    internal_eventEmitter = new EventEmitter();
    keyParseState = INITIAL_STATE;
    // Timer for flushing incomplete escape sequences
    incompleteEscapeTimer = null;
    // Timeout durations for incomplete sequences (ms)
    NORMAL_TIMEOUT = 50; // Short timeout for regular esc sequences
    PASTE_TIMEOUT = 500; // Longer timeout for paste operations
    // Terminal query/response dispatch. Responses arrive on stdin (parsed
    // out by parse-keypress) and are routed to pending promise resolvers.
    querier = new TerminalQuerier(this.props.stdout);
    // Multi-click tracking for double/triple-click text selection. A click
    // within MULTI_CLICK_TIMEOUT_MS and MULTI_CLICK_DISTANCE of the previous
    // click increments clickCount; otherwise it resets to 1.
    lastClickTime = 0;
    lastClickCol = -1;
    lastClickRow = -1;
    clickCount = 0;
    // Deferred hyperlink-open timer — cancelled if a second click arrives
    // within MULTI_CLICK_TIMEOUT_MS (so double-clicking a hyperlink selects
    // the word without also opening the browser). DOM onClick dispatch is
    // NOT deferred — it returns true from onClickAt and skips this timer.
    pendingHyperlinkTimer = null;
    // Last mode-1003 motion position. Terminals already dedupe to cell
    // granularity but this also lets us skip dispatchHover entirely on
    // repeat events (drag-then-release at same cell, etc.).
    lastHoverCol = -1;
    lastHoverRow = -1;
    // Timestamp of last stdin chunk. Used to detect long gaps (tmux attach,
    // ssh reconnect, laptop wake) and trigger terminal mode re-assert.
    // Initialized to now so startup doesn't false-trigger.
    lastStdinTime = Date.now();
    // Determines if TTY is supported on the provided stdin
    isRawModeSupported() {
        return this.props.stdin.isTTY;
    }
    render() {
        return _jsx(TerminalSizeContext.Provider, { value: {
                columns: this.props.terminalColumns,
                rows: this.props.terminalRows
            }, children: _jsx(AppContext.Provider, { value: {
                    exit: this.handleExit
                }, children: _jsx(StdinContext.Provider, { value: {
                        stdin: this.props.stdin,
                        setRawMode: this.handleSetRawMode,
                        isRawModeSupported: this.isRawModeSupported(),
                        internal_exitOnCtrlC: this.props.exitOnCtrlC,
                        internal_eventEmitter: this.internal_eventEmitter,
                        internal_querier: this.querier
                    }, children: _jsx(TerminalFocusProvider, { children: _jsx(ClockProvider, { children: _jsx(CursorDeclarationContext.Provider, { value: this.props.onCursorDeclaration ?? (() => { }), children: this.state.error ? _jsx(ErrorOverview, { error: this.state.error }) : this.props.children }) }) }) }) }) });
    }
    componentDidMount() {
        // In accessibility mode, keep the native cursor visible for screen magnifiers and other tools
        if (this.props.stdout.isTTY && !isEnvTruthy(process.env.OWLCODA_ACCESSIBILITY)) {
            this.props.stdout.write(HIDE_CURSOR);
        }
    }
    componentWillUnmount() {
        if (this.props.stdout.isTTY) {
            this.props.stdout.write(SHOW_CURSOR);
        }
        // Clear any pending timers
        if (this.incompleteEscapeTimer) {
            clearTimeout(this.incompleteEscapeTimer);
            this.incompleteEscapeTimer = null;
        }
        if (this.pendingHyperlinkTimer) {
            clearTimeout(this.pendingHyperlinkTimer);
            this.pendingHyperlinkTimer = null;
        }
        // ignore calling setRawMode on an handle stdin it cannot be called
        if (this.isRawModeSupported()) {
            this.handleSetRawMode(false);
        }
    }
    componentDidCatch(error) {
        this.handleExit(error);
    }
    handleSetRawMode = (isEnabled) => {
        const { stdin } = this.props;
        if (!this.isRawModeSupported()) {
            if (stdin === process.stdin) {
                throw new Error('Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported');
            }
            else {
                throw new Error('Raw mode is not supported on the stdin provided to Ink.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported');
            }
        }
        stdin.setEncoding('utf8');
        if (isEnabled) {
            // Ensure raw mode is enabled only once
            if (this.rawModeEnabledCount === 0) {
                // Stop early input capture right before we add our own readable handler.
                // Both use the same stdin 'readable' + read() pattern, so they can't
                // coexist -- our handler would drain stdin before Ink's can see it.
                // The buffered text is preserved for REPL.tsx via consumeEarlyInput().
                stopCapturingEarlyInput();
                stdin.ref();
                stdin.setRawMode(true);
                stdin.addListener('readable', this.handleReadable);
                // Enable bracketed paste mode
                this.props.stdout.write(EBP);
                // OwlCoda low-churn mode skips optional terminal side channels so
                // plain typing stays on the shortest possible input/render path.
                if (!usesLowChurnTerminalMode()) {
                    this.props.stdout.write(EFE);
                }
                // Enable extended key reporting so ctrl+shift+<letter> is
                // distinguishable from ctrl+<letter>. We write both the kitty stack
                // push (CSI >1u) and xterm modifyOtherKeys level 2 (CSI >4;2m) —
                // terminals honor whichever they implement (tmux only accepts the
                // latter).
                if (supportsExtendedKeys()) {
                    this.props.stdout.write(ENABLE_KITTY_KEYBOARD);
                    this.props.stdout.write(ENABLE_MODIFY_OTHER_KEYS);
                }
                // Probe terminal identity. XTVERSION survives SSH (query/reply goes
                // through the pty), unlike TERM_PROGRAM. Used for wheel-scroll base
                // detection when env vars are absent. Fire-and-forget: the DA1
                // sentinel bounds the round-trip, and if the terminal ignores the
                // query, flush() still resolves and name stays undefined.
                // Deferred to next tick so it fires AFTER the current synchronous
                // init sequence completes — avoids interleaving with alt-screen/mouse
                // tracking enable writes that may happen in the same render cycle.
                if (!usesLowChurnTerminalMode()) {
                    setImmediate(() => {
                        void Promise.all([this.querier.send(xtversion()), this.querier.flush()]).then(([r]) => {
                            if (r) {
                                setXtversionName(r.name);
                                logForDebugging(`XTVERSION: terminal identified as "${r.name}"`);
                            }
                            else {
                                logForDebugging('XTVERSION: no reply (terminal ignored query)');
                            }
                        });
                    });
                }
            }
            this.rawModeEnabledCount++;
            return;
        }
        // Disable raw mode only when no components left that are using it
        if (--this.rawModeEnabledCount === 0) {
            this.props.stdout.write(DISABLE_MODIFY_OTHER_KEYS);
            this.props.stdout.write(DISABLE_KITTY_KEYBOARD);
            // Disable terminal focus reporting (DECSET 1004)
            this.props.stdout.write(DFE);
            // Disable bracketed paste mode
            this.props.stdout.write(DBP);
            stdin.setRawMode(false);
            stdin.removeListener('readable', this.handleReadable);
            stdin.unref();
        }
    };
    // Helper to flush incomplete escape sequences
    flushIncomplete = () => {
        // Clear the timer reference
        this.incompleteEscapeTimer = null;
        // Only proceed if we have incomplete sequences
        if (!this.keyParseState.incomplete)
            return;
        // Fullscreen: if stdin has data waiting, it's almost certainly the
        // continuation of the buffered sequence (e.g. `[<64;74;16M` after a
        // lone ESC). Node's event loop runs the timers phase before the poll
        // phase, so when a heavy render blocks the loop past 50ms, this timer
        // fires before the queued readable event even though the bytes are
        // already buffered. Re-arm instead of flushing: handleReadable will
        // drain stdin next and clear this timer. Prevents both the spurious
        // Escape key and the lost scroll event.
        if (this.props.stdin.readableLength > 0) {
            this.incompleteEscapeTimer = setTimeout(this.flushIncomplete, this.NORMAL_TIMEOUT);
            return;
        }
        // Process incomplete as a flush operation (input=null)
        // This reuses all existing parsing logic
        this.processInput(null);
    };
    // Process input through the parser and handle the results
    processInput = (input, inputSeq = null) => {
        // Parse input using our state machine
        const [keys, newState] = parseMultipleKeypresses(this.keyParseState, input);
        this.keyParseState = newState;
        traceInputLatencyParsedKeys(inputSeq, keys);
        // Process ALL keys in a SINGLE discreteUpdates call to prevent
        // "Maximum update depth exceeded" error when many keys arrive at once
        // (e.g., from paste operations or holding keys rapidly).
        // This batches all state updates from handleInput and all useInput
        // listeners together within one high-priority update context.
        if (keys.length > 0) {
            reconciler.discreteUpdates(processKeysInBatch, this, keys, undefined, undefined);
        }
        // If we have incomplete escape sequences, set a timer to flush them
        if (this.keyParseState.incomplete) {
            // Cancel any existing timer first
            if (this.incompleteEscapeTimer) {
                clearTimeout(this.incompleteEscapeTimer);
            }
            this.incompleteEscapeTimer = setTimeout(this.flushIncomplete, this.keyParseState.mode === 'IN_PASTE' ? this.PASTE_TIMEOUT : this.NORMAL_TIMEOUT);
        }
    };
    handleReadable = () => {
        // Detect long stdin gaps (tmux attach, ssh reconnect, laptop wake).
        // The terminal may have reset DEC private modes; re-assert mouse
        // tracking. Checked before the read loop so one Date.now() covers
        // all chunks in this readable event.
        const now = Date.now();
        if (now - this.lastStdinTime > STDIN_RESUME_GAP_MS) {
            this.props.onStdinResume?.();
        }
        this.lastStdinTime = now;
        try {
            let chunk;
            while ((chunk = this.props.stdin.read()) !== null) {
                // Process the input chunk
                this.processInput(chunk, traceInputLatencyStdinChunk(chunk));
            }
        }
        catch (error) {
            // In Bun, an uncaught throw inside a stream 'readable' handler can
            // permanently wedge the stream: data stays buffered and 'readable'
            // never re-emits. Catching here ensures the stream stays healthy so
            // subsequent keystrokes are still delivered.
            logError(error);
            // Re-attach the listener in case the exception detached it.
            // Bun may remove the listener after an error; without this,
            // the session freezes permanently (stdin reader dead, event loop alive).
            const { stdin } = this.props;
            if (this.rawModeEnabledCount > 0 && !stdin.listeners('readable').includes(this.handleReadable)) {
                logForDebugging('handleReadable: re-attaching stdin readable listener after error recovery', {
                    level: 'warn'
                });
                stdin.addListener('readable', this.handleReadable);
            }
        }
    };
    handleInput = (input) => {
        // Exit on Ctrl+C
        if (input === '\x03' && this.props.exitOnCtrlC) {
            this.handleExit();
        }
        // Note: Ctrl+Z (suspend) is now handled in processKeysInBatch using the
        // parsed key to support both raw (\x1a) and CSI u format from Kitty
        // keyboard protocol terminals (Ghostty, iTerm2, kitty, WezTerm)
    };
    handleExit = (error) => {
        if (this.isRawModeSupported()) {
            this.handleSetRawMode(false);
        }
        this.props.onExit(error);
    };
    handleTerminalFocus = (isFocused) => {
        // setTerminalFocused notifies subscribers: TerminalFocusProvider (context)
        // and Clock (interval speed) — no App setState needed.
        setTerminalFocused(isFocused);
    };
    handleSuspend = () => {
        if (!this.isRawModeSupported()) {
            return;
        }
        // Store the exact raw mode count to restore it properly
        const rawModeCountBeforeSuspend = this.rawModeEnabledCount;
        // Completely disable raw mode before suspending
        while (this.rawModeEnabledCount > 0) {
            this.handleSetRawMode(false);
        }
        // Show cursor, disable focus reporting, and disable mouse tracking
        // before suspending. DISABLE_MOUSE_TRACKING is a no-op if tracking
        // wasn't enabled, so it's safe to emit unconditionally — without
        // it, SGR mouse sequences would appear as garbled text at the
        // shell prompt while suspended.
        if (this.props.stdout.isTTY) {
            this.props.stdout.write(SHOW_CURSOR + DFE + DISABLE_MOUSE_TRACKING);
        }
        // Emit suspend event for the host app to handle. Mostly just has a notification
        this.internal_eventEmitter.emit('suspend');
        // Set up resume handler
        const resumeHandler = () => {
            // Restore raw mode to exact previous state
            for (let i = 0; i < rawModeCountBeforeSuspend; i++) {
                if (this.isRawModeSupported()) {
                    this.handleSetRawMode(true);
                }
            }
            // Hide cursor (unless in accessibility mode) and re-enable focus reporting after resuming
            if (this.props.stdout.isTTY) {
                if (!isEnvTruthy(process.env.OWLCODA_ACCESSIBILITY)) {
                    this.props.stdout.write(HIDE_CURSOR);
                }
                // Re-enable focus reporting to restore terminal state
                if (!usesLowChurnTerminalMode()) {
                    this.props.stdout.write(EFE);
                }
            }
            // Emit resume event for the host app to handle
            this.internal_eventEmitter.emit('resume');
            process.removeListener('SIGCONT', resumeHandler);
        };
        process.on('SIGCONT', resumeHandler);
        process.kill(process.pid, 'SIGSTOP');
    };
}
// Helper to process all keys within a single discrete update context.
// discreteUpdates expects (fn, a, b, c, d) -> fn(a, b, c, d)
function processKeysInBatch(app, items, _unused1, _unused2) {
    // Update interaction time for notification timeout tracking.
    // This is called from the central input handler to avoid having multiple
    // stdin listeners that can cause race conditions and dropped input.
    // Terminal responses (kind: 'response') are automated, not user input.
    // Mode-1003 no-button motion is also excluded — passive cursor drift is
    // not engagement (would suppress idle notifications + defer housekeeping).
    if (items.some(i => i.kind === 'key' || i.kind === 'mouse' && !((i.button & 0x20) !== 0 && (i.button & 0x03) === 3))) {
        updateLastInteractionTime();
    }
    for (const item of items) {
        // Terminal responses (DECRPM, DA1, OSC replies, etc.) are not user
        // input — route them to the querier to resolve pending promises.
        if (item.kind === 'response') {
            app.querier.onResponse(item.response);
            continue;
        }
        // Mouse click/drag events update selection state (fullscreen only).
        // Terminal sends 1-indexed col/row; convert to 0-indexed for the
        // screen buffer. Button bit 0x20 = drag (motion while button held).
        if (item.kind === 'mouse') {
            handleMouseEvent(app, item);
            continue;
        }
        const sequence = item.sequence;
        // Handle terminal focus events (DECSET 1004)
        if (sequence === FOCUS_IN) {
            app.handleTerminalFocus(true);
            const event = new TerminalFocusEvent('terminalfocus');
            app.internal_eventEmitter.emit('terminalfocus', event);
            continue;
        }
        if (sequence === FOCUS_OUT) {
            app.handleTerminalFocus(false);
            // Defensive: if we lost the release event (mouse released outside
            // terminal window — some emulators drop it rather than capturing the
            // pointer), focus-out is the next observable signal that the drag is
            // over. Without this, drag-to-scroll's timer runs until the scroll
            // boundary is hit.
            if (app.props.selection.isDragging) {
                finishSelection(app.props.selection);
                app.props.onSelectionChange();
            }
            const event = new TerminalFocusEvent('terminalblur');
            app.internal_eventEmitter.emit('terminalblur', event);
            continue;
        }
        // Failsafe: if we receive input, the terminal must be focused
        if (!getTerminalFocused()) {
            setTerminalFocused(true);
        }
        // Handle Ctrl+Z (suspend) using parsed key to support both raw (\x1a) and
        // CSI u format (\x1b[122;5u) from Kitty keyboard protocol terminals
        if (item.name === 'z' && item.ctrl && SUPPORTS_SUSPEND) {
            app.handleSuspend();
            continue;
        }
        app.handleInput(sequence);
        const event = new InputEvent(item);
        traceInputLatencyInputEvent({
            input: event.input,
            key: {
                ctrl: event.key.ctrl,
                meta: event.key.meta,
                shift: event.key.shift,
                return: event.key.return,
                backspace: event.key.backspace,
                delete: event.key.delete,
                tab: event.key.tab,
                escape: event.key.escape
            }
        });
        app.internal_eventEmitter.emit('input', event);
        // Also dispatch through the DOM tree so onKeyDown handlers fire.
        app.props.dispatchKeyboardEvent(item);
    }
}
/** Exported for testing. Mutates app.props.selection and click/hover state. */
export function handleMouseEvent(app, m) {
    // Allow disabling click handling while keeping wheel scroll (which goes
    // through the keybinding system as 'wheelup'/'wheeldown', not here).
    if (isMouseClicksDisabled())
        return;
    const sel = app.props.selection;
    // Terminal coords are 1-indexed; screen buffer is 0-indexed
    const col = m.col - 1;
    const row = m.row - 1;
    const baseButton = m.button & 0x03;
    if (m.action === 'press') {
        if ((m.button & 0x20) !== 0 && baseButton === 3) {
            // Mode-1003 motion with no button held. Dispatch hover; skip the
            // rest of this handler (no selection, no click-count side effects).
            // Lost-release recovery: no-button motion while isDragging=true means
            // the release happened outside the terminal window (iTerm2 doesn't
            // capture the pointer past window bounds, so the SGR 'm' never
            // arrives). Finish the selection here so copy-on-select fires. The
            // FOCUS_OUT handler covers the "switched apps" case but not "released
            // past the edge, came back" — and tmux drops focus events unless
            // `focus-events on` is set, so this is the more reliable signal.
            if (sel.isDragging) {
                finishSelection(sel);
                app.props.onSelectionChange();
            }
            if (col === app.lastHoverCol && row === app.lastHoverRow)
                return;
            app.lastHoverCol = col;
            app.lastHoverRow = row;
            app.props.onHoverAt(col, row);
            return;
        }
        if (baseButton !== 0) {
            // Non-left press breaks the multi-click chain.
            app.clickCount = 0;
            return;
        }
        if ((m.button & 0x20) !== 0) {
            // Drag motion: mode-aware extension (char/word/line). onSelectionDrag
            // calls notifySelectionChange internally — no extra onSelectionChange.
            app.props.onSelectionDrag(col, row);
            return;
        }
        // Lost-release fallback for mode-1002-only terminals: a fresh press
        // while isDragging=true means the previous release was dropped (cursor
        // left the window). Finish that selection so copy-on-select fires
        // before startSelection/onMultiClick clobbers it. Mode-1003 terminals
        // hit the no-button-motion recovery above instead, so this is rare.
        if (sel.isDragging) {
            finishSelection(sel);
            app.props.onSelectionChange();
        }
        // Fresh left press. Detect multi-click HERE (not on release) so the
        // word/line highlight appears immediately and a subsequent drag can
        // extend by word/line like native macOS. Previously detected on
        // release, which meant (a) visible latency before the word highlights
        // and (b) double-click+drag fell through to char-mode selection.
        const now = Date.now();
        const nearLast = now - app.lastClickTime < MULTI_CLICK_TIMEOUT_MS && Math.abs(col - app.lastClickCol) <= MULTI_CLICK_DISTANCE && Math.abs(row - app.lastClickRow) <= MULTI_CLICK_DISTANCE;
        app.clickCount = nearLast ? app.clickCount + 1 : 1;
        app.lastClickTime = now;
        app.lastClickCol = col;
        app.lastClickRow = row;
        if (app.clickCount >= 2) {
            // Cancel any pending hyperlink-open from the first click — this is
            // a double-click, not a single-click on a link.
            if (app.pendingHyperlinkTimer) {
                clearTimeout(app.pendingHyperlinkTimer);
                app.pendingHyperlinkTimer = null;
            }
            // Cap at 3 (line select) for quadruple+ clicks.
            const count = app.clickCount === 2 ? 2 : 3;
            app.props.onMultiClick(col, row, count);
            return;
        }
        startSelection(sel, col, row);
        // SGR bit 0x08 = alt (xterm.js wires altKey here, not metaKey — see
        // comment at the hyperlink-open guard below). On macOS xterm.js,
        // receiving alt means macOptionClickForcesSelection is OFF (otherwise
        // xterm.js would have consumed the event for native selection).
        sel.lastPressHadAlt = (m.button & 0x08) !== 0;
        app.props.onSelectionChange();
        return;
    }
    // Release: end the drag even for non-zero button codes. Some terminals
    // encode release with the motion bit or button=3 "no button" (carried
    // over from pre-SGR X10 encoding) — filtering those would orphan
    // isDragging=true and leave drag-to-scroll's timer running until the
    // scroll boundary. Only act on non-left releases when we ARE dragging
    // (so an unrelated middle/right click-release doesn't touch selection).
    if (baseButton !== 0) {
        if (!sel.isDragging)
            return;
        finishSelection(sel);
        app.props.onSelectionChange();
        return;
    }
    finishSelection(sel);
    // NOTE: unlike the old release-based detection we do NOT reset clickCount
    // on release-after-drag. This aligns with NSEvent.clickCount semantics:
    // an intervening drag doesn't break the click chain. Practical upside:
    // trackpad jitter during an intended double-click (press→wobble→release
    // →press) now correctly resolves to word-select instead of breaking to a
    // fresh single click. The nearLast window (500ms, 1 cell) bounds the
    // effect — a deliberate drag past that just starts a fresh chain.
    // A press+release with no drag in char mode is a click: anchor set,
    // focus null → hasSelection false. In word/line mode the press already
    // set anchor+focus (hasSelection true), so release just keeps the
    // highlight. The anchor check guards against an orphaned release (no
    // prior press — e.g. button was held when mouse tracking was enabled).
    if (!hasSelection(sel) && sel.anchor) {
        // Single click: dispatch DOM click immediately (cursor repositioning
        // etc. are latency-sensitive). If no DOM handler consumed it, defer
        // the hyperlink check so a second click can cancel it.
        if (!app.props.onClickAt(col, row)) {
            // Resolve the hyperlink URL synchronously while the screen buffer
            // still reflects what the user clicked — deferring only the
            // browser-open so double-click can cancel it.
            const url = app.props.getHyperlinkAt(col, row);
            // xterm.js (VS Code, Cursor, Windsurf, etc.) has its own OSC 8 link
            // handler that fires on Cmd+click *without consuming the mouse event*
            // (Linkifier._handleMouseUp calls link.activate() but never
            // preventDefault/stopPropagation). The click is also forwarded to the
            // pty as SGR, so both VS Code's terminalLinkManager AND our handler
            // here would open the URL — twice. We can't filter on Cmd: xterm.js
            // drops metaKey before SGR encoding (ICoreMouseEvent has no meta
            // field; the SGR bit we call 'meta' is wired to alt). Let xterm.js
            // own link-opening; Cmd+click is the native UX there anyway.
            // TERM_PROGRAM is the sync fast-path; isXtermJs() is the XTVERSION
            // probe result (catches SSH + non-VS Code embedders like Hyper).
            if (url && process.env.TERM_PROGRAM !== 'vscode' && !isXtermJs()) {
                // Clear any prior pending timer — clicking a second link
                // supersedes the first (only the latest click opens).
                if (app.pendingHyperlinkTimer) {
                    clearTimeout(app.pendingHyperlinkTimer);
                }
                app.pendingHyperlinkTimer = setTimeout((app, url) => {
                    app.pendingHyperlinkTimer = null;
                    app.props.onOpenHyperlink(url);
                }, MULTI_CLICK_TIMEOUT_MS, app, url);
            }
        }
    }
    app.props.onSelectionChange();
}
//# sourceMappingURL=App.js.map