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 CardUsageSnapshot } from './md-card.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 文案 + ✅/⬜️),消息额度 * 展示当前值并通过独立输入卡修改。即时项在卡片回调后刷新。 * 只吃纯数据 {@link ConfigCardData},不反向依赖 store,避免循环依赖。 */ export declare function buildConfigCard(data: ConfigCardData, locale?: Locale): string; /** * 消息额度输入子卡:接受 1–1000 的任意整数,留空恢复内置策略。 * 使用独立新卡承载 v1 form,避免主配置卡的开关 patch 到含 form 的卡体。 */ export declare function buildConfigQuotaCard(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; /** Shared terminal multi-url behavior for streaming and dashboard cards. */ export declare function terminalMultiUrl(url: string): Record; /** * 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, localCliReady?: boolean, runtimeDisplayName?: string): 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 `), 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") 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, runtimeDisplayName?: string, resumeStartsFresh?: boolean): string; /** Parent-topic panel for `/fork `. Links and live/closed state are * resolved by the command layer; this function only renders the card. */ export declare function buildForkPanelCard(children: Array<{ instruction: string; status: 'active' | 'closed'; link: string; }>, 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; /** * 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, localCliReady?: boolean, usage?: CardUsageSnapshot, runtimeDisplayName?: string, serviceTierBadge?: 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 controls: * • when available, a read-only "open terminal" link and "get write link"; * • "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, runtimeDisplayName?: string): string; /** Repo selection card. `multiPicker` (persisted per-bot via worktreeMultiPicker) * flips the worktree control between an instant single-select dropdown (false) * and the inline multi-select form (true). */ export declare function buildRepoSelectCard(projects: ProjectInfo[], currentPath?: string, rootMessageId?: string, locale?: Locale, multiPicker?: boolean): 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'; /** 当前卡片暂存的限制;缺省使用产品默认值。 */ durationMs?: number; quota?: number; } /** 授权卡片:有效期与消息额度并列展示,owner 一次提交两项限制。 */ export declare function buildGrantCard(o: GrantCardOpts, locale?: Locale): string; /** 授权成功后给被授权人的通知卡(独立消息)。支持一次通知多个被授权人;带额度时追加"(额度 N 条)"。 * * **bot grantee 有名字就用纯文本名字、拿不到名字才 `` 兜底;真人 grantee 一律 `` 点名**: * 卡片里的 `` 会被对方 bot 的 daemon 当成一次「被 @」消息,凭新授权/同伴 peer * 关系在本群拉起一个空会话(实测:手动 /grant 后没有 prompt → 空会话「等待输入」)。所以能拿到 * bot 名字时优先用纯文本(不产生 mention、不唤醒对方);只有名字缺失时才退回 ``——此时飞书 * 能据 open_id 展示对方身份(远比裸 open_id 可读),代价是可能偶尔触发一次空会话(产品上可接受, * 且名字缺失是少数边角情况)。真人被 `` 不会自动开会话。传 string/string[](无 isBot 信息) * 时按真人处理(@ 全部),保持旧调用方/单测兼容。 */ export declare function buildGrantNotifyCard(kind: 'chat' | 'global', target: string | string[] | Array<{ openId: string; name?: string; isBot?: boolean; }>, locale?: Locale, quota?: number, expiresAt?: number): string; /** 额度用尽通知卡(@被授权人):daemon 收回该 scope 授权后发到 session/线程。 */ export declare function buildQuotaExhaustedCard(targetOpenId: string, limit: number, locale?: Locale): string; /** * Reject card for `/adopt` (and Codex App / resume import) attempted while the * session is still on the first-spawn repo-select gate (`pendingRepo`). Adopt * attaches to an already-running CLI, so it cannot double as a way to finish * that gate: the two states are mutually exclusive by design. Rather than fold * the buffered repo-card messages into the takeover (complex + leaks botmux * envelopes into the external CLI), we refuse and offer a one-tap "close * session" so the user can retire the pending session and re-issue `/adopt` * cleanly. The close button reuses the shared `action: 'close'` handler; the * resulting closed card honours privateCard on its own. */ export declare function buildAdoptBlockedCard(rootId: string, sessionId: string, cliId: CliId | undefined, locale?: Locale): string; /** 被授权目标的 @ 渲染:bot 有名字用纯文本(不 免唤醒对方),真人/无名字 bot 用 点名。 */ type GrantTargetEntry = { openId: string; name?: string; isBot?: boolean; }; /** 授权处置后的终态卡(无按钮,防重复点击)。授权成功(chat/global)时**就地 patch 原卡**即为 * 此卡:正文直接 @ 被授权人 + 额度/有效期,一张卡既是结果态又 ping 到 ta,无需再单独发通知卡或 * 撤回原卡(见申晗 2026-07-31 反馈)。deny 或无 targets 时回落到不带 @ 的简单状态文案。 */ export declare function buildGrantResultCard(kind: 'chat' | 'global' | 'deny', locale?: Locale, quota?: number, expiresAt?: number, targets?: string | string[] | GrantTargetEntry[]): string; /** * Build a Feishu interactive card for a TUI prompt (ask-hook / CoCo picker). * 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; /** Build a terminal failure state when worker/backend input was not confirmed. */ export declare function buildTuiPromptFailedCard(message: 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', /** When 'private', the card is (or will be) delivered as an ephemeral card * visible only to the invoker — so the session title / source-chat name never * leak to other group members. Baked into every button value as `visibility` * so the re-render handlers (select / page / search) know they must delete + * resend an ephemeral card instead of returning a body for Lark to patch in * place (ephemeral cards can't be PATCH-updated). Default 'public' preserves * the legacy visible-to-all picker. Only ever set 'private' for flat chat- * scope 普通群 targets: ephemeral has no thread anchor, so command-handler * gates it on `targetScope === 'chat'` (thread-scope 话题群/话题 stay public * in-thread — see the gate comment there). p2p never goes ephemeral. */ visibility?: 'private' | 'public'): string; export type AdoptEntryKind = 'live' | 'resume'; export interface AdoptPickerEntry { /** Synthetic selection key, unique & deterministic across both sources. * live → "live:" + adoptTargetKey / zellij target; resume → "resume:" + cliSessionId. * Deterministic so a re-render (which re-discovers) reproduces the same key. */ key: string; kind: AdoptEntryKind; cliId?: CliId; cliDisplayName?: string; /** resume: first user prompt; live: project (cwd basename). */ title: string; /** cwd basename, shown compactly. */ project: string; /** Absolute working dir, shown verbatim. */ cwd: string; /** live: probed CLI session id (may be undefined); resume: cliSessionId. */ sessionId?: string; /** live: tmux/zellij/herdr target label. */ target?: string; /** live: startedAt (uptime); resume: lastActivityAt. */ timeMs?: number; /** One-based position among history candidates, for same-screen disambiguation. */ candidateNumber?: number; } /** Deterministic key for a live adoptable session (tmux/herdr/zellij). * Exported so the card-handler's confirm path can match a clicked entry_key * back to a freshly-discovered session without re-deriving the format. * * ⚠️ zellij keys are pid-AGNOSTIC on purpose — do NOT add cliPid back. * Confirm re-discovers and matches `adoptLiveKey(fresh) === entryKey`; a * zellij pane's resolved CLI pid legitimately shifts between render and * confirm (wrapper⇄native collapse, re-fork), so baking pid into the key * makes that match spuriously fail → user sees a false "目标已退出". This * is exactly the bug fix 57dcbebbb removed ("点击候选改按 (session,paneId) * 匹配"): (zellijSession, zellijPaneId) already uniquely identifies the pane. * tmux/herdr keep adoptTargetKey (tmux includes pid, herdr does not) — tmux's * confirm fast-path parses the trailing pid, and that path is unchanged. */ export declare function adoptLiveKey(s: AdoptableSession | ZellijAdoptableSession): string; /** Fold both adopt sources into one uniform entry list. Live entries come * first (they're the "act now" targets), resume entries after. Order is * stable so pagination is deterministic across re-renders. * * `resumeCliId` labels the resume (history) entries with the bot's own CLI — * ResumableSession carries no cliId (resume only ever offers the bot's own * CLI, so the caller knows it), and the user wants to see "Codex" on each * history row rather than a blank. */ export declare function buildAdoptEntries(sessions: Array, resumable: ResumableSession[], resumeCliId?: CliId, runtimeDisplayName?: string): AdoptPickerEntry[]; export interface AdoptPickerState { selectedKey?: string; searchQuery?: string; page?: number; } /** * V2 adopt picker card. Layout mirrors buildRelayPickerCard: search box → * ≤5 session cards (clickable, highlight on select) → paginator → confirm. * All state (search / page / selected / root_id / invoker) rides on the * value objects since Lark cards are stateless server-side. * * `truncated` renders a hint when the resume list was capped, so the user * knows to narrow via search instead of assuming they saw everything. */ export declare function buildAdoptSelectCard(sessions: Array, rootMessageId?: string, locale?: Locale, resumable?: ResumableSession[], state?: AdoptPickerState, invokerOpenId?: string, resumeLimit?: number, resumeCliId?: CliId, runtimeDisplayName?: string): string; export declare function buildCodexAppThreadSelectCard(threads: CodexAppThreadSummary[], rootMessageId?: string, locale?: Locale): string; export {}; //# sourceMappingURL=card-builder.d.ts.map