/** * Footer layout: the two chrome rows, and the `/status` report that spells the * first one out. * * Layout assembles chips and rows; it does not decide how a chip degrades (that * is `footer-budget.ts`) and does not spell a reading (that is * `footer-format.ts`). The contract it does own is *where a signal lives*: the * status row carries health, activity, speed, quota and context, the workspace * row carries metadata, and the counters the status row gave up are answered by * {@link formatStatusReport}. * * @module dsh-ssh-tui/footer-layout */ import { type ColorDepth } from './color-depth.js'; import { type QuotaPeriod, type QuotaSnapshot } from './quota.js'; import { type ThroughputView } from './throughput.js'; import { type SessionStatsRow } from './stats.js'; import type { DisconnectPolicyName } from './transcript-types.js'; import { type ContextPressureView } from './footer-format.js'; import type { FooterActivityKind } from './footer-budget.js'; export declare const WAIT_INDICATOR_MS = 8000; export interface FooterStatsInput { turns: number; steps: number; llmMs: number; toolMs: number; ttftMs: number; ttftSteps: number; decodeMs: number; decodeTokens: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; } /** Stats groups in drop order (last is dropped first when the row is too wide). */ /** * @deprecated Legacy surface, removed in 0.9. * * The status row shows one session total and `/status` prints the counters; this * group list was the previous footer's first row, and the only thing still * composing it is `statsText()` (itself test-only). Kept exported for 0.8.x. */ export declare function footerStatsGroups(stats: FooterStatsInput): string[]; /** * One group in the status strip, with the graphic-only form a narrow terminal * keeps. `priority` orders the losses: 0 is the last thing to go. */ /** * @deprecated Legacy surface, removed in 0.9. * * The default row stopped using this when the footer became the status row * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous * generation's fitting and formatting, kept exported so 0.8.x does not break an * importer. New callers want the budget module's `runtimeStrip`, the meters, or * `formatStatusReport` for the spelled-out numbers. */ export interface FooterChip { id: string; /** Glyph plus text: what a wide terminal shows. */ long: string; /** Glyph only, or `''` for a group that is pure text. */ short: string; priority: number; } /** * Fit an ordered strip into `width` cells, losing text before graphics. * * The old fitter dropped whole groups from the end, so a narrow terminal lost * the ⚠ and the context ring — the two signals that say something is wrong — * while keeping long numeric groups. Two passes now: every chip trades its text * for its glyph in reverse priority order, and only then do whole groups go, * again lowest priority first. The result never exceeds the width. * * Chips may carry SGR accents (the link pips, the ⚠) and the caller may pass a * styled separator, so both the measurement and the final cut are ANSI-aware: * feeding styled text to a plain-text truncator strips the accent's `ESC` and * leaves its `[32m` body on the grid. */ /** * @deprecated Legacy surface, removed in 0.9. * * The default row stopped using this when the footer became the status row * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous * generation's fitting and formatting, kept exported so 0.8.x does not break an * importer. New callers want the budget module's `runtimeStrip`, the meters, or * `formatStatusReport` for the spelled-out numbers. */ export declare function fitFooterChips(chips: readonly FooterChip[], width: number, separator?: string): string; /** * The roster's health as a chip: absent means `/mode` cannot switch presets and * the preset-owned tools are missing. It leads the status row and keeps its glyph * longest, because it is the one group that reports a broken install. */ export declare function footerHealthChip(missing: boolean, color?: boolean, /** Which rows are missing: the 0.1.5 roster, or the 0.1.7 agent plane. */ kind?: 'roster' | 'agent-plane'): FooterChip | undefined; /** * @deprecated Legacy surface, removed in 0.9. * * The default row stopped using this when the footer became the status row * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous * generation's fitting and formatting, kept exported so 0.8.x does not break an * importer. New callers want the budget module's `runtimeStrip`, the meters, or * `formatStatusReport` for the spelled-out numbers. */ export declare function fitFooterStatsLine(chip: string, groups: readonly string[], width: number): string; export interface FooterStatusInput { running: boolean; /** * What the running tools are: the tool's own short name (`edit`, `terminal`, * `search`), the way the transcript's card titles spell it, or a count when * several are open at once. */ toolLabel?: string; planReview: boolean; waitingQuestion: boolean; /** A tool call is stopped on a human yes/no. */ waitingApproval: boolean; compacting: boolean; retry?: { retry: number; maxRetries: number; }; subagents: number; tools: number; planLeftOpen: boolean; planPending: boolean; planActive: boolean; goalPhase?: 'active' | 'paused' | 'blocked'; idleMs: number; /** Reasoning deltas have arrived for the step in flight. */ streamingReasoning?: boolean; /** Visible text has started arriving for the step in flight. */ streamingText?: boolean; /** * Wall clock the current activity began — the open tool call, the running * subagents, the compaction, the step's first delta. The row prints it as * `· 28s`; without one the chip shows the verb alone rather than borrowing a * number that measures something else. */ activityStartedAt?: number; model: string; effort?: string; preset?: string; provider: string; parentModel: string; subModel: string; /** Explicit `/submodel` provider override; undefined means "inherit parent". */ subProvider?: string; /** Explicit `/subeffort` override for subagent children. */ subEffort?: string; quotaCode?: string; quotaPercent?: number; /** Which rolling window `quotaPercent` belongs to: tagged `5Hr`/`1Wk`/`1Mo`. */ quotaPeriod?: QuotaPeriod; /** The provider has a quota surface but no reading yet: paint `░░░░░░░░ ?%`. */ quotaUnknown?: boolean; contextChip?: string; balanceText?: string; /** * The subscription badge for the row's last part (`OpenCode-GO`, * `CommandCode-GOAT`), for the routes that have one. See `planRouteBadge`. */ planBadge?: string; search?: { index: number; total: number; }; foldedInput: boolean; multiLineInput: boolean; queued: number; cwdLabel?: string; compactView?: boolean; } export declare function footerActivity(input: FooterStatusInput): { kind: FooterActivityKind; text: string; }; /** * Paint the `sub:` chip on an otherwise muted status line. * * Only a *foreign* route is accented: cyan when `/submodel` pinned a supplier * the parent is not on. A child that follows the parent (or that was pinned * back onto the parent's own provider) keeps the identity row's mute — the * accent means "this route is not your parent's", and spending it on every * child made the following case look pinned. * * The mute (`90`) is reopened after the chip so the rest of the identity row * stays dim. */ export declare function paintFooterSubagentChip(line: string, chip: string, foreign: boolean, muteSgr?: string, depth?: ColorDepth, identitySgr?: string): string; export declare function footerSubagentForeign(input: Pick): boolean; /** * The workspace row is **ordered by priority**, and the fitter drops from the * right - one mechanism, not two. * * The row used to be ordered by what a footer has always shown (preset, cwd, * route) while the fitter cut the right-most part, so a narrow terminal deleted a * queue count while keeping a preset the header already prints. Sorting the parts * by how much they are worth puts both rules in one order: * * 1. `[compact]` - the view mode, the one marker nothing else repeats; * 2. `dir:...` - where this session is working, which the header never says; * 3. `bal ...` - a reading the row must not lose: on a balance-only provider * (DeepSeek) it is the only one, so it outlives the configuration behind it; * 4. `search i/n`, the input's shape, `queued n` - *state*: what the session is * doing right now, which is what the reader needs mid-turn; * 5. `sub:...` - configuration, inherited by every child and changed only by * `/submodel`, so it sits after the state; and * 6. the subscription badge (`OpenCode-GO`, `CommandCode-GOAT`) - context rather * than action, so it closes the row and is the first part a narrow row gives * up. Losing the last part reads as "the row got shorter", never as a hole in * the middle of it. * * The preset is deliberately absent: the header prints it permanently. */ /** Options for {@link footerIdentityParts}. */ export interface FooterIdentityOptions { /** * Leave the provider/model/effort out. * * The header paints the live route permanently, so on the workspace row the * parent model is the one piece of information the reader already has — and * the piece that used to push the `sub:` chip off a 100-column row. */ omitModel?: boolean; } /** * The workspace row: what this session is, not what it is doing. * * Quota and context used to live here and now own capacity meters on the row * above, so they are deliberately absent: a number painted twice is a number the * reader has to check twice. What is left is the configuration the header does * not carry — the preset, the working directory, the child route, and the * input's own transient state. */ export declare function footerIdentityParts(input: FooterStatusInput, options?: FooterIdentityOptions): string[]; /** * Fit the workspace row under `width`. * * The metadata is secondary by construction: it follows the activity label at a * dimmer intensity and is dropped from the right, one part at a time, rather * than being allowed to wrap into the status row above it. */ export declare function fitFooterStatusLine(activity: string, identity: readonly string[], width: number): string; export interface StatusReportInput { sessionId: string; pluginVersion: string; provider: string; model: string; effort?: string; agentStatus: string; preset: string; activeSubagents: number; plan: 'off' | 'pending' | 'on'; paint: string; disconnect?: DisconnectPolicyName; waitingQuestions: number; quota?: QuotaSnapshot; context?: ContextPressureView; parentModel?: string; subProvider?: string; subModel: string; cwd?: string; /** Session counters: the block the default row gave up (B-footer pass). */ stats?: SessionStatsRow; /** Throughput accounting: exact rate and the running estimate. */ throughput?: ThroughputView; } /** * Lines printed by `/status` — SSH first-boot diagnostics, no extra command. * * This is where the status row's removed telemetry lives. Turns, steps, model * time, tool time, cache hit and the input/output breakdown are session * analytics: they answer no question a live operator is asking, which is why * they left the default row, and their disappearance would be information loss — * so `/status` prints all of them, plus the throughput accounting, in the shapes * the row used to use. */ export declare function formatStatusReport(input: StatusReportInput): string[]; /** * The stats block `/status` prints: everything the default row gave up. * * `tokens/sec` here is the exact per-step rate (`decodeTokens / decodeMs`), not * the running estimate, and the line says which span it covers: the last step * and the session so far are different questions and only one of them is a * benchmark. */ export declare function formatStatusStats(stats: SessionStatsRow): string[]; /** One line: what the throughput number means, and which span it covers. */ export declare function formatStatusThroughput(throughput: ThroughputView): string;