/** * Responsive budget policy: how much of the row survives at which width. * * The row is one ordered list of chips, and this module owns the *degradation * ladder* every chip answers to — a single integer level, tried from 0 upward, * where each step removes cells in the order of the information priority the row * was designed around. Nothing outside this module may decide that a chip is * dropped, shortened, or spelled differently. * * @module dsh-ssh-tui/footer-budget */ import { type ColorDepth } from './color-depth.js'; import { type PaintLinkKind } from './paint.js'; import type { QuotaPeriod } from './quota.js'; import type { ThroughputView } from './throughput.js'; import { type ContextPressureView } from './footer-format.js'; export type FooterActivityKind = 'plan-review' | 'waiting' | 'approval' | 'compacting' | 'retry' | 'agent' | 'tools' | 'plan-open' | 'plan-pending' | 'goal' | 'thinking' | 'reply' | 'waiting-llm' | 'idle'; /** * How the default row paints its link. * * Deliberately the same four-pip vocabulary `formatLinkQualityChip` established: * the thresholds behind it (`linkQualityOf`) were validated against real SSH * probes, and a UI pass that re-derived its own numbers would put two * contradictory opinions about the same link on the same screen. */ export interface FooterStripLink { kind: PaintLinkKind; intervalMs: number; /** Measured round-trip, when the CSI-6n probe answered. */ rttMs?: number; probed: boolean; } /** * The activity chip's input: the verb, and when it started. * * Deliberately *not* the subject — not the command being run, not the file being * edited, not the path being searched. The status row answers "what is happening" * (`终端`, `edit`, `thinking`); *which* command, on *which* file, is the * transcript card's job, one line up, where the reader can actually read it. A * command in the chip also spent the row's cells on the least load-bearing thing * on it: a `pytest -x tests/ --maxfail=1` pushed the quota and context meters * towards the drop edge, and `/status`-like detail in a status row is exactly the * telemetry dump this row was rebuilt to stop being. */ export interface FooterStripActivity { kind: FooterActivityKind; text: string; /** Wall clock the action started; drives the `· 28s` suffix. */ startedAt?: number; now?: number; } export interface FooterStripQuota { /** 0–100, remaining. `undefined` paints the empty bar with `?%`. */ remainingPercent?: number; period?: QuotaPeriod; } /** Everything the default status row paints, in the order a glance reads it. */ export interface FooterStripInput { link: FooterStripLink; activity: FooterStripActivity; throughput?: ThroughputView; running: boolean; quota?: FooterStripQuota; context?: ContextPressureView; /** * Session-cumulative tokens, **only** when the harness published the total * itself. Absent means the chip is not painted at all: a total assembled from * the billed parts is a different accounting, and `/status` is where the two are * told apart (`sessionTokenTotal` returns which one it had). */ totalTokens?: number; /** * The install warning that leads the row, when the profile is missing rows. * * It is on the *same* ladder as everything else rather than being fitted around * it: it used to be composed into a chip strip by `fitFooterChips` and the rest * of the row fitted into what was left, which meant two convergence algorithms * on one line and a width hand-computed at the call site. It keeps the highest * priority, so it is still the last thing a narrow terminal gives up, and only * its text degrades — to the glyph, and off the row only at the very end. */ warning?: { long: string; short: string; }; /** Whether the terminal must be painted with ASCII glyphs. */ ascii?: boolean; /** Colour depth the accents are downgraded for. */ depth?: ColorDepth; /** Paint accents at all. False is the `no-color` terminal. */ color?: boolean; } /** * A chip on the default row: one signal, painted at one of nine densities. * * The level is a *degradation ladder*, not a size class. Level 0 is the full * form a 160-column terminal shows; each step buys cells from the least * load-bearing part of the row. The order is the whole design: the session * total goes first, then the activity's subject, then the meters give up cells * before they give up their number, and the link keeps its dots almost to the * end — because those dots are the one component that says whether the session * is alive. A narrow terminal ends up *quieter*, never denser. */ export interface FooterStripChip { id: string; /** Component order. Lower is more load-bearing. */ priority: number; /** The chip at one degradation level; `''` drops the chip at that level. */ render: (level: number) => string; } /** * The deepest degradation level any chip understands. * * Sixteen levels, one per cell of budget, in the priority order the row was * designed around. A coarser ladder left the 72–99 range showing the same row as * a 100-column terminal, which is the failure mode this exists to avoid: the * narrow terminal must get *less*, not merely a compressed copy. */ export declare const FOOTER_STRIP_MAX_LEVEL = 17; /** The default row's group separator, matching the pre-strip footer. */ export declare const FOOTER_STRIP_SEPARATOR = " \u2502 "; /** * The activity chip's text: `⠹ edit · 28s`, `⠹ thinking · 18s`, `idle`. * * The spinner is passed in rather than computed here so the row has exactly one * clock (`SPINNER` in `tui.ts`), which is what keeps the glyph in step with the * transcript's own live rows. * @param activity - the current operational state. * @param spinner - the frame to show in front of a working state, if any. * @param level - degradation level; the glyph and the clock go before the verb. */ export declare function activityMeter(activity: FooterStripActivity, spinner: string, level?: number): string; /** * Build the default status row: the ordered chips, degraded to fit `width`. * * Order is the information priority the row is built around — link health, what * is happening, how fast tokens are arriving, what quota is left, how full the * context is, and finally how big the session has grown. Turns, steps, model * time, tool time and cache hit left this row on purpose: they are session * analytics, they answer no question a live operator is asking, and they are all * still in `/status`. * * Nothing here paints threshold colour on throughput: the harness publishes no * performance classification, and inventing one would paint a healthy reasoning * model red for being deliberate. * @param input - the signals, already resolved by the caller. * @param width - cells available on the row. * @param separator - the (already accented) group separator. * @param spinner - current animation frame for working states. * @returns the row, never wider than `width`. */ export declare function runtimeStrip(input: FooterStripInput, width: number, separator?: string, spinner?: string): string; /** * What a Screen's compact runtime strip reports, in the order it is read. * * A Screen replaces the whole workspace, so the footer's two rows are gone with * it. This is the subset that has to survive: the reader must still be able to * answer "is the agent running", "is anything waiting", "is the link alive". It * deliberately carries no throughput, no session total, no cwd and no subagent * metadata — those are workspace facts about *work*, and a Screen is a place you * visit while the work continues (AD-8). */ export interface ScreenStripInput { activity: FooterStripActivity; /** Human interactions waiting behind this screen: a question or an approval. */ waiting: boolean; /** Messages the reader queued for the agent. */ queued: number; /** Agent-initiated questions the reader has not answered yet. */ queuedQuestions: number; link: FooterStripLink; context?: ContextPressureView; quota?: FooterStripQuota; ascii?: boolean; color?: boolean; depth?: ColorDepth; } /** * The Screen strip: the status row's own chips, a subset, fitted from the back. * * Rendered with the *same* chip functions the status row uses (`linkChipText`, * `activityMeter`, `contextChip`, `quotaChip`), so a Screen cannot drift into a * second visual language or a second reading of the same state. Fitting is the * opposite of the workspace row's: that row spends its cells on the tail first and * degrades everything together, while the Screen strip holds its highest-priority * group and drops whole groups from the end (quota, context, link, queued) — the * reader is here for the report, and a strip that re-read as it scrolled would be * worse than a shorter one. * @param input - the runtime facts, all read from the same fields the footer reads. * @param width - cells available on the row. * @param separator - the (already accented) group separator. * @param spinner - current animation frame for working states. * @returns the row, never wider than `width`. */ export declare function screenRuntimeStrip(input: ScreenStripInput, width: number, separator?: string, spinner?: string): string;