import type { ProjectInfo } from '../../services/project-scanner.js';
import type { CliId, ResumableSession } from '../../adapters/cli/types.js';
import { type AdoptableSession } from '../../core/session-discovery.js';
import type { ZellijAdoptableSession } from '../../core/zellij-adopt-discovery.js';
import type { CodexAppThreadSummary } from '../../services/codex-app-threads.js';
import type { DisplayMode, StreamStatus } from '../../types.js';
import type { CliUsageLimitState } from '../../utils/cli-usage-limit.js';
import { type Locale } from '../../i18n/index.js';
import type { ConfigCardData } from '../../services/bot-config-store.js';
/** select_static 里代表「清回默认 / 未设置」的哨兵值（model / lang 下拉用）。 */
export declare const CONFIG_UNSET = "__unset__";
/**
 * 交互配置卡片：`/botconfig`（裸）返回它。按配置页逻辑分区（运行 / 卡片行为 / 主动开工 /
 * 安全·授权），cli·model·lang 用下拉，布尔字段用切换按钮（i18n 文案 + ✅/⬜️），消息额度
 * 用下拉。点一下即改并就地刷新（见 card-handler 的 config_set / config_toggle / config_quota）。
 * 只吃纯数据 {@link ConfigCardData}，不反向依赖 store，避免循环依赖。
 */
export declare function buildConfigCard(data: ConfigCardData, locale?: Locale): string;
/**
 * 「文本设置」子卡：从主配置卡点「✏️ 文本设置」唤起。承载自由文本字段——卡片签名
 * （brandLabel）、入群首轮 prompt（autoStartOnGroupJoinPrompt）、默认角色（team role）。
 * v1 `form`+`input` 实现（仓库已验证），输入框预填当前值，一个「保存」提交全部
 * （form_submit → config_text_save），留空=清除该项；「⬅ 返回」回主卡（config_back）。
 */
export declare function buildConfigTextCard(data: ConfigCardData, locale?: Locale): string;
export declare function getCliDisplayName(cliId: CliId): string;
/**
 * Build a Feishu interactive card with terminal button + action buttons.
 * @param showManageButtons - When true, include restart & close buttons (used in the private write-link card — delivered as a "visible-to-you" ephemeral card in plain groups, or DM'd as fallback).
 * @param adoptMode - When true, the danger button reads "⏏ 断开" with action `disconnect` (only tears down botmux's bridge worker, leaves the user's tmux pane / Claude process alone). Mutually exclusive with `showManageButtons` (DM management isn't surfaced for adopt sessions). Without this flag the card uses the original "❌ 关闭会话" button which closes the underlying CLI — wrong for adopt where we never owned the CLI in the first place.
 */
export declare function buildSessionCard(sessionId: string, rootId: string, terminalUrl: string, title: string, cliId?: CliId, showManageButtons?: boolean, adoptMode?: boolean, locale?: Locale): string;
/**
 * Build the "session closed" card shown after `/close` (or the close button).
 * Surfaces a Resume button + a copyable terminal command so the user has an
 * obvious path back instead of just a dead-end status text.
 *
 * The terminal command is the *CLI's own* resume invocation (e.g.
 * `claude --resume <id>`), built by the per-CLI adapter's
 * `buildResumeCommand`. That keeps the conversation portable: users can
 * pick it up locally without going through botmux. CLIs that can't resume
 * a specific session from CLI args (gemini's "latest only", opencode)
 * surface a fallback note instead of a fake command.
 *
 * The "▶️ 恢复会话" button still goes through botmux — it re-enables the
 * Lark bridge so future replies route back into this topic.
 */
export declare function buildSessionClosedCard(sessionId: string, rootId: string, title: string, cliId?: CliId, workingDir?: string, cliResumeCommand?: string | null, locale?: Locale): string;
/** Build the lightweight placeholder shown while a no-streaming-card bot works. */
export declare function buildPendingResponseCard(locale?: Locale): string;
/**
 * Build the `/list-slash-command` card (schema 2.0): a coloured header and four
 * sections — ① fixed passthrough allowlist, ② adapter-default passthrough,
 * ③ user-configured custom passthrough, ④ auto-discovered CLI commands/skills/plugins
 * rendered as a paginated native table (command | description). An optional MCP
 * servers note is appended.
 */
export declare function buildSlashListCard(params: {
    cliName: string;
    builtin: string[];
    adapterDefaults?: string[];
    custom: string[];
    discovered: {
        name: string;
        description?: string;
    }[];
    workingDir: string;
    mcpServers: string[];
    discoverySupported?: boolean;
}, locale?: Locale): string;
export declare function buildDetouredPendingResponseCard(locale?: Locale): string;
export declare function buildMentionedPendingResponseCard(locale?: Locale): string;
/**
 * Build a frozen-snapshot card to PATCH onto the source-chat streaming card
 * after `/relay` moves the session elsewhere.
 *
 * Why this exists: a live streaming card carries action buttons (close /
 * toggle display / get write link). Those buttons identify their session by
 * `session_id` in the value payload, so clicking them after relay still
 * reaches the now-relocated session — closing it, toggling its display
 * mode, etc. — but the visible feedback all lands on the NEW card in the
 * target chat, not this one. The source-chat card then looks like a "live
 * console" while actually being a footgun. PATCH it to an inert snapshot
 * so the user sees clearly it's historical.
 *
 * Last-frame rendering:
 *   - imageKey present (session was in 'screenshot' / expanded mode at
 *     relay time) → embed the same img element the live card had.
 *     img_key is a Lark server resource independent of the card it lived
 *     on, so the PATCHed card can still reference it.
 *   - imageKey absent (hidden / collapsed mode) → render nothing extra.
 *     The header + body notice already convey the state; raw tmux pane
 *     text as a code-block is too long and noisy (王皓 caught this in
 *     testing).
 *
 * No action buttons are rendered in either case.
 */
export declare function buildRelayedFrozenCard(title: string, cliId?: CliId, imageKey?: string, locale?: Locale): string;
/**
 * Truncate content to fit within `maxBytes`, keeping the tail (most recent
 * output). Defaults to {@link MAX_CONTENT_BYTES}; callers that wrap the content
 * in additional card JSON (e.g. the private snapshot's code fence) pass a
 * tighter budget so the whole card stays under Feishu's ~109 KB hard limit.
 */
export declare function truncateContent(content: string, locale?: Locale, maxBytes?: number): string;
/**
 * Build a Feishu streaming card that shows live terminal output + controls.
 * This card is PATCHed in-place as the CLI works.
 *
 * displayMode:
 *   - 'hidden'     — body collapsed; only header + main controls visible.
 *   - 'screenshot' — img element (rendered server-side, uploaded for img_key).
 *
 * Quick-action buttons (Esc, ^C, Tab, Space, Enter, ←↑↓→, ½屏 ↑/↓) appear
 * whenever displayMode !== 'hidden'.
 */
export declare function buildStreamingCard(sessionId: string, rootId: string, terminalUrl: string, title: string, screenContent: string, status: StreamStatus, cliId?: CliId, displayMode?: DisplayMode, cardNonce?: string, imageKey?: string, adoptMode?: boolean, showTakeover?: boolean, locale?: Locale, usageLimit?: CliUsageLimitState, writableTerminalUrl?: string): string;
/**
 * Build a static "private snapshot" card for `/card` in private mode — sent via
 * the ephemeral API to one user at a time. Unlike {@link buildStreamingCard} it
 * is **never PATCH-updated** (ephemeral cards can't be), so it carries only a
 * one-shot snapshot of the terminal screenshot plus three buttons:
 *   • read-only "open terminal" link (a plain URL button — no callback);
 *   • "get write link", whose callback DMs the writable link to the clicker;
 *   • "close session", whose callback kills the session and (in private mode)
 *     sends the "closed" card ephemeral to the owner audience too — so the
 *     session title / CLI name / workingDir on it don't leak to the group.
 * The last two have callbacks but neither patches THIS card (one DMs, the other
 * sends a fresh card), so both work fine on an ephemeral card. Both are
 * `canOperate`-gated in the handler — talk-only viewers who tap them are denied.
 * The patch-driven controls (toggle/refresh/export/term keys) and the inline
 * writable link are still omitted: those need to update this card, which
 * ephemeral cards can't do.
 */
export declare function buildPrivateSnapshotCard(terminalUrl: string, title: string, status: StreamStatus, cliId: CliId | undefined, imageKey: string | undefined, screenContent: string, sessionId: string, rootId: string, locale?: Locale, usageLimit?: CliUsageLimitState): string;
/**
 * Build a Feishu interactive card with a dropdown selector for projects.
 * Returns a JSON string suitable for msg_type: 'interactive'.
 */
export declare function buildRepoSelectCard(projects: ProjectInfo[], currentPath?: string, rootMessageId?: string, locale?: Locale): string;
export interface GrantCardOpts {
    ownerOpenId: string;
    /** 待授权目标，支持一次 /grant @a @b 多目标；owner 点一次范围对全部生效。 */
    targets: Array<{
        openId: string;
        name: string;
    }>;
    chatId: string;
    nonce: string;
    /** 'request' = 无权限者自助申请；'owner' = owner 主动 /grant。仅文案不同。 */
    mode: 'request' | 'owner';
}
/** 授权卡片：正文 @owner，三枚按钮各带 action + 上下文 + nonce。
 *  多目标共用一张卡，按钮 value 带 target_open_ids 数组，owner 点一次范围套用到全部。 */
export declare function buildGrantCard(o: GrantCardOpts, locale?: Locale): string;
/** 授权成功后给被授权人的通知卡（独立消息）。支持一次通知多个被授权人；带额度时追加"（额度 N 条）"。
 *
 *  **真人 grantee 用 `<at>` 点名，bot grantee 只用纯文本名字**：卡片里的 `<at id=botOpenId>` 会被
 *  对方 bot 的 daemon 当成一次「被 @」消息，从而凭新授权/同伴 peer 关系在本群误拉起一个空会话
 *  （申晗实测 bug：手动 /grant 后面没有 prompt，不该触发自动会话）。纯文本名字不产生 mention 事件，
 *  既保留「谁被授权」的可读信息，又不会唤醒对方 bot。传 string/string[]（无 isBot 信息）时按真人
 *  处理（@ 全部），保持旧调用方/单测兼容。 */
export declare function buildGrantNotifyCard(kind: 'chat' | 'global', target: string | string[] | Array<{
    openId: string;
    name?: string;
    isBot?: boolean;
}>, locale?: Locale, quota?: number): string;
/** 额度用尽通知卡（@被授权人）：daemon 收回该 scope 授权后发到 session/线程。 */
export declare function buildQuotaExhaustedCard(targetOpenId: string, limit: number, locale?: Locale): string;
/** 授权处置后的终态卡（无按钮，防重复点击）。 */
export declare function buildGrantResultCard(kind: 'chat' | 'global' | 'deny', locale?: Locale): string;
/**
 * Build a Feishu interactive card for a TUI prompt detected by ScreenAnalyzer.
 * Select-type options get buttons; input-type options shown in list with a note.
 */
export declare function buildTuiPromptCard(rootId: string, sessionId: string, description: string, options: Array<{
    label?: string;
    text: string;
    selected: boolean;
    type?: string;
    keys?: string[];
}>, multiSelect?: boolean, toggledIndices?: number[], locale?: Locale): string;
/**
 * Build a "processing" TUI prompt card — shown immediately when user clicks a button.
 */
export declare function buildTuiPromptProcessingCard(selectedText: string, locale?: Locale): string;
/**
 * Build a resolved TUI prompt card — shows which option was selected.
 */
export declare function buildTuiPromptResolvedCard(selectedText: string, locale?: Locale): string;
export interface RelayPickerEntry {
    sessionId: string;
    /** Short human label for the source chat — chat name if resolvable, else chatId. */
    chatLabel: string;
    /** First-turn title or current-turn topic — already truncated by the caller. */
    title: string;
    /** Absolute working dir, displayed verbatim. */
    workingDir?: string;
    /** CLI identifier, used to render a friendly name. */
    cliId?: CliId;
    /** Last activity timestamp, used to render a relative duration. */
    lastMessageAt?: number;
    /** Source chat's conversational topology. Drives the type tag in the
     *  picker. Caller supplies based on getChatNameAndMode lookup + the
     *  session's own chatType for the p2p case. */
    chatMode?: 'group' | 'topic' | 'p2p';
    /** Snapshot of whether the session's worker is mid-turn at render time.
     *  When the selected entry is running, the picker disables the confirm
     *  button (transferSession would refuse a busy worker anyway). Snapshot,
     *  not live — re-selecting the entry recomputes it. */
    running?: boolean;
}
export interface RelayPickerState {
    /** Currently selected sessionId, if any (drives the highlight + confirm button). */
    selectedSessionId?: string;
    /** Case-insensitive substring filter applied to title / chatLabel / workingDir. */
    searchQuery?: string;
    /** 0-indexed page within the filtered list. Clamped to valid range at render time. */
    page?: number;
}
/**
 * Card listing the operator's relayable sessions, paginated 5 per page with
 * a search box at the top and a confirm button at the bottom. Layout:
 *
 *   ┌──────────────────────────────────────┐
 *   │ 📋 选择要接力的会话                   │  header
 *   ├──────────────────────────────────────┤
 *   │ 🔍 [______________] [搜索]            │  form: input + submit button
 *   ├──────────────────────────────────────┤
 *   │  [interactive_container 1]            │  current page (≤5 cards),
 *   │  [interactive_container 2]            │  each clickable for selection
 *   │   ...                                 │
 *   ├──────────────────────────────────────┤
 *   │  [← 上一页]  1 / 4  [下一页 →]        │  paginator row
 *   ├──────────────────────────────────────┤
 *   │   [确认接力到本群]                    │  primary button (only when
 *   │                                       │   a selected session is on
 *   │                                       │   the current filtered set)
 *   └──────────────────────────────────────┘
 *
 * State (search / page / selected) is propagated entirely via the value
 * objects on each button and container — Lark cards are stateless, so any
 * server-side re-render must reconstruct from what the click sent us.
 * That's why every interactive value here carries `search`, `page`,
 * `target_chat_id`, `root_id`.
 *
 * Note: typing into the search box without clicking 搜索 does NOT update
 * the in-callback state — container/paginator clicks use whatever search
 * was applied at card-render time. To apply a new filter, click 搜索.
 */
export declare function buildRelayPickerCard(entries: RelayPickerEntry[], targetChatId: string, targetRootMessageId: string, invokerOpenId: string, locale?: Locale, state?: RelayPickerState, 
/** Target routing scope baked into every button value so the confirm /
 *  re-render handlers know whether to land the relayed session as a 话题
 *  (thread, reply_in_thread to `root_id`) or flat chat-scope. Default 'chat'
 *  preserves the legacy普通群-flat behavior. */
targetScope?: 'thread' | 'chat', 
/** Target chat type baked into every button value so relay_confirm can pass
 *  the right chatType to transferSession (a DM target must flip the session
 *  to p2p, or post-relay inbound routing misclassifies it as a group).
 *  Authoritative from the /relay command's session chatType. Default 'group'
 *  covers legacy cards rendered before this field existed. */
targetChatType?: 'group' | 'p2p'): string;
export declare function buildAdoptSelectCard(sessions: Array<AdoptableSession | ZellijAdoptableSession>, rootMessageId?: string, locale?: Locale, resumable?: ResumableSession[]): string;
export declare function buildCodexAppThreadSelectCard(threads: CodexAppThreadSummary[], rootMessageId?: string, locale?: Locale): string;
export interface LandCardOpts {
    sessionId: string;
    workingDir: string;
    statText: string;
    files: number;
    insertions: number;
    deletions: number;
    preview: string;
    truncated?: boolean;
    patchAttached?: boolean;
}
export declare function buildLandCard(o: LandCardOpts, locale?: Locale): string;
export declare function buildLandResultCard(kind: 'applied' | 'discarded' | 'failed', detail: string, locale?: Locale): string;
//# sourceMappingURL=card-builder.d.ts.map