@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
113 lines (112 loc) • 5.62 kB
TypeScript
import { type Target } from "./protocol.js";
import type { Entry } from "./transcripts.js";
/**
* The reply being written. `delta` and `streamed` appear only on text built
* from a harness's own deltas (Codex, OpenCode, phren's agent), never on text
* read off a terminal: `delta` is what was appended since this socket's
* previous preview frame (the whole text when the block is new to it).
*/
export interface TranscriptPreview {
turnStartedAt: string;
text: string;
delta?: string;
streamed?: true;
}
export declare const PREVIEW_INTERVAL_MS = 500;
/** Only the last Claude reply after the current prompt is eligible. A missing
* prompt anchor is deliberately silent: scrollback could belong to an old turn. */
export declare function claudePanePreview(rendered: string, prompt: string, previous?: string): string;
/** An unbulleted tool-group summary line, e.g. " Read 1 file, ran 1 shell command". */
export declare function claudeToolSummary(line: string): boolean;
/**
* Claude Code's own screen text, never part of a reply: the update and
* restart notices, status lines it marks ✔ / ✗ / ⚠ / ※, tips, and the
* context and auto-accept hints drawn around the input box.
*/
export declare function claudeChrome(line: string): boolean;
/**
* Undoes the terminal's word wrap. Claude draws a paragraph wrapped to the
* pane's width, so the phone would show each wrap as a hard break. A line is
* joined to the next when the next line's first word would not have fitted
* on it, which is exactly what a soft wrap looks like; the widest line
* stands in for the pane width, and below 40 columns nothing is joined. Blank lines, block starts and fenced code
* keep their breaks. `display` is what is joined, line for line: the text
* with Markdown put back, measured by its plain twin.
*/
export declare function unwrapTerminalLines(lines: readonly string[], display?: readonly string[]): string;
/** Claude's spinner line, as structured fields: the verb, the turn's elapsed
* seconds, the token count and its direction, and whether it is thinking.
* "✻ Whirlpooling… (27s · ↓ 2.3k tokens · thinking)". */
export interface ClaudeSpinner {
verb: string;
elapsed?: number;
tokens?: {
count: number;
direction: "up" | "down";
};
thinking: boolean;
/** "thought for 4s": how long the finished thinking took. */
thoughtFor?: number;
}
/** One spinner line, or undefined for anything else. The parenthesis must
* start with a time or say "esc to interrupt", as Claude's does. */
export declare function parseClaudeSpinnerLine(line: string): ClaudeSpinner | undefined;
/** The newest spinner line on screen. */
export declare function claudeSpinner(rendered: string): ClaudeSpinner | undefined;
/** The word Claude's own spinner shows ("✻ Pondering… (12s · esc to interrupt)"). */
export declare function claudeSpinnerVerb(rendered: string): string | undefined;
/** Copilot's reasoning state for the running turn, from the headers it draws
* above each reasoning block ("⌄ Thought for 16s", "Thinking" while it
* streams), newest below the turn's prompt line. Only the header is read:
* the reasoning under it ("│ …") never leaves the Hook. */
export declare function copilotThinking(rendered: string): ClaudeSpinner | undefined;
export declare function readPreviewPane(target: Target): Promise<string>;
/**
* One pane line as Markdown bold. Claude draws a reply's **bold** with SGR 1
* and drops the asterisks, so a plain read loses it and the phone's preview
* would gain it only when the reply lands. Whitespace stays outside the
* markers (Markdown does not close `**a **`); other styles are dropped.
*/
export declare function claudeBoldMarkdown(line: string): string;
/** Older Codex rollouts may carry public text deltas between response items.
* Keep a bounded raw cursor, since history intentionally filters these out. */
export declare class CodexRolloutPreview {
private revision?;
private cursor;
private startedAt?;
private text;
read(file: string): Promise<TranscriptPreview | null>;
}
export declare function readDeltaPreview(target: Target, file?: string, rollout?: CodexRolloutPreview): Promise<TranscriptPreview | null>;
/** Socket-local, ephemeral state. It never writes transcript rows or advances
* the history cursor. Final rows bypass the text throttle and clear atomically. */
export declare class TranscriptPreviewStream {
private readonly target;
private readonly pane;
private readonly delta;
private startedAt?;
private prompt;
private landed;
private ended;
/** Claude's own spinner word for the running turn, sent beside frames as
* `activityVerb` for older phones. */
verb: string | undefined;
/** The whole spinner line for the running turn, sent as `activity`;
* phones without it ignore the field. */
activity: ClaudeSpinner | undefined;
private sentActivity;
private lastRead;
private lastSent;
private current;
private observedLine;
private wasWorking;
/** Replies that already landed as entries: a sidecar or thread a tick
* behind them must not bring the same text back as a preview. */
private readonly completed;
private readonly rollout;
constructor(target: Target, pane?: () => Promise<string>, delta?: (file?: string) => Promise<TranscriptPreview | null>);
observe(entries: Entry[], reset?: boolean): void;
update(status: unknown, file?: string, now?: number): Promise<{
preview: TranscriptPreview | null;
} | undefined>;
}