import { type SpecPtyEvent } from './adapter.js';
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
import { type TraceEntry } from './evaluator.js';
import { type TransitionEval } from './fsm-evaluator.js';
import { chunkPreservingSurrogates as chunkPreservingSurrogatesShared } from '../../cli-adapters/pty-write-chunking.js';
export type DashboardEvent = {
    kind: 'pty_data';
    chunk: string;
} | {
    kind: 'state_changed';
    state: {
        id: string;
        label: string;
        title: string | null;
        status: 'idle' | 'generating' | 'approval';
    };
    modal: {
        title: string | null;
        buttons: {
            index: number;
            label: string;
        }[];
        kind: 'approval' | 'picker' | 'confirm' | null;
    } | null;
    controls: {
        id: string;
        label: string;
        action_type: string;
    }[];
} | {
    kind: 'notification';
    id: string;
    title: string;
    body: string;
} | {
    kind: 'delegate';
    id: string;
    task: string;
} | {
    kind: 'spec_trace';
    entries: TraceEntry[];
} | {
    kind: 'exit';
    exit_code: number;
} | {
    kind: 'spec_error';
    errors: string[];
};
export type DashboardCommand = {
    kind: 'send_message';
    text: string;
} | {
    kind: 'pty_write';
    data: string;
} | {
    kind: 'click_control';
    control_id: string;
    payload?: unknown;
} | {
    kind: 'click_modal_button';
    index: number;
} | {
    kind: 'attach_image';
    blob: string;
    mime: string;
} | {
    kind: 'resize';
    cols: number;
    rows: number;
} | {
    kind: 'cancel';
} | {
    kind: 'shutdown';
};
export interface DriverHistoryEntry {
    stateId: string;
    label: string;
    at: number;
    durationMs: number;
    reason: string;
    matchedStateId?: string;
    matchedRules?: string[];
    debounceKind?: string;
    idleHoldMs?: number;
    busyHoldMs?: number;
    via?: string;
}
/**
 * A frozen snapshot of the FULL FSM evaluation captured at the instant a
 * transition fired — the rich `transitions[]` table (per-transition eligible /
 * hold countdown / per-condition CondResult + remainingMs) that `getFsmDebug()`
 * otherwise only computes live for the current instant. Kept in a separate ring
 * buffer from `stateHistory` (which stays intentionally lightweight) so the
 * "why did this rule fire just before the transition" question is answerable
 * after the fact. before-only: this is the evaluation that PRODUCED the
 * transition, not the post-transition state.
 */
export interface FsmSnapshotEntry {
    /** State we transitioned out of. */
    stateFrom: string;
    /** State we transitioned into (the fired transition's destination). */
    stateTo: string;
    /** Wall-clock time the transition committed (ms). */
    at: number;
    /** The fired transition's destination state id (== stateTo; kept explicit
     *  to mirror the rule that fired). */
    firedTo: string;
    /** Human label of the fired transition (e.g. "approval→busy"). */
    firedLabel: string;
    /** Why-it-fired summary, same shape produced for stateHistory.matchedRules. */
    reason: string[];
    /** Every outgoing transition from `stateFrom` as evaluated at `at`, each
     *  with its eligible / hold / per-condition CondResult + remainingMs. This
     *  is the full pre-transition evaluation table — the whole point of the
     *  snapshot. */
    transitions: TransitionEval[];
}
export interface ISpecDriver {
    subscribe(listener: (ev: DashboardEvent) => void): () => void;
    start(): void;
    dispatch(cmd: DashboardCommand): void;
    /**
     * BUTTON-INDEX-MISMAP (Fix C.3): click a modal button by its FSM display index and report
     * whether a button was actually matched and its confirm keys dispatched. Unlike the
     * fire-and-forget `dispatch('click_modal_button')`, callers that need to know the click
     * landed (mesh_approve → SpecCliAdapter.resolveModalMatched) can observe a miss.
     */
    clickModalButton(index: number): boolean;
    updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
    snapshot(): string;
    getCursorPosition(): {
        row: number;
        col: number;
    };
    getScreen(): string;
    /** Current terminal geometry (columns × rows). Optional so a non-Fsm
     *  ISpecDriver implementation (test doubles) need not provide it; the
     *  mesh_read_terminal path falls back to a 0×0 geometry when absent. */
    getScreenSize?(): {
        cols: number;
        rows: number;
    };
    getSpecPath(): string;
    shutdown(): void;
    getStateHistory(): ReadonlyArray<DriverHistoryEntry>;
    getSections(): Array<{
        id: string;
        text: string;
    }> | null;
    getLastBusyAt(): number;
    hasIdleHoldPending(): boolean;
    hasSeenReady(): boolean;
    getCompletionIdleDebounceState(): {
        active: boolean;
        ageMs: number;
        holdMs: number;
        forceAfterMs: number;
    } | null;
    getFsmDebug?(): unknown;
    getFsmSnapshotHistory?(): ReadonlyArray<FsmSnapshotEntry>;
    getEventTimeline?(limit?: number): ReadonlyArray<SpecPtyEvent>;
}
export interface SpecDriverOpts {
    specPath: string;
    workingDir: string;
    extraEnv?: Record<string, string>;
    cols?: number;
    rows?: number;
    hotReload?: boolean;
    emitTrace?: boolean;
    transportFactory?: PtyTransportFactory;
    extraCliArgs?: string[];
}
export type Win32SubmitMode = 'paste' | 'soft_newline';
export declare function resolveWin32SubmitMode(env?: NodeJS.ProcessEnv): Win32SubmitMode;
export declare function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number;
/** Re-export of the shared surrogate-safe splitter so existing imports of
 *  `chunkPreservingSurrogates` from this module keep working. The implementation
 *  lives in ../../cli-adapters/pty-write-chunking so the spec driver and the
 *  legacy adapter share one definition. */
export declare const chunkPreservingSurrogates: typeof chunkPreservingSurrogatesShared;
export declare function guessExt(mime: string): string;
type HistoryEntry = DriverHistoryEntry;
export declare class FsmDriver implements ISpecDriver {
    private readonly opts;
    private spec;
    private adapter;
    private listeners;
    private currentStateId;
    private stateEnteredAt;
    private startedAtMs;
    private prevScreenLines;
    /** Per stable region → last time that region's content changed. Drives
     *  stable_ms conditions. Key = stableRegionKey(cond): numeric cursor_above
     *  (−1 = whole screen), or a `section:<id>` / `<region>#ignore:<pat>` string
     *  when the clause scopes to a section or declares an ignore_lines filter. */
    private regionLastChangedAt;
    /** COMPLETION-EARLYNOTIFY stable-eval trace: last stable/not-stable verdict
     *  recorded per stable region, so the trace fires only when the verdict FLIPS
     *  (not every quiet frame). Cleared on every transition alongside
     *  regionLastChangedAt. Diagnostic-only — never consulted by the FSM. */
    private stableVerdictCache;
    /** Timer that re-runs evaluate() when a time-condition would flip true
     *  with no PTY frame to trigger it. */
    private wakeTimer;
    /** Timer driving the focus-gated stall watchdog (refocus_when_stalled_ms).
     *  Re-arms itself while the machine is generating so a re-prime can fire
     *  even when the PTY has gone completely quiet. */
    private stallTimer;
    /** Wall-clock time the last stall re-prime was injected. The cooldown gate:
     *  after a re-prime we don't re-inject until the screen changes (which
     *  resets the stall reference) or another full stall window lapses. */
    private lastRefocusAt;
    /** Timer driving the win32 verification-based submit resend loop (see
     *  WIN32_SUBMIT_* and scheduleWin32Submit). Re-arms itself until the FSM
     *  leaves idle (submitted) or the resend budget is spent. */
    private win32SubmitTimer;
    /** Wall-clock (ms) of the most recent raw PTY output chunk. Advances on every
     *  on_pty_data — including the echo of text written into the composer — so the
     *  win32 submit settle-gate can tell when input has finished landing. */
    private lastPtyDataAt;
    /** Wall-clock (ms) of the most recent win32 message-body input write. Bridges
     *  the gap between writing a chunk and its echo so the settle-gate does not
     *  declare "quiet" mid-write. */
    private lastWin32WriteAt;
    /** Pending paced chunk-write timer for a large win32 body (see writeWin32Body). */
    private win32WriteTimer;
    /** Timer driving the win32 verification-based modal-confirm CR resend loop (see
     *  scheduleWin32ModalConfirm). A lone CR that confirms an approval/picker choice
     *  is absorbed by ConPTY the same way a send_message submit CR is, so the confirm
     *  must be resent until the modal actually resolves (status leaves 'approval'). */
    private win32ModalConfirmTimer;
    private currentEval;
    private stateHistory;
    private prevStateAt;
    private readySeenOnce;
    private pendingSends;
    private pickerInProgress;
    private delegateTimers;
    private specWatcher;
    /** Last full FSM evaluation, kept for the debugger. */
    private lastFsmEval;
    /** Ring buffer (max 20) of the full FSM evaluation captured at each
     *  transition — the rich pre-transition table that lastFsmEval only keeps
     *  for the single most recent evaluation. Separate from stateHistory. */
    private fsmSnapshotHistory;
    constructor(opts: SpecDriverOpts);
    subscribe(listener: (ev: DashboardEvent) => void): () => void;
    start(): void;
    private scheduleSpawnPrime;
    /** Write the declared `send_on_spawn` sequences to the PTY once. Used both
     *  at spawn (scheduleSpawnPrime) and, for focus-gated TUIs, by the stall
     *  watchdog to re-inject the focus-in wake mid-turn. No-op when the spec
     *  declares no prime, so non-focus-gated CLIs are never poked. */
    private sendSpawnPrime;
    dispatch(cmd: DashboardCommand): void;
    /** Forward runtime metadata to the terminal transport so mesh binding
     *  fields (meshNodeId / meshNodeFor / workspaceLabel / lifecycle) reach
     *  the session registry. Not a DashboardCommand — this is a control-plane
     *  update, not user input. */
    updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
    snapshot(): string;
    getCursorPosition(): {
        row: number;
        col: number;
    };
    getScreen(): string;
    getScreenSize(): {
        cols: number;
        rows: number;
    };
    /** Scrollback-inclusive screen as line array — used only for modal/button
     *  content extraction so a tall prompt's off-screen anchors stay matchable.
     *  Falls back to the viewport snapshot if scrollback read is unavailable. */
    private scrollbackLines;
    getSpecPath(): string;
    shutdown(): void;
    getFsmDebug(): {
        currentState: string;
        label: string;
        stateAgeMs: number;
        status: string;
        cursor: {
            row: number;
            col: number;
        };
        transitions: TransitionEval[];
    };
    getStateHistory(): ReadonlyArray<HistoryEntry>;
    getFsmSnapshotHistory(): ReadonlyArray<FsmSnapshotEntry>;
    /** Debug-only PTY input/output/resize/cursor timeline from the adapter. */
    getEventTimeline(limit?: number): ReadonlyArray<SpecPtyEvent>;
    getSections(): Array<{
        id: string;
        text: string;
    }> | null;
    /** v3-compat shims so the cli-adapter's existing debug snapshot keeps
     *  working without branching on driver type. */
    getLastBusyAt(): number;
    hasIdleHoldPending(): boolean;
    /**
     * True once the machine has reached its first non-initial idle state (the
     * prompt is genuinely drawn — see maybeMarkReady). The cli-adapter surfaces
     * this on its idle status so CliProviderInstance can re-arm the queue-claim
     * agent:ready on the first genuine ready, independent of the boot-time
     * starting→idle one-shot (which is consumed too early for specs whose
     * initial state already reports idle).
     */
    hasSeenReady(): boolean;
    getCompletionIdleDebounceState(): {
        active: boolean;
        ageMs: number;
        holdMs: number;
        forceAfterMs: number;
    } | null;
    private loadSpecOrThrow;
    private buildAdapterOpts;
    private armSpecWatcher;
    private emitInitialState;
    private buildClock;
    private evalFsmNow;
    private reevaluate;
    private commitTransition;
    /** Snapshot the full FSM evaluation that produced a transition into the
     *  separate fsmSnapshotHistory ring buffer (max 20). The transitions[]
     *  table is captured by reference — it is freshly built per evaluation in
     *  evaluateFsm and never mutated after, so no clone is needed. */
    private pushFsmSnapshot;
    /** Re-derive the visible modal + controls for the current state and emit a
     *  state_changed if anything differs from the last emit. */
    private emitStateChanged;
    private deriveModal;
    private deriveTitle;
    private deriveControls;
    /** Track which stable regions changed since the previous frame so
     *  stable_ms conditions can measure quiet time. We record every distinct
     *  stable region referenced in the current state (numeric cursor_above /
     *  whole-screen -1, and named `section:<id>` regions) plus, for each, the
     *  optional `ignore_lines` filter that folds into its key.
     *
     *  `ignore_lines` is the content-aware fix for the busy→idle wedge: lines
     *  matching it are stripped from BOTH frames before the comparison, so a
     *  benign residual ticker (bare token counter / elapsed timer that repaints
     *  every frame post-generation) no longer resets the clock — while an active
     *  spinner line, which does NOT match the benign pattern, still does (the
     *  FALSEIDLE2 / FALSEBUSY-B whole-screen invariant is preserved). */
    private trackRegionChanges;
    /** Every distinct stable-region descriptor referenced by stable_ms
     *  conditions in the current state's outgoing transitions, plus the plain
     *  whole-screen key (-1) that other machinery (stall watchdog) reads.
     *  De-duplicated by key. Cached lazily per spec load would be nicer but the
     *  set is tiny. */
    private stableRegionDescriptors;
    /** Schedule a re-evaluation for the soonest pending time-condition on any
     *  outgoing transition (elapsed_ms / stable_ms / min_hold_ms). Without
     *  this, a state whose only exit is time-based would never leave once the
     *  PTY goes quiet. */
    private scheduleWakeForState;
    /** True when this spec opts into stall recovery (declares a positive
     *  refocus window AND a wake sequence to re-inject). */
    private stallRecoveryEnabled;
    /** Wall-clock time the screen last changed in the current state, falling
     *  back to state entry when it has not changed since (i.e. fully stalled
     *  from the start of the state). */
    private lastScreenChangeAt;
    /** Arm a timer to re-inject the focus-in prime if the screen stays frozen
     *  through a `generating` state. Only active for opted-in focus-gated specs;
     *  a no-op (and cleared) for every other CLI and every non-generating state.
     *  Re-arms itself so it keeps watching while the PTY is quiet. */
    private scheduleStallWatchdog;
    /** Fire when the stall window elapses: if the screen is still frozen and we
     *  are still generating, re-inject the focus-in wake once, then re-arm. */
    private onStallTick;
    private maybeMarkReady;
    private fireNotifications;
    private armOrCancelDelegateTimers;
    private fireDelegate;
    private handleSendMessage;
    private actuallySendMessage;
    /** The agent's current coarse status, derived from the FSM node we're in. */
    private currentStatus;
    /** Record a win32 body write so the settle-gate counts it as input activity
     *  even before the echo arrives. */
    private markWin32Write;
    /** Most recent win32 input activity — a write we issued OR a PTY output chunk
     *  (echo). The submit settle-gate waits for this to go quiet. */
    private lastWin32InputActivityAt;
    /**
     * Write the message body to the PTY for win32, paced into bounded chunks. A
     * single unbounded ConPTY write can overflow the input pipe and drop leading
     * bytes; splitting it with a short inter-chunk gap keeps the console input
     * buffer from overflowing. Small bodies still go out in a single write. Each
     * write advances lastWin32WriteAt so the submit settle-gate keeps waiting until
     * the final segment is out and echoed.
     *
     * FIX-B-v2: a body that contains an embedded newline cannot be written raw —
     * on the real win32 Ink/ConPTY composer each '\n' SUBMITS the preceding line as
     * its own entry, truncating the prompt to only the tail fragment. So a
     * newline-bearing body is rewritten so its embedded newlines never submit:
     *   - 'paste' (default): wrap the body in a bracketed-paste (ESC[200~ … ESC[201~)
     *     — the composer takes the whole thing, newlines and all, as pasted text.
     *   - 'soft_newline': replace each embedded newline with a non-submitting
     *     Shift+Enter (CSI-u) so the body is typed as one multi-line entry.
     * The trailing submit CR is NOT written here — it stays separate and is fired
     * later by scheduleWin32Submit (concern (A)). The bracketed-paste markers are
     * written as their own atomic segments (never chunked), so chunking can never
     * split ESC[200~ / ESC[201~ mid-sequence regardless of body length.
     */
    private writeWin32Body;
    /**
     * win32 submit. Two phases:
     *
     *  Phase 1 (echo-gate): hold the first CR until the body text is CONFIRMED in the
     *  composer (its whitespace-collapsed tail appears in the rendered screen) AND the
     *  PTY output is then quiet for WIN32_SUBMIT_SETTLE_MS (the full, possibly multi-KB
     *  / multiline body has finished arriving). This replaces a bare output-quiet
     *  settle: an early write that races claude's boot is buffered, not dropped, so the
     *  screen can go quiet with the body NOT YET in the composer — a quiet-only gate
     *  would then fire a CR into an empty composer (no submit). Waiting on the echo
     *  closes that. Honors an initial minimum delay and a generous WIN32_ECHO_MAX_WAIT_MS
     *  last-resort blind fire so a body that truly never confirms still submits (carried
     *  by phase 2) rather than hanging. A settled session echoes immediately → no delay.
     *
     *  Phase 2 (verified resend — unchanged): send the submit key, wait a gap, and
     *  if the FSM is still 'idle' (the CR was absorbed as a multiline-paste
     *  newline) resend, up to WIN32_SUBMIT_MAX_RESENDS. The first CR always fires
     *  (a stale/edge status never suppresses it); resends are gated on still being
     *  idle and stop the instant the agent leaves idle (submitted → generating /
     *  approval). This preserves the win32 lone-CR-swallow handling.
     */
    private scheduleWin32Submit;
    private handleClickControl;
    /**
     * BUTTON-INDEX-MISMAP (Fix C.3): public modal-click entry that returns whether a button
     * matching the requested FSM display index was actually found and its confirm keys were
     * dispatched. The old private handleClickModalButton silently `return`ed on a miss (no
     * modal captured, or no button whose `.index` equals the requested display index), so a
     * mis-mapped index looked identical to a successful press. Callers that need to know
     * whether the click landed (mesh_approve → resolveModal) can now observe the miss instead
     * of reporting success into the void. The generic `dispatch('click_modal_button')` path
     * keeps ignoring the return (fire-and-forget UI clicks).
     */
    clickModalButton(index: number): boolean;
    private handleClickModalButton;
    /**
     * Submit a modal-confirm key sequence (the choice key + its trailing CR).
     *
     * On win32 the trailing CR is the SAME lone-CR-swallow case as a send_message
     * submit: ConPTY can absorb a single CR as a literal newline instead of a
     * confirm, so the approval/picker modal never resolves and the FSM flaps
     * approval↔busy while auto-approve keeps firing into the void (APPROVESTUCK).
     * So we split any non-CR prefix (e.g. the "1" of "1\r") off, write it once, and
     * resend the CR on a fixed cadence until the modal actually resolves (status
     * leaves 'approval'). Non-win32 keeps the single direct write — its CR submits
     * on the first try.
     */
    private submitModalConfirm;
    /**
     * win32 modal-confirm CR resend loop. Mirrors scheduleWin32Submit's phase-2
     * verified resend, but gated on still being IN a modal (status 'approval')
     * rather than still idle: the first CR fires immediately, then resends every
     * WIN32_SUBMIT_RESEND_GAP_MS while the FSM is still showing the modal, up to
     * WIN32_SUBMIT_MAX_RESENDS. The instant the modal resolves (status flips to
     * generating/idle) we stop, so no stray CR leaks into the next turn's composer.
     */
    private scheduleWin32ModalConfirm;
    private handleAttachImage;
    private tryAdvancePicker;
    private handleExit;
    private pushHistory;
    private specTag;
    private emit;
}
/** Drop lines matching `ignoreRe` so a per-frame repaint confined to them does
 *  not register as a region change. No filter → lines returned unchanged.
 *  Exported for unit tests of the stable_ms `ignore_lines` change-detection. */
export declare function filterIgnoredLines(lines: string[], ignoreRe: RegExp | undefined): string[];
export {};
