import type { SessionBackend, SpawnOpts } from './types.js';
/** Convert `\n` to `\r\n` while leaving existing `\r\n` alone. Exported for
 *  unit tests; used to normalise tmux capture-pane output before sending it
 *  to xterm.js (which treats bare LF as "down one row, keep column"). */
export declare function normaliseCaptureLineEndings(s: string): string;
/**
 * Compose the web-terminal seed body from a normalised capture-pane snapshot
 * and the pane's cursor position.
 *
 * The receiving xterm replays this body and then resumes the LIVE pipe-pane
 * byte stream. Claude Code (and other Ink TUIs) repaint their bottom block with
 * height-RELATIVE moves (`\x1b[<n>A` + `\r\n`), so the FIRST live redraw assumes
 * the cursor sits exactly where the pane's cursor is. Raw capture-pane output
 * carries no cursor position and ends with a trailing newline — that newline
 * scrolls the receiving grid one row PAST the content (desyncing the viewport
 * from the app's coordinates) and parks the cursor on the bottom row instead of
 * the app's real row. The first relative redraw then lands a row low, and
 * because the CLI tracks position relatively, every subsequent frame stays
 * shifted (the status-line update bleeds into the line below — the bug 申晗
 * reported).
 *
 * Fix: strip the SINGLE trailing line terminator so no extra scroll happens,
 * then restore the cursor with CUP (`\x1b[row;colH`). Strip exactly one `\r\n`,
 * NOT a greedy `(\r\n)+` — capture-pane emits every pane row including trailing
 * BLANK rows below the cursor (Claude's bottom row is usually blank). Greedily
 * stripping would delete those blank rows and shift the whole grid up one row,
 * parking the cursor above the real input line (an upward drift — the same bug,
 * mirrored). CUP is viewport-relative and tmux's `cursor_x`/`cursor_y` are
 * 0-based viewport coordinates, so +1 each lands correctly even when the capture
 * includes full scrollback. Verified against a real 208x62 Claude pane.
 * Exported for tests.
 */
export declare function composeSeedBody(normalisedCapture: string, cursor: {
    x: number;
    y: number;
} | null): string;
export declare class TmuxPipeBackend implements SessionBackend {
    /** Real tmux pane address (e.g. "0:2.0") or botmux session name (bmx-*). */
    private readonly paneTarget;
    private readonly fifoPath;
    private readStream;
    /** Streaming UTF-8 decoder. The fifo read emits raw Buffer chunks at libuv's
     *  64KB highWaterMark boundary, which can fall in the middle of a multi-byte
     *  character (CJK = 3 bytes, box-drawing = 3 bytes, emoji = 4 bytes). Decoding
     *  each chunk independently with `chunk.toString('utf8')` would split that
     *  character into U+FFFD replacement chars on both halves — one wide glyph
     *  becomes 2-3 garbage chars and every following column shifts right, which
     *  is the intermittent "错位" seen in the web terminal during heavy CLI
     *  re-renders (a full redraw is a big burst, far more likely to cross a 64KB
     *  boundary). StringDecoder holds the incomplete trailing bytes and prepends
     *  them to the next chunk, so a character split across reads is reassembled. */
    private readonly decoder;
    private readonly dataCbs;
    /** Bounded tail of the decoded output tmux most recently replicated from the
     *  pane (kept to the last RECENT_OUTPUT_MAX UTF-16 code units, not an exact
     *  byte count — fine for a diagnostic). Crash diagnostic: when a send fails
     *  because the pane vanished, capture-pane can no longer read the (now-gone)
     *  screen — but this text was already received over the pipe and is the
     *  CLI's actual final stdout/stderr (e.g. a gateway/API error) right before
     *  it exited. */
    private recentOutput;
    private static readonly RECENT_OUTPUT_MAX;
    private readonly exitCbs;
    private lifecycleTimer;
    private cols;
    private rows;
    private exited;
    /** Set after pipe-pane subscription is active so kill() knows to cancel it. */
    private pipeAttached;
    private readonly createSession;
    private readonly ownsSession;
    private readonly _isReattach;
    /** Adopt-mode CLI pid. Pane liveness alone is insufficient because the CLI
     *  can exit back to the user's shell while the tmux pane stays alive. */
    private readonly watchCliPid;
    /** Claude Code session JSONL path — set by worker for claude-code sessions so
     *  the claude-code adapter can verify paste+Enter submissions via file growth. */
    claudeJsonlPath?: string;
    /** PID of the spawned Claude Code child — used by the claude-code adapter to
     *  follow Claude's authoritative session id via ~/.claude/sessions/<pid>.json. */
    cliPid?: number;
    /** Working directory the CLI was spawned in — cross-checked against the pid
     *  file's cwd field so a recycled PID can't mislead the resolver. */
    cliCwd?: string;
    /** Whether this backend re-attached to an existing bmx-* tmux session
     *  (rather than creating a new detached one). Mirrors TmuxBackend.isReattach
     *  so the worker can branch on reattach behaviour without a private-cast. */
    get isReattach(): boolean;
    constructor(paneTarget: string, opts?: {
        createSession?: boolean;
        ownsSession?: boolean;
        isReattach?: boolean;
        cliPid?: number;
    });
    /** spawn() sets up the pipe-pane subscription + fifo reader. In managed
     *  mode it first creates a detached bmx-* tmux session that runs the CLI. */
    spawn(bin: string, args: string[], opts: SpawnOpts): void;
    write(data: string): void;
    sendText(text: string): boolean;
    sendSpecialKeys(...keys: string[]): boolean;
    /**
     * Paste text into the pane via load-buffer + paste-buffer.
     * The -p flag asks tmux to wrap the buffer in bracketed-paste markers
     * (\e[200~ … \e[201~) when the application has requested bracketed paste.
     * This is REQUIRED for CoCo/Ink: without the markers the TUI treats the
     * paste as a rapid input burst and swallows the trailing Enter as a soft
     * newline, stranding the message in the input box (it then gets submitted
     * by the *next* paste — the "replies to the previous message" off-by-one).
     * NB: TmuxPipeBackend is the only backend used at runtime (see
     * selectSessionBackend), so this is the path that actually matters.
     */
    pasteText(text: string): void;
    /**
     * Run a tmux write (send-keys / load-buffer / paste-buffer) that must never
     * crash the worker on failure.
     *
     * Background: when the CLI process exits mid-write its tmux session/pane is
     * destroyed, so the very next `tmux send-keys` returns exit 1. The 1s
     * lifecycle watcher hasn't fired yet, so `this.exited` is still false and the
     * guard above doesn't help. Previously execFileSync's synchronous throw
     * propagated through writeInput → flushPending (a fire-and-forget async with
     * no .catch) → unhandledRejection, which killed the entire worker process
     * (and with it every Lark session it served).
     *
     * Classify the failure instead of letting it escape:
     *   - pane GONE  → the CLI exited; convert to a normal onExit so the worker
     *                  tears the session down and tells the user "CLI exited",
     *                  exactly like the lifecycle watcher would have.
     *   - pane ALIVE → a transient tmux hiccup; log and drop the keystroke. The
     *                  claude-code adapter's JSONL retry/verify loop will catch a
     *                  non-submission and surface a submit-failure notice.
     *
     * Either way this method never throws — every send-keys caller (web-terminal
     * keys, TUI input, the typing loop) stays crash-safe without its own guard.
     *
     * Returns true when the write succeeded, false when it was dropped (pane gone
     * or a pane-alive hiccup). Callers that verify submission (runner adapters)
     * read this to report a non-submission; the many fire-and-forget callers
     * (web-terminal keys, copy-mode) just ignore it.
     */
    private guardedSend;
    private exitCopyModeIfNeeded;
    enterCopyMode(): void;
    sendCopyModeCommand(xCommand: string): void;
    resize(cols: number, rows: number): void;
    onData(cb: (data: string) => void): void;
    onExit(cb: (code: number | null, signal: string | null) => void): void;
    kill(): void;
    destroySession(): void;
    getChildPid(): number | null;
    getAttachInfo(): null;
    private startLifecycleWatcher;
    private stopLifecycleWatcher;
    private handlePaneExit;
    private createDetachedSession;
    private applySessionOptions;
    /** Snapshot the full pane history WITH ANSI escapes (`-S - -E -`).
     *
     *  Used by web reattach so a brand-new web client sees the whole prior
     *  conversation. For the screenshot / screen_update fast path use
     *  `captureViewport()` instead — that one only returns the visible pane
     *  and is safe to seed a transient xterm-headless with.
     *
     *  IMPORTANT: tmux capture-pane separates rows with bare `\n`, no `\r`.
     *  xterm.js (and any VT100-compliant emulator) treats a bare LF as
     *  "move down one row, keep column" — every captured line lands further
     *  to the right than the previous one. Normalising every `\n` to `\r\n`
     *  makes the snapshot render correctly. The live pipe-pane stream itself
     *  doesn't need this fix — applications write proper `\r\n`. */
    captureCurrentScreen(): string;
    /** Snapshot ONLY the currently visible pane (no scrollback). Equivalent to
     *  `tmux capture-pane` with no `-S`/`-E` flags, which defaults to the
     *  viewport. This is the right input for a transient xterm-headless seed:
     *  the snapshot row count matches the transient terminal's row count, so
     *  no normal-buffer scroll happens and the rendered screenshot lines up
     *  with what the user is seeing in the web terminal right now. */
    captureViewport(): string;
    private captureWithBounds;
    /** Current pane cursor position (0-based, viewport-relative — matches xterm
     *  CUP semantics). Used to restore the cursor in the web-terminal seed so the
     *  CLI's first height-relative redraw lands on the right row. */
    private getCursorPosition;
    /** Current real tmux pane dimensions. Drives transient-renderer sizing so
     *  the screenshot canvas matches whatever the web client resized the pane
     *  to. Returns null if tmux can't be queried (pane gone, server gone). */
    getPaneSize(): {
        cols: number;
        rows: number;
    } | null;
    /** Cheap probe: is the adopted pane currently in the alternate screen
     *  buffer? Used by captureCurrentScreen to decide whether the snapshot
     *  needs an alt-buffer-enter prefix for correct rendering. */
    private isPaneInAltBuffer;
    /** True if the underlying pane is still addressable in tmux. Cheap check —
     *  used by callers to detect "user closed the pane while we were piping". */
    isPaneAlive(): boolean;
    /** Unknown pid → pane-only liveness. EPERM still means the process exists. */
    private isCliPidAlive;
    private fireExit;
}
//# sourceMappingURL=tmux-pipe-backend.d.ts.map