import { type ModuleSnapshot } from "../modules/runtime.js";
import { type ServerResponse } from "node:http";
import { type Json, type Provider, type Target } from "./protocol.js";
import { ToolChanges } from "./changes.js";
import { ApprovalPushService } from "./push.js";
import { type RequestKind } from "./approval-summary.js";
import { type DialogAnswer, type DialogQuestion } from "./claude-question-dialog.js";
import { type TerminalChoice } from "./terminal-choice.js";
import { ApprovalWatchLeases } from "./agent-hook-stores.js";
import { SudoBroker } from "./sudo.js";
import type { AppServerRequestId, PendingServerRequest } from "./codex-app-server.js";
import { PaneServerWatcher, type PaneAsks } from "./opencode-panes.js";
import type { PaneClient, PaneServerEntry } from "./opencode-pane-server.js";
export { permissionPrompt, terminalChoice, visibleTerminalChoice, type TerminalChoice, type TerminalQuestion } from "./terminal-choice.js";
export { ApprovalWatchLeases, PushBindingStore, recordedSession } from "./agent-hook-stores.js";
/** Where a held request's answer goes: the callback's HTTP response, or the
 * reply to a Codex app-server request (codex-servers.ts). Either takes the
 * PermissionRequest-shaped JSON; "{}" gives the request back to the terminal. */
/** Whether the caller hung up on `res` before it was answered. Not
 * `req.destroyed`: a request body read to its end is destroyed on every
 * Node this supports, so that read as a hang-up on each normal callback. */
export declare function watchHangUp(res: ServerResponse): () => boolean;
interface HeldReply {
    end(body: string): unknown;
    readonly destroyed: boolean;
}
interface Pending {
    target: Target;
    response: HeldReply;
    tool: string;
    input: unknown;
    message: string;
    request: string;
    requestKind: RequestKind;
    title?: string;
    choice?: TerminalChoice;
    expiresAt: string;
    timer?: NodeJS.Timeout;
    conductor?: {
        action: "dispatch" | "hand_off";
        project?: string;
        computer?: string;
    };
    /** A server request of the Hook's own Codex app-server: answered over RPC, never held on a timer. */
    appServer?: {
        requestId: AppServerRequestId;
        answer: (result: Json) => void;
    };
}
/** The approval card's tool and input for an app-server request, in the
 * shapes `approvalSummary` and the phone already read for Codex's hook.
 * Undefined for questions (`item/tool/requestUserInput`) and MCP
 * elicitations, which stay in the pane. */
export declare function appServerApproval(request: PendingServerRequest): {
    tool: string;
    input: Json;
} | undefined;
/** The app-server's answer for the phone's allow or deny (T3's
 * CodexSessionRuntime shapes: accept / decline, a permission grant or none). */
export declare function appServerDecision(request: PendingServerRequest, allow: boolean): Json;
/** What a dispatched worker's Hook tells the dispatching Hook it is waiting on:
 * a card without the request's full message or details, which can run to 32 KB
 * and cross SSH on every poll. */
export interface ForwardedApproval {
    actionId: string;
    tool: string;
    title?: string;
    request?: string;
    requestKind?: RequestKind;
    expiresAt?: string;
    /** The pane is drawing this as a terminal dialog; the answer types its keys. */
    terminal?: true;
    conductor?: Pending["conductor"];
    /** This Hook already pushed the request to a phone, so the dispatching Hook does not push it again. */
    pushed?: true;
}
/** A conductor `dispatch` or `hand_off` permission ask the Hook can answer
 * itself under a standing grant, or offer the phone two grant-writing answers. */
export declare function conductorCall(tool: string, input: unknown): Pending["conductor"] | undefined;
/** The decisions an OpenCode permission ask offers the phone. OpenCode's
 * `always` covers the current session, so both grant answers reply `always`;
 * `allow-everywhere` additionally writes an allow rule into the user's
 * OpenCode config. Advertised as the approval card's `options`, the same
 * shape the phone already renders for a provider-supplied option list. */
export declare const OPENCODE_APPROVAL_OPTIONS: Json[];
/** The OpenCode permission reply a card decision maps to: both grant scopes
 * are `always`, a plain approve is `once`, a deny is `reject`. Undefined for
 * anything else. */
export declare function opencodePermissionReply(decision: unknown): "once" | "always" | "reject" | undefined;
export type DeliveryOutcome = "delivered" | "blocked" | "pending";
/** A phone message typed under its delivery id, so the phone can ask what
 * became of it by that id. `typed` until the Hook answers it queued; the
 * typed record's own settlement moves it on by id, never by its words. */
interface TrackedDelivery {
    source: Provider;
    session: string;
    state: "typed" | "queued" | "delivered" | "blocked";
    at: number;
}
export type TrackedState = Exclude<TrackedDelivery["state"], "typed"> | "unknown";
export declare class AgentHooks {
    readonly push: ApprovalPushService;
    private modules?;
    readonly changes: ToolChanges;
    private pending;
    /** Prompts Phren has typed into a pane, by a hash of their words, until the agent that
     * actually receives one reports in through UserPromptSubmit. Herdr writes
     * to a pane, not a conversation; the receiving agent's hook is the only
     * party that knows which conversation consumed the text, so it is the one
     * that can refuse it when that is not the conversation the phone meant. */
    private deliveries;
    /** Phone messages by delivery id, until TRACK_MS: see `queueDelivery`. */
    private tracked;
    /** The permission request a conversation is drawing in its own terminal
     * because nobody was there to hold it: what the phone shows above its
     * answer keys until the pane stops waiting. `dialog` marks a choice parsed
     * from the pane's own numbered lines, whose answers gain an Enter, and
     * `questions` carries a released AskUserQuestion's normalized question set
     * with the index currently being answered. */
    private terminalPrompts;
    /** The last time each pane's terminal lines were read for a dialog, so a
     * status tick reads them at most once per three seconds per pane. */
    private dialogReads;
    /** Panes whose last read terminal line is a password prompt, so the status
     * frame can offer the phone's secret sheet only when one is really asking. */
    private passwords;
    /** Conversations Claude Code is compacting, by target, until the new context
     * starts. The phone shows the state instead of the summary row's text. */
    private compactingSince;
    /** Panes where Phren just typed a bare slash command: the agent is drawing
     * that command's menu, which Herdr reports as an idle agent, so keys are
     * allowed there for a short while to walk and confirm it. The command text
     * and a one-shot confirmation attempt ride with the window. */
    private menus;
    private watching;
    readonly overview: ApprovalWatchLeases;
    private server?;
    private pushBindings;
    /** Held asks that were pushed: action -> when the notification expires. */
    private pushedHolds;
    /** Pushed asks whose hold ended with the request left in the terminal. */
    private releasedHolds;
    /** Terminal dialogs pushed to phones: by pane, the dialog last pushed; by
     * action, what an answer from the notification types. */
    private dialogPushes;
    private dialogActions;
    /** Terminal dialogs offered to a dispatching Hook, by pane: the `dialog-` action minted for the dialog's current title. */
    private forwardedDialogs;
    /** Requests this Hook pushed to its own phone on behalf of a dispatched worker on another computer: action -> the answer to send back. */
    private forwardedPushes;
    /** Panes a dispatching Hook polled lately (`server\npane\nsource` -> expiry). */
    private dispatchLeases;
    /** opencode permission asks seen on disk or listed by a served pane, by request id. */
    private opencode;
    /** Questions served OpenCode panes are asking, by question id. */
    private servedQuestions;
    /** One event subscription per served OpenCode pane; ticked with the Hook. */
    readonly paneServers: PaneServerWatcher;
    private closed;
    private opencodeSweep?;
    private opencodeLive;
    private fanoutLive;
    private fanoutSweeping;
    private opencodeWatcher?;
    private opencodePoll?;
    private opencodeDebounce?;
    /** Fan-out jobs already pushed as blocked, by job id and blocked timestamp. */
    private fanoutSeen;
    private fanoutTimer?;
    private fanoutArchiveTimer?;
    /** The name the phone paired with (macOS Computer Name), for approval alerts;
     * the short host name until that lookup answers. */
    private computerName;
    /** sudo -A requests from askpass, answered from the phone (sudo.ts). */
    readonly sudo: SudoBroker;
    constructor(push?: ApprovalPushService, modules?: ModuleSnapshot | undefined);
    private approvalsDirectory;
    private scheduleOpencodeSweep;
    /** Read every live opencode request file, map it to a target through the
     * recorded bindings or Herdr's explicit opencode session id, and register it
     * for a push and a push-binding answer. A file that vanished or expired is
     * forgotten. */
    sweepOpencodeApprovals(): Promise<void>;
    private readOpencodeApprovals;
    /** The pane a served OpenCode ask belongs to, as the phone names it: the
     * pane's place from its terminal, and the root of the asking session (a
     * subagent's ask shows on the conversation the pane shows). */
    private servedTarget;
    /** Everything a served pane is asking right now. New asks become the same
     * cards (and pushes) as the plugin's file asks; asks no longer listed were
     * answered in the TUI or went away, and their cards go with them. */
    servedAsks(entry: PaneServerEntry, client: PaneClient, asks: PaneAsks): Promise<void>;
    /** A served pane's process is gone: nothing it asked can be answered. */
    servedGone(key: string): void;
    /** The question a served OpenCode pane is asking, in the shape the phone
     * answers Claude's AskUserQuestion with (`/v1/questions/answer`). */
    servedQuestion(target: Target): Json | undefined;
    private servedClient;
    /** Answers a served pane's permission ask over its own API. */
    private answerServed;
    /** The phone's answers to a served pane's question: the labels it chose
     * (and a typed answer) per question, for exactly the questions it shows. */
    answerServedQuestion(target: Target, questions: DialogQuestion[], answers: DialogAnswer[]): Promise<void>;
    /** Keys the phone sends to a served OpenCode pane that its API answers
     * better than the terminal: Esc declines a pending question or stops a
     * working turn (`session.abort`), and a digit answers a pending
     * single-question, single-choice set. False leaves the keys to the pane. */
    servedKeys(target: Target, keys: readonly string[], status: string): Promise<boolean>;
    /** A session's exact pane. A recorded binding carries the full target; when
     * there is none, Herdr's explicit session identity names the pane. */
    private resolveTarget;
    /** Push once for each fan-out job whose blocked.json the plugin wrote. */
    private sweepBlockedFanouts;
    /** Move finished fan-out folders older than a day into the archive; one log
     * line records a sweep that moved or deleted anything, and a sweep that
     * fails never takes the Hook down. */
    private sweepFanoutArchive;
    watch(target: Target): () => void;
    /** Register a prompt about to be typed into `target`'s pane. The returned
     * promise settles "delivered" once that conversation's own hook submits the
     * text, "blocked" if another conversation in the pane tried to, or "pending"
     * after `waitMs`: a busy agent queues typed input and submits it only when
     * its turn ends, so the record outlives the wait (up to ten minutes) and a
     * late submission to the wrong conversation is still refused. A phone
     * message's `id` follows the record, so its outcome lands on that id. */
    expectDelivery(target: Target, text: string, waitMs?: number, signal?: AbortSignal, id?: string): Promise<DeliveryOutcome>;
    /** A prompt Phren typed into `target` that its conversation has not submitted yet. */
    deliveryPending(target: Target, text: string): boolean;
    /** Wait again for a delivery `expectDelivery` already reported pending,
     * after the Hook pressed Enter a second time. */
    awaitLateDelivery(target: Target, text: string, waitMs?: number): Promise<DeliveryOutcome>;
    private track;
    /** The Hook answered the phone message `id` as queued: its typed record
     * still waits for the agent's hook, which settles it by this id. */
    queueDelivery(id: string, target: Target): void;
    /** What became of the phone message `id` sent to `target`'s conversation:
     * queued until the agent submits it, then delivered, or blocked when
     * another conversation in the pane took it. Unknown while the Hook has not
     * answered it, for another conversation, or older than TRACK_MS. A
     * message answered uncertain still turns delivered once its hook takes it. */
    deliveryState(id: string, target: Target): TrackedState;
    /** Every known phone message to `target`'s conversation, for its stream. */
    deliveriesFor(target: Target): {
        deliveryId: string;
        state: TrackedState;
    }[];
    private settleTracked;
    /** The conversation `target` just submitted `prompt`. Nothing Phren typed
     * matches: a locally typed prompt, always allowed. Otherwise the oldest
     * matching delivery decides: its own conversation consumes it; any other
     * conversation is told to drop it, so the text is never spoken to the
     * wrong agent and the phone can safely send it again. */
    private submitted;
    /** The conversation's own turn events, for dispatch returns (turn-records.ts).
     * A failed write never fails the agent's callback. */
    private recordTurn;
    private rememberTerminalPrompt;
    /** The request the agent is showing in its terminal, if one fell through
     * in the last fifteen minutes; the caller only asks while the pane waits. */
    terminalPrompt(target: Target): Json | undefined;
    clearTerminalPrompt(target: Target): void;
    /** True while the pane's own terminal is reading a password. */
    passwordPrompt(target: Target): boolean;
    /** Claude Code's auto-mode fallback, opencode and Codex draw a numbered
     * dialog in the pane with no PermissionRequest hook behind it. While the pane
     * waits with nothing else to ask, read its last lines (at most once per
     * three seconds per pane) and publish the dialog as a terminal choice; drop
     * it when the pane leaves waiting or the dialog lines vanish. A remembered
     * permission request that is not a dialog keeps the slot untouched. The same
     * read notes whether the terminal is reading a password. */
    syncTerminalDialog(target: Target, active: boolean): Promise<void>;
    /** Resolve a phone option identifier. Real shortcuts retain their key path;
     * keyless rows are reached and verified before returning Enter to the route. */
    dialogAnswerKeys<K extends string>(target: Target, keys: readonly K[]): Promise<(K | "Enter")[]>;
    moveDialogHighlight(target: Target, expected: TerminalChoice, key: string, beforeKeys?: () => Promise<void>): Promise<void>;
    /** Put OpenCode's cursor on Allow once, the row's first option: ← as many
     * times as the cursor sits to its right, then read the colors again. The
     * same prompt must still be showing, or nothing more is sent. */
    private selectOpencodeOnce;
    /** Answer Claude's AskUserQuestion dialog in the pane: every question in
     * `answers` from `from` on, then the set's submission when `submit`. The
     * walk reads the pane before and after each key, so a dialog on another
     * tab, a changed question or a missed key stops it with nothing stray
     * typed. A finished set clears the remembered question. */
    answerClaudeQuestions(target: Target, questions: DialogQuestion[], answers: DialogAnswer[], options?: {
        from?: number;
        submit?: boolean;
    }): Promise<void>;
    /** An older phone answers a released AskUserQuestion one question at a
     * time with the chosen digits through `/v1/keys`. Walk that question with
     * the same verified steps and submit after the last. False when the
     * prompt is not a remembered question, so the keys take the usual path. */
    answerReleasedQuestion(target: Target, keys: readonly string[]): Promise<boolean>;
    private startCompacting;
    private stopCompacting;
    /** True while Claude Code is compacting `target`; a boundary older than ten
     * minutes is stale, so a missed SessionStart cannot pin the state forever. */
    compacting(target: Target): boolean;
    menuOpened(target: Target, command?: string): void;
    menuOpen(target: Target): boolean;
    /** The bare slash command that opened this pane's current menu window. */
    menuCommand(target: Target): string | undefined;
    menuClosed(target: Target): void;
    /** The pane with its colors, for dialogs whose cursor is only a color. */
    paneAnsi(target: Target): Promise<string>;
    /** Read what the pane draws, stripping ANSI unless placeholder styling is needed. */
    paneLines(target: Target, stripAnsi?: boolean): Promise<string>;
    /** After the phone walks Codex's /permissions menu onto Full Access, the
     * agent draws a second "Enable full access?" confirmation. Watch the pane's
     * terminal lines for it, answer with its own numbered choice, and only then
     * close the menu window. A confirmation that never arrives within three
     * seconds is left open as the visible prompt, published as the terminal
     * choice the phone draws as a question card. Runs at most once per window. */
    walkMenuConfirmation(target: Target): Promise<{
        menuClosed: boolean;
        waiting?: {
            message: string;
            choice?: TerminalChoice;
        };
    }>;
    approval(target: Target): Json | undefined;
    private opencodeApproval;
    /** Live fan-out worker asks shown on this parent conversation, oldest first. */
    private fanoutHeld;
    pendingPanes(server: string, state: Json): Set<string>;
    /** Hands the owner's answer to the fan-out launcher waiting on the worker's session. */
    private answerFanout;
    answer(target: Target, id: string, decision: unknown, updatedInput?: unknown): Promise<void>;
    /** A held approval that is really a terminal dialog, answered from the
     * phone with its own keys: let the callback fall back to the terminal at
     * once instead of leaving the card up until the 55-second timer. */
    releaseChoice(target: Target): void;
    /** The phone's Esc or Cancel on a question the Hook holds. A held
     * AskUserQuestion has no terminal choice, so `releaseChoice` never lets it
     * go and the terminal stays frozen until the hold timer; decline it now so
     * Claude moves on. True when a held question was cancelled. */
    cancelHeldQuestion(target: Target): boolean;
    answerPush(binding: string, decision: unknown): Promise<void>;
    /** Where a pushed ask lives, without answering it: the phone opens that
     * session's details when the notification itself is tapped. */
    pushTarget(binding: string): Target | undefined;
    /** Every waiting pane on this computer, each Hook tick, whether or not a
     * phone watches: an approval an agent draws as a numbered dialog in its
     * terminal (Claude's fallback prompts, Codex, OpenCode, Copilot) has no
     * hook behind it, so this is the only way it reaches a phone with phren
     * closed. Each dialog is pushed once; a pane that stops waiting drops it. */
    observeWaitingPanes(server: string, panes: Json[], resolve: (pane: Json) => Promise<Target | undefined>): Promise<void>;
    /** Keeps the held asks of these dispatched panes for the dispatching Hook's
     * next polls: without a phone, a watcher or push here, a worker's request
     * would go straight to its terminal and only its `blocked` state would
     * reach the conductor. Keyed by pane, since a worker that has not yet
     * reported a conversation has no session. */
    leaseDispatch(targets: readonly {
        server: string;
        pane: string;
        source: string;
    }[]): void;
    private dispatchLeased;
    private dropForwardedDialog;
    /** What this pane's worker is waiting on, for the dispatching Hook: the held
     * or app-server request, else the terminal dialog the pane draws (offered
     * under a `dialog-` action the answer route accepts). */
    workerApproval(target: Target): ForwardedApproval | undefined;
    /** A worker's request on another computer, sent to this Hook's own phone:
     * the notification's answer calls `answer`, which sends it back over the
     * dispatch path. */
    pushForwarded(summary: {
        provider: string;
        computer: string;
        project?: string;
        request: string;
        requestKind?: RequestKind;
    }, answer: (decision: "approve" | "deny") => Promise<void>): void;
    private adoptReleasedHold;
    private dropDialogPush;
    /** The dialog a pane's screen shows now. Codex draws "> 1. Yes, proceed (y)"
     * rows and Copilot a boxed select with a cursor row, both answered by moving
     * to a row; OpenCode's prompt is one row of options whose cursor is only a
     * color; the other fallbacks number rows without a key in the label. */
    private screenDialog;
    /** Approve picks the dialog's yes/allow row (else its first), Deny its
     * no/deny row (else Escape), typed as the pane's own keys. */
    private answerDialog;
    private dropPushBindings;
    /** A server request from one of the Hook's own Codex app-servers: an
     * approval card like a held PermissionRequest, pushed the same way, but
     * with no hold timer. The request stays parked in Codex until someone
     * answers it, here or in the pane's TUI (`codexResolved`). */
    codexRequest(target: Target, request: PendingServerRequest, answer: (result: Json) => void): void;
    /** Another client (the pane's TUI) answered the request, or this one declined it. */
    codexResolved(target: Target, requestId: AppServerRequestId): void;
    start(): Promise<void>;
    close(): void;
}
/** What a Stop payload says about the turn it ends: the harness's last
 * assistant message (Claude, Codex) and, from Claude Code, the background
 * tasks still in flight, which will wake the conversation again when they
 * finish. Absent fields are left out, so the Hook falls back to the transcript. */
export declare function stopFacts(value: Json): {
    background?: number;
    reply?: string;
};
/** One agent callback, run as `hook <source>`. Resolves "abandoned" when a
 * tool call's callback ran out of its budget: the caller then exits at once,
 * since a socket or a process read still pending would hold the agent. */
export declare function agentHook(source: Provider, elapsed?: () => number): Promise<void | "abandoned">;
/**
 * Runs a callback's work. A PreToolUse or PostToolUse callback gets what is
 * left of TOOL_HOOK_BUDGET_MS since its process started (hook-fast.ts says
 * why): "abandoned" once that runs out, and the caller exits without an
 * answer so the tool runs. Every other event runs to its own timeouts.
 */
export declare function withinToolBudget<T>(event: string, work: (left: () => number) => Promise<T>, elapsed?: () => number, budgetMs?: number): Promise<T | "abandoned">;
