type SubtitleType = 'voice' | 'selection' | 'agent' | 'post' | 'info';
interface SubtitleShowOptions {
    text: string;
    type: SubtitleType;
    onAccept?: () => void;
    onReject?: () => void;
    onCancel?: () => void;
    onCopy?: () => void;
    hints?: string;
    autoHide?: number;
    /**
     * Maximum characters shown per page before paging kicks in. When `text`
     * exceeds this length the subtitle is split on sentence boundaries
     * (`.` / `。` / `!` / `?` / newlines) into pages and the user advances
     * with Space. The accept / reject callbacks fire on the LAST page —
     * earlier pages just advance. Set to `0` to disable paging entirely
     * (render the whole text in one shot). Default `220` — about three
     * lines on the standard subtitle bar.
     */
    maxCharsPerPage?: number;
    /**
     * When `true`, the subtitle bar refuses every dismissal except the
     * explicit accept / reject gestures (Space / double-tap Space) wired
     * via `onAccept` / `onReject`. The × close button is hidden, Esc is
     * ignored, click-outside is ignored, and any-key dismiss is disabled.
     *
     * Use for cases where the host MUST collect a user signal before the
     * bar can go — the end-of-loop feedback closure is the canonical
     * example. Don't use for normal action subtitles; users expect Esc
     * to work as an escape valve everywhere else.
     *
     * Default `false`.
     */
    persistent?: boolean;
}
/**
 * Options for the multi-choice subtitle (`Subtitle.showChoice(...)`).
 *
 * Renders the question + an ordered list of options. Each option can be
 * picked by clicking, by pressing the digit `1..N`, or — when voice is
 * active — by speaking the option text. Esc cancels.
 *
 * `allowFreeText` (default `true`) appends a trailing free-text input
 * after the options so the user can answer outside the predefined
 * choices. On submit the typed value is delivered to `onChoose` with
 * `index === -1` so the caller can distinguish free-text answers from
 * canonical picks.
 */
interface SubtitleChoiceOptions {
    /** The prompt itself — naturally spoken sentence. */
    question: string;
    /** The selectable choices. 2–4 canonical options + optional "Other" slot. */
    options: string[];
    /** Default `true`. When false, no free-text input is shown. */
    allowFreeText?: boolean;
    /** Placeholder shown in the free-text input. Default localised. */
    freeTextLabel?: string;
    /**
     * Fires when the user makes a pick. `index` is the position in
     * `options` (0-based) for canonical picks, OR `-1` for a free-text
     * answer (in which case `value` is the typed string).
     */
    onChoose: (value: string, index: number) => void;
    /** Esc / backdrop dismiss. */
    onCancel?: () => void;
    /** Auto-hide timeout in ms. Default: stays until interacted with. */
    autoHide?: number;
}

/**
 * Subtitle bar — DOM-rendered, CSS-variable themed.
 * See ../../docs/07-subtitle-ui.md for the full design.
 */

declare class Subtitle {
    private el;
    private indicator;
    private autoHideTimer;
    private currentOpts;
    private delegatedHandler;
    /** Extra teardown for the multi-choice mode (keyboard listener on document). */
    private choiceTeardown;
    /** Click-anywhere / any-key dismiss teardown for info subtitles. */
    private dismissTeardown;
    private locale;
    /** Indicator request that was suppressed because a subtitle was
     *  visible. Materialises when the subtitle hides — that way an
     *  agent loop's 'thinking' indicator gets queued behind the current
     *  subtitle instead of stacking on top of it. */
    private pendingIndicator;
    constructor(opts?: {
        locale?: string;
    });
    setLocale(locale: string): void;
    /** Set by the orchestrator so it can flip GestureManager.hasSuggestion
     *  whenever a subtitle becomes visible / hidden. Without this hookup a
     *  single space tap on a proactive prompt would be a no-op (space-tap
     *  → accept only fires when `hasSuggestion` is true). */
    private onVisibilityChange?;
    setVisibilityListener(fn: (visible: boolean) => void): void;
    /**
     * Pluggable TTS hook. Every subtitle copy in dddk is written to be
     * naturally readable aloud (no debug log lines, no JSON blobs — that
     * is the editorial rule, see project memory). If the host wires up a
     * TTSProvider via `setTTSProvider`, every `subtitle.show()` call also
     * sends the text through it for real-time speech synthesis. The host
     * decides what provider (free Web Speech `speechSynthesis` / OpenAI
     * TTS / Gemini TTS / Azure / ElevenLabs) and how to gate it with a
     * user pref toggle.
     *
     * The provider is called fire-and-forget — failures don't block the
     * subtitle render, they just mean no audio for that line.
     */
    private ttsProvider?;
    setTTSProvider(fn: typeof this.ttsProvider | null): void;
    /**
     * Wire a host-supplied handler for the bar's × close button (and
     * any equivalent "user dismissed without committing" gestures). The
     * orchestrator typically wires this to stop the agent loop + surface
     * a brief "stopped — say something new" feedback subtitle.
     */
    setCloseHandler(fn: (() => void) | null): void;
    /** Multi-page state for a single `show()` call. When the caller's text
     *  exceeds maxCharsPerPage we paginate; Space advances. The final
     *  page's accept callback fires the original onAccept. */
    private pages;
    private pageIdx;
    /** Host-supplied "user explicitly closed the bar" handler (typically
     *  wired to stop the agent + show a brief stopped-feedback subtitle). */
    private closeHandler;
    /** Live text node the streaming appendStreamed() writes into. */
    private streamingTextNode;
    /** Cursor element shown after the live text node (blinking caret). */
    private streamingCursor;
    /** Untranscribed text held back for sentence-boundary TTS. */
    private streamingTtsBuffer;
    /** Raw accumulated streaming text (for replaceStreamed / introspection). */
    private streamingFullText;
    /** Active "pause hint" callbacks — set by applyStreamingPauseHint
     *  when the agent pauses mid-stream. Space invokeAccept fires
     *  accept (advances the agent), double-tap fires reject (stops it).
     *  Both clear the hint and the callbacks. */
    private streamingPauseAccept;
    private streamingPauseReject;
    show(opts: SubtitleShowOptions): void;
    /**
     * Click-anywhere / any-key dismiss handler.
     *
     * Applies to EVERY subtitle — info or action:
     *  - Click OUTSIDE the bar dismisses (info: hide; action: same, no
     *    accept/reject fired so it's just "ignore the prompt").
     *  - Click INSIDE the bar is ignored — that's a button click and the
     *    button's own handler should fire (wireButtons attaches that).
     *  - Any non-modifier key press dismisses (Space / double-Space are
     *    handled by the gesture manager BEFORE this listener fires, so
     *    Accept / Reject still work).
     *
     * 120ms attach delay so the click that OPENED the subtitle (a palette
     * row click etc.) doesn't immediately dismiss it.
     */
    private installInteractionDismiss;
    update(text: string): void;
    /**
     * Append a text delta to the streaming bar. First call materialises
     * the bar in streaming mode. Sentence-boundary TTS is buffered and
     * flushed automatically — see `flushStreamingTts`.
     */
    appendStreamed(delta: string): void;
    /**
     * Wipe the streaming bar's current text and replace with `text`.
     * Use when the host wants a "明確一步一步" refresh between agent
     * steps. Passing an empty string clears the bar but keeps it visible.
     */
    replaceStreamed(text: string): void;
    /**
     * Mark the streaming bar as "settled" — remove the typing cursor and
     * apply standard dismiss + autoHide. After this the bar behaves like
     * any other `show()` info subtitle (any-key / click dismiss).
     */
    finalizeStreamed(opts?: {
        autoHide?: number;
    }): void;
    /**
     * Tear down the streaming bar immediately. Used when the agent run
     * stops mid-stream (cancel / error). Distinct from `hide()` only in
     * that it always resets streaming state — `hide()` is the universal
     * "bar gone" used by every mode.
     */
    clearStreamed(): void;
    isStreaming(): boolean;
    /**
     * Attach a "press space to continue" pause UI to the active
     * streaming bar — without replacing the bar or wiping the text the
     * agent just streamed. The hint sits BELOW the streamed text so the
     * user can finish reading before deciding.
     *
     * Wiring:
     *   - Space tap (invokeAccept) → fires opts.onAccept, clears hint.
     *   - Double-tap Space (invokeReject) → fires opts.onReject, clears
     *     hint. Host typically stops the agent on reject.
     *   - × button → behaves like double-tap (fires reject + close).
     */
    applyStreamingPauseHint(opts: {
        hint: string;
        rejectHint?: string;
        onAccept: () => void;
        onReject: () => void;
    }): void;
    /**
     * Tear down the pause hint and restore the cursor. Called on accept
     * (the agent will continue with new deltas) and on reject. Safe to
     * call when no hint is active.
     */
    clearStreamingPauseHint(): void;
    private defaultPauseRejectHint;
    /** Keep the scrollable inner pinned to the bottom as new text streams
     *  in — but only if the user hasn't scrolled up to re-read. If they
     *  scrolled up, leave them there. */
    private autoScrollStream;
    private ensureStreamingBar;
    /**
     * Send buffered streaming text through TTS at sentence boundaries.
     * `force = true` flushes whatever's left regardless of punctuation
     * (used on finalize / replace).
     *
     * Sentence-end characters covered: `。 . ！ ! ？ ? \n`. To avoid
     * choppy mid-clause reads, we also require either a sentence-end OR
     * buffer length ≥ 80 characters before flushing in non-forced mode.
     */
    private flushStreamingTts;
    /**
     * Multi-choice variant of `show()`. Renders the question + numbered
     * option list inside the subtitle bar. The user picks via click, digit
     * key (1..N), or — if `allowFreeText` is true (default) — by typing
     * into the inline free-text input and pressing Enter.
     *
     * The subtitle bar is replaced wholesale (any current single-line
     * subtitle is dismissed first). Esc invokes `onCancel`.
     *
     * Designed to mirror Claude Code's AskUserQuestion envelope (2–4
     * options + optional free-text fallback), but rendered into the
     * standard subtitle slot so a host that already styles the subtitle
     * gets a consistent look for both yes/no and multi-choice prompts.
     */
    showChoice(opts: SubtitleChoiceOptions): void;
    private installChoiceDismiss;
    hide(): void;
    isVisible(): boolean;
    /**
     * Touch-only tap routing on the subtitle bar itself — single tap calls
     * `invokeAccept()` and double tap calls `invokeReject()`, so phone /
     * tablet users without a physical Space key can advance the streaming
     * pause, accept a confirm gate, etc. by tapping the bar.
     *
     * Mouse clicks pass through unchanged (single-tap-to-accept on a
     * desktop bar would be a footgun — users misclick). We feature-detect
     * `pointerType === 'touch'` rather than viewport size so a laptop in
     * tablet mode also gets the gesture.
     *
     * Taps on the embedded × button bubble up to here too — we ignore
     * them by checking the event target's role.
     */
    private bindTouchTapGestures;
    /**
     * Programmatically trigger the current subtitle's accept/reject/cancel
     * callback. Used by the orchestrator's gesture manager — single space
     * tap → `invokeAccept()`, double tap → `invokeReject()`, Esc →
     * `invokeCancel()`. Without these the subtitle bar would only respond
     * to mouse clicks on its buttons.
     *
     * No-ops when no subtitle is currently shown.
     */
    invokeAccept(): boolean;
    invokeReject(): boolean;
    invokeCancel(): boolean;
    /**
     * Override the "running" indicator label shown immediately after the
     * user taps Space accept / reject. The default is locale-aware
     * ("處理中…" / "Working…") via `setLocale()`. Hosts that want a
     * different word ("Thinking…", "Running automation…") set their own.
     * Pass `null` to fall back to the bundled default.
     */
    private runningLabel;
    setRunningLabel(label: string | null): void;
    private resolveRunningLabel;
    showIndicator(state: 'listening' | 'processing' | 'done', label?: string): void;
    hideIndicator(): void;
    /**
     * Render whichever page of `pages` we're on right now into the bar.
     * When pages is null we just render the original opts.text. Each
     * intermediate page hides the accept-callback presence so wireButtons
     * doesn't double-fire; the final page restores it. We also append a
     * `(n/N · space →)` paging hint so the user knows there's more.
     */
    private renderCurrentPage;
    /**
     * Wrap any bar inner-HTML in a frame that gives us:
     *   1. a non-scrolling × close button pinned to the top-right corner,
     *      always reachable even when long content scrolls
     *   2. a `bar-scroll` inner container so the content can be max-height
     *      capped + scrolled without losing the close button
     *
     * The × button is skipped entirely when the current opts are
     * `persistent: true` — the host has declared the user MUST answer
     * before the bar can go (e.g. end-of-loop feedback).
     */
    private wrapBarShell;
    private closeLabel;
    /**
     * Wire the × close button's click to hide() + fire any host-supplied
     * close handler. Idempotent — safe to call after any render path that
     * uses `wrapBarShell`.
     */
    private wireClose;
    private handleCloseClick;
    private formatPageHint;
    private renderHints;
    private renderButtons;
    /**
     * Single delegated listener (vs three direct addEventListeners). Cheaper
     * to install per show() and means we never accumulate dangling listeners
     * if innerHTML is replaced mid-life by update() variants.
     */
    private wireButtons;
    /**
     * Wire keyboard + click for the multi-choice bar.
     *  - 1..N digit keys pick the corresponding option and call onChoose.
     *  - Click on any `[data-dddk-action="choose"]` row picks that option.
     *  - When free-text is allowed, Enter inside the input submits the
     *    typed value with `index === -1`.
     *
     * The keyboard listener is installed on `document` (capture phase) so
     * digit keys are caught even when focus is elsewhere. We remove it on
     * `hide()` via the existing delegatedHandler cleanup path.
     */
    private wireChoice;
}

export { Subtitle };
