/** * ui.ts — Everything a human reads, as pure functions. * * Two audiences, and they want opposite things. The owner is on a phone * with no terminal: they need to know a five-minute turn is alive, that a * failure happened, and they need a way to stop a turn that went wrong * without walking back to the computer. The operator is in front of pi: * they need the channel's state without tailing a log file. * * Both used to be served almost entirely by `log.info` into * `~/.pi/wechat/channel.log`, which is to say: not served. The strings * live here rather than inline in index.ts for the usual reason — a * message the user sees is worth a test, and none of this needs state. * * Type-only imports keep the module free of runtime relative imports, as * node's native type stripper requires. */ import type { ChannelConfig } from './config.js'; import type { MediaSurvey } from './media.js'; export type ChannelCommandKind = 'cancel' | 'status' | 'queue' | 'clear' | 'help'; /** * Is this message a command to the channel rather than a message for the * agent? * * Only an exact known name counts. The alternative — treating any * `#word` as a command and answering unknown ones with a usage list — * hijacks ordinary messages (`#3 还是不对`, `#紧急`), and a message the * owner meant for the agent silently turning into an error reply is worse * than a mistyped `#cancle` reaching the agent, where they will see it in * the answer. */ export declare function parseChannelCommand(text: string, prefix: string): ChannelCommandKind | null; /** The reply to `#help`, listing what works from the phone. */ export declare function channelHelpText(prefix: string): string; export type OwnerNotice = /** An answer was abandoned. `why` matches GIVEUP_REASON / restore paths. */ { kind: 'dropped'; why: DropReason; attempts?: number; } /** The watchdog fired before the agent finished. */ | { kind: 'timeout'; ms: number; } /** Messages arrived while the previous turn was still running. */ | { kind: 'queued'; count: number; } /** The channel came back and is working through what piled up. */ | { kind: 'resumed'; count: number; } /** `#cancel` took effect. */ | { kind: 'cancelled'; } /** `#cancel` with nothing to cancel. */ | { kind: 'nothingToCancel'; } /** `#clear` dropped queued messages. */ | { kind: 'cleared'; count: number; } /** Progress while the agent works. */ | { kind: 'progress'; what: string; } /** * Another extension is asking a question only the terminal can answer. * `prefix` is the channel-command prefix, so the way out named in the text * is the one this deployment actually answers to. */ | { kind: 'askUserQuestion'; questions: AskQuestion[]; prefix: string; } /** `#status` / `#queue`. */ | { kind: 'status'; connected: boolean; queued: number; busy: boolean; contextPercent: number | null; } | { kind: 'queue'; queued: number; busy: boolean; }; /** Render one notice as the WeChat message the owner receives. */ export declare function renderOwnerNotice(n: OwnerNotice): string; export type DropReason = 'partial' | 'unknown' | 'exhausted' | 'parked' | 'inherited'; /** ms as the coarse Chinese duration these notices want ("30 分钟"). */ export declare function formatDuration(ms: number): string; /** The channel the ask package publishes its questions on. Their name, fixed. */ export declare const ASK_USER_PROMPT_EVENT = "rpiv:ask-user:prompt"; export interface AskOption { label: string; description: string; } export interface AskQuestion { question: string; /** The short chip shown beside the question. May be empty. */ header: string; multiSelect: boolean; options: AskOption[]; } /** * Read an `rpiv:ask-user:prompt` payload, keeping only what is usable. * * Empty means "nothing worth sending" — a payload of the wrong shape, or * questions with neither text nor chip. The caller sends no notice then, * which is the right failure: a malformed prompt is not worth telling the * owner to walk to their terminal for. */ export declare function normalizeAskPrompt(payload: unknown): AskQuestion[]; /** * A one-line Chinese description of the tool call in progress. * * Paths are shown as their basename: the owner knows their own project, * and a full path is mostly noise on a phone screen. `bash` shows its * command because "正在执行命令" alone is the least informative line in a * turn — and the only reader is the owner, whose own machine it is. */ export declare function toolProgressPhrase(toolName: string, args: unknown): string; /** Last path segment, empty-safe (no runtime import of node:path here). */ export declare function baseName(p: string): string; /** * Is enough time gone by to send another progress line? * * `lastSentMs === 0` means none has been sent for this turn, and the first * one goes out immediately — a turn that names what it is doing within a * second of starting is what makes the rest of the wait tolerable. */ export declare function shouldSendProgress(lastSentMs: number, nowMs: number, intervalMs: number): boolean; export interface StatusSnapshot { connected: boolean; accountId: string; ownerUserId: string; workerState: 'stopped' | 'starting' | 'ready'; /** Whether this session may run a worker at all. */ channelOwned: boolean; /** The pid holding the channel instead, when this session does not. */ ownershipBlockedBy: number; queued: number; merging: boolean; busy: boolean; turnsUsed: number; turnsMax: number; windowMs: number; delivery: { attempts: number; maxAttempts: number; } | null; preserveMarkdown: boolean; persisting: boolean; persistDisabledReason: string; configWarnings: number; logFile: string; } export declare function formatStatus(s: StatusSnapshot): string; /** The footer line. Small enough to sit next to the git branch. */ export declare function formatFooterStatus(s: Pick): string; export interface HistorySnapshot { queued: number; inFlight: number; dedupIds: number; pendingDelivery: boolean; lastDropped: { userId: string; at: string; why: string; } | null; stateFile: string; } export declare function formatHistory(h: HistorySnapshot): string; /** * The effective configuration, marking what came from the file. * * `warnings` is the point of the whole subcommand: a rejected value or a * misspelled key was previously logged to a file nobody opens, which is * how someone spends an afternoon wondering why their setting does * nothing. */ export declare function formatConfig(config: ChannelConfig, defaults: ChannelConfig, file: string, fileExists: boolean, warnings: readonly string[]): string; /** `/wechat` with no arguments, or an unknown subcommand. */ export declare function formatUsage(): string; /** * `/wechat log --tail [n]` — how many lines to print. * * Bounded at both ends: a missing or nonsensical count falls back to 20, * and the cap keeps a stray `--tail 100000` from pushing the whole session * out of the TUI's scrollback. */ export declare function parseTailCount(parts: readonly string[], fallback?: number, max?: number): number; export interface SendMediaArgs { targetUser: string; /** A local path or an https URL; '' when the command named neither. */ source: string; caption: string; } /** * `/wechat send-media [] [caption…]` * * The owner is the only recipient this channel has, so making their id * mandatory was pure friction — but an explicit id still works, for the * case of a second contact who has written in. * * Which form was typed is decided by asking `isSource` (the filesystem, in * practice) rather than by the shape of the token. A WeChat id looks like * `wxid_xxx@im.wechat`, so "contains a dot" reads it as a path, and * "starts with wxid_" fails for any other id format. A first token that is * neither a URL nor an existing file, with a second token behind it, is an * id. */ export declare function parseSendMediaArgs(parts: readonly string[], defaultUser: string, isSource: (token: string) => boolean): SendMediaArgs; export declare function isHttpUrl(s: string): boolean; /** `/wechat log --tail` — the lines, or the fact that there are none. */ export declare function formatLogTail(file: string, lines: readonly string[]): string; /** `/wechat prefix` — what a message must start with, and where to change it. */ export declare function formatPrefixInfo(aiPrefix: string, commandPrefix: string, configFile: string): string; /** `/wechat owner` — the binding, or the fact that the next sender takes it. */ export declare function formatOwnerInfo(ownerUserId: string, ownerFile: string): string; /** * `/wechat stop` — say what stopping costs. * * Queued messages sit unanswered until the next start, and a parked reply * is on a timer that will eventually throw the answer away. */ export declare function formatStopSummary(queued: number, undeliveredReply: boolean): string; /** `/wechat media` — what is on disk, and how much of it is stale. */ export declare function formatMediaSummary(survey: MediaSurvey, dir: string): string; /** Subcommand names for the command-line completer. */ export declare const SUBCOMMANDS: readonly string[]; /** * Complete a partially typed subcommand, plus the flags that belong to it. * Returns null when there is nothing useful to offer, which is what pi's * `getArgumentCompletions` wants for "fall through to the default". */ export declare function completeSubcommand(argumentPrefix: string): string[] | null; //# sourceMappingURL=ui.d.ts.map