dsh-ssh-tui 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.en.md +118 -15
  3. package/README.md +78 -13
  4. package/docs/release-notes-0.7.4.md +91 -0
  5. package/docs/release-notes-0.8.0.md +56 -0
  6. package/docs/release-notes-0.8.1.md +79 -0
  7. package/docs/release-notes-0.8.2-rc.1.md +159 -0
  8. package/docs/release-notes-0.8.2.md +62 -0
  9. package/docs/screenshots/reconnect.gif +0 -0
  10. package/docs/terminals.md +23 -0
  11. package/docs/windows.md +13 -0
  12. package/lib/approval-state.js +148 -0
  13. package/lib/approval-state.js.map +1 -0
  14. package/lib/auth-failure.js +38 -0
  15. package/lib/auth-failure.js.map +1 -1
  16. package/lib/color-depth.js +26 -0
  17. package/lib/color-depth.js.map +1 -1
  18. package/lib/copy-text.js +30 -8
  19. package/lib/copy-text.js.map +1 -1
  20. package/lib/dialogs.js +81 -0
  21. package/lib/dialogs.js.map +1 -1
  22. package/lib/display-sock.js +138 -23
  23. package/lib/display-sock.js.map +1 -1
  24. package/lib/footer-accents.js +28 -0
  25. package/lib/footer-accents.js.map +1 -0
  26. package/lib/footer-budget.js +328 -0
  27. package/lib/footer-budget.js.map +1 -0
  28. package/lib/footer-format.js +433 -0
  29. package/lib/footer-format.js.map +1 -0
  30. package/lib/footer-layout.js +407 -0
  31. package/lib/footer-layout.js.map +1 -0
  32. package/lib/footer-meters.js +185 -0
  33. package/lib/footer-meters.js.map +1 -0
  34. package/lib/footer.js +19 -558
  35. package/lib/footer.js.map +1 -1
  36. package/lib/i18n/en.js +65 -5
  37. package/lib/i18n/en.js.map +1 -1
  38. package/lib/i18n/zh.js +67 -5
  39. package/lib/i18n/zh.js.map +1 -1
  40. package/lib/paint.js +53 -8
  41. package/lib/paint.js.map +1 -1
  42. package/lib/plan-projection.js +291 -0
  43. package/lib/plan-projection.js.map +1 -0
  44. package/lib/plan.js +50 -0
  45. package/lib/plan.js.map +1 -1
  46. package/lib/question-state.js +83 -0
  47. package/lib/question-state.js.map +1 -0
  48. package/lib/representation.js +283 -0
  49. package/lib/representation.js.map +1 -0
  50. package/lib/screen.js +105 -0
  51. package/lib/screen.js.map +1 -0
  52. package/lib/selection.js +5 -3
  53. package/lib/selection.js.map +1 -1
  54. package/lib/startup.js +49 -0
  55. package/lib/startup.js.map +1 -1
  56. package/lib/stats.js +101 -3
  57. package/lib/stats.js.map +1 -1
  58. package/lib/term-text.js +154 -55
  59. package/lib/term-text.js.map +1 -1
  60. package/lib/theme.js +20 -3
  61. package/lib/theme.js.map +1 -1
  62. package/lib/throughput.js +305 -0
  63. package/lib/throughput.js.map +1 -0
  64. package/lib/tool-present.js +57 -7
  65. package/lib/tool-present.js.map +1 -1
  66. package/lib/tui.js +3776 -1105
  67. package/lib/tui.js.map +1 -1
  68. package/lib/types/approval-state.d.ts +75 -0
  69. package/lib/types/auth-failure.d.ts +25 -0
  70. package/lib/types/color-depth.d.ts +21 -0
  71. package/lib/types/copy-text.d.ts +18 -1
  72. package/lib/types/dialogs.d.ts +111 -1
  73. package/lib/types/display-sock.d.ts +15 -0
  74. package/lib/types/footer-accents.d.ts +22 -0
  75. package/lib/types/footer-budget.d.ts +204 -0
  76. package/lib/types/footer-format.d.ts +221 -0
  77. package/lib/types/footer-layout.d.ts +292 -0
  78. package/lib/types/footer-meters.d.ts +151 -0
  79. package/lib/types/footer.d.ts +19 -269
  80. package/lib/types/paint.d.ts +48 -0
  81. package/lib/types/plan-projection.d.ts +162 -0
  82. package/lib/types/plan.d.ts +13 -0
  83. package/lib/types/question-state.d.ts +96 -0
  84. package/lib/types/representation.d.ts +498 -0
  85. package/lib/types/screen.d.ts +126 -0
  86. package/lib/types/startup.d.ts +24 -0
  87. package/lib/types/stats.d.ts +64 -1
  88. package/lib/types/term-text.d.ts +45 -1
  89. package/lib/types/throughput.d.ts +163 -0
  90. package/lib/types/tool-present.d.ts +41 -0
  91. package/lib/types/transcript-types.d.ts +63 -2
  92. package/lib/types/tui.d.ts +862 -11
  93. package/package.json +16 -6
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Presentation formatting: a value in, text out.
3
+ *
4
+ * Pure functions that know nothing about rows or widths, and exist as their own
5
+ * module because the *same* reading is painted in several places (the status row,
6
+ * the workspace row, `/status`, a transcript chip) and those call sites must not
7
+ * each grow their own spelling of it.
8
+ *
9
+ * It also owns the context-pressure reading — parsing DSH's `contextPressure`,
10
+ * its bands, and its alert text. That is a deliberate exception to "formatting
11
+ * only": the chip, the status line and the thresholds are one decision about one
12
+ * number, and splitting them would let the painted level drift from the level
13
+ * compaction acts on.
14
+ *
15
+ * @module dsh-ssh-tui/footer-format
16
+ */
17
+ import { type QuotaPeriod, type QuotaSource } from './quota.js';
18
+ /** Compact token count, matching the web stats line (517 / 12.2K / 1.2M). */
19
+ export declare function formatTokens(n: number): string;
20
+ /**
21
+ * Prompt occupancy of the next request, from DSH `contextPressure`.
22
+ * Provider-agnostic: uses the routed model's advertised window, not a
23
+ * hardcoded xAI size. Compaction-basic still owns in-turn pressure at 80%.
24
+ */
25
+ export declare const CONTEXT_PRESSURE_WARN_RATIO = 0.8;
26
+ export declare const CONTEXT_PRESSURE_DANGER_RATIO = 0.95;
27
+ /** Idle auto-compact starts here so recovery finishes before the 80% in-turn trigger. */
28
+ export declare const CONTEXT_IDLE_COMPACT_RATIO = 0.72;
29
+ export interface ContextPressureSample {
30
+ usedTokens: number;
31
+ contextWindow: number;
32
+ }
33
+ export interface ContextPressureView {
34
+ usedTokens: number;
35
+ contextWindow: number;
36
+ percent: number;
37
+ level: 'ok' | 'warn' | 'danger';
38
+ }
39
+ /** Prompt-side occupancy of one usage sample: uncached input plus cache traffic. */
40
+ export declare function promptPressureTokens(usage: {
41
+ inputTokens: number;
42
+ cacheReadTokens?: number;
43
+ cacheWriteTokens?: number;
44
+ }): number;
45
+ /** Prefer the next-request projection; fall back to last-request pressure. */
46
+ export declare function contextPressureUsedTokens(pressure: {
47
+ projectedTokens?: number;
48
+ pressureTokens?: number;
49
+ } | undefined): number | undefined;
50
+ export declare function parseContextPressure(value: unknown): ContextPressureSample | undefined;
51
+ export declare function contextPressureView(sample: ContextPressureSample): ContextPressureView;
52
+ /**
53
+ * 8-segment Braille ring. Empty `⣀`; full `⣿`. Width is always 1 cell.
54
+ * Index is `ceil(percent / 12.5)` clamped to 0..8.
55
+ */
56
+ export declare const CONTEXT_RING_EMPTY = "\u28C0";
57
+ export declare const CONTEXT_RING_SEGMENTS: readonly ["⣀", "⠉", "⠋", "⠛", "⠞", "⠟", "⠿", "⡿", "⣿"];
58
+ /** The full cell of the same Braille family, for bars built like the ring. */
59
+ export declare const CONTEXT_RING_FULL = "\u28FF";
60
+ export declare function formatContextPressureRing(percent: number): string;
61
+ export declare function contextPressureRingColor(level: ContextPressureView['level']): string;
62
+ export declare function formatContextPressureChip(view: ContextPressureView, color?: boolean): string;
63
+ export declare function formatContextPressureStatusLine(view: ContextPressureView | undefined): string;
64
+ export declare function contextPressureAlertText(view: ContextPressureView): string;
65
+ export declare function shouldIdleAutoCompact(view: ContextPressureView | undefined): boolean;
66
+ /**
67
+ * Compact elapsed time for the activity chip: `28s`, `3m12s`, `1h04m`.
68
+ *
69
+ * Distinct from {@link formatDuration} on purpose: that one is a session
70
+ * statistic (`45.2s`), where a decimal is informative, while this one ticks
71
+ * every second beside a spinner and should move in whole units.
72
+ */
73
+ export declare function formatElapsedShort(ms: number): string;
74
+ /** Compact duration, matching the web stats line (45.2s / 2m42s). */
75
+ export declare function formatDuration(ms: number): string;
76
+ /**
77
+ * @deprecated Legacy surface, removed in 0.9.
78
+ *
79
+ * The default row stopped using this when the footer became the status row
80
+ * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous
81
+ * generation's fitting and formatting, kept exported so 0.8.x does not break an
82
+ * importer. New callers want the budget module's `runtimeStrip`, the meters, or
83
+ * `formatStatusReport` for the spelled-out numbers.
84
+ */
85
+ export declare function formatTokensPerSecond(tokensPerSecond: number): string;
86
+ export declare function providerShortCode(provider: string): string;
87
+ /** Short remaining-quota bar: 8 pips, filled from the left. */
88
+ export declare function formatQuotaBar(remainingPercent: number, width?: number): string;
89
+ /**
90
+ * `sub:<model>` chip for the identity row: `sub:grok-4.5`, or
91
+ * `sub:grok-4.5(xhigh)` when an explicit `/subeffort` is set.
92
+ *
93
+ * It shows the model name only, like the parent's own chip, unless `/submodel`
94
+ * pinned a different provider — then `sub:xai/grok-4.5` so the foreign route
95
+ * is visible on the identity row as well as the recolored chip.
96
+ */
97
+ export declare function subagentRouteLabel(model: string, provider?: string, effort?: string): string;
98
+ /**
99
+ * The model as the footer shows it: the model's own name, without a
100
+ * `provider/model` route prefix.
101
+ *
102
+ * Some providers carry the vendor in the model id (`xai/grok-4.6`,
103
+ * `deepseek-official/deepseek-v4-flash`). The route is worth a row in the header
104
+ * and in `/status`; in the status line it spent cells on something the reader
105
+ * already knows, and it pushed the quota badge towards the drop edge.
106
+ * `sub:` routes keep their provider on purpose — that is a *different* route
107
+ * from the parent's, and hiding it would make the two indistinguishable.
108
+ */
109
+ export declare function shortModelName(model: string): string;
110
+ /**
111
+ * `SuperGrok 5Hr ███████░ 82%` — short plan badge, the window the number belongs
112
+ * to, then the bar.
113
+ *
114
+ * The tags are fixed technical labels (`5Hr` / `1Wk` / `1Mo`) rather than
115
+ * localized words: they sit in a footer that is already fighting for cells, and
116
+ * a window tag that changes with the locale would be unreadable in a screenshot
117
+ * or a bug report. Everything else about the window (label, reset time, detail)
118
+ * stays in `/quota`.
119
+ */
120
+ /**
121
+ * @deprecated Legacy surface, removed in 0.9.
122
+ *
123
+ * The default row stopped using this when the footer became the status row
124
+ * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous
125
+ * generation's fitting and formatting, kept exported so 0.8.x does not break an
126
+ * importer. New callers want the budget module's `runtimeStrip`, the meters, or
127
+ * `formatStatusReport` for the spelled-out numbers.
128
+ */
129
+ export declare function formatFooterQuota(percent: number, code?: string, period?: QuotaPeriod): string;
130
+ /**
131
+ * The quota widget before the first reading arrives, or while the provider
132
+ * cannot be reached: an empty bar and a question mark.
133
+ *
134
+ * Deliberately not `0%`: an unreachable quota API is not a used-up quota, and a
135
+ * number the footer cannot back up is worse than no number. The bar is there
136
+ * from the first frame so the row does not jump when the reading lands.
137
+ */
138
+ /**
139
+ * @deprecated Legacy surface, removed in 0.9.
140
+ *
141
+ * The default row stopped using this when the footer became the status row
142
+ * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous
143
+ * generation's fitting and formatting, kept exported so 0.8.x does not break an
144
+ * importer. New callers want the budget module's `runtimeStrip`, the meters, or
145
+ * `formatStatusReport` for the spelled-out numbers.
146
+ */
147
+ export declare function formatQuotaUnknown(): string;
148
+ /** Fixed-width tag for the window a footer quota number belongs to. */
149
+ export declare function quotaWindowTag(period: QuotaPeriod | undefined): string;
150
+ /**
151
+ * The window tag as an out-of-room row spells it: `5Hr` → `5h`, `1Wk` → `1w`.
152
+ *
153
+ * Deliberately not `toLowerCase()`: the tag encodes two facts in three cells
154
+ * (how many, and which unit), and the unit letter is the one worth keeping —
155
+ * `5hr` costs a cell to repeat that it is hours, which the `h` already said.
156
+ */
157
+ export declare function quotaWindowTagShort(period: QuotaPeriod | undefined): string;
158
+ /**
159
+ * The badge the footer shows instead of the billing plan's own label:
160
+ * `SuperGrok`, `OC·GO` (OpenCode Go), `CC·GOAT` (Command Code Goat).
161
+ *
162
+ * The long names live in `/quota`; this one has to survive next to a model id.
163
+ * Without a `source` (a hand-built snapshot) the plan label is matched instead,
164
+ * so old callers and tests keep a sensible badge.
165
+ */
166
+ export declare function shortQuotaPlanName(snapshot: {
167
+ plan: string;
168
+ source?: QuotaSource;
169
+ }): string;
170
+ /**
171
+ * The subscription a session is running on, spelled out for the footer's last
172
+ * part: `OpenCode-GO`, `CommandCode-GOAT`.
173
+ *
174
+ * Only the two generations that carry a *named subscription* get one. SuperGrok
175
+ * already says so in its quota chip's window tag, and DeepSeek is metered (its
176
+ * balance line is the reading), so neither claims a badge here. The spelling is
177
+ * longer than the identity row's old `OC·GO` / `CC·GOAT` on purpose: this one sits
178
+ * at the very end of the row where the least load-bearing part goes, and a reader
179
+ * who still has it on screen reads a plan name rather than an abbreviation.
180
+ *
181
+ * The provider id alone is enough for the Go route; Command Code's *tier* comes
182
+ * from the billing reply, so before the first reading lands the badge names the
183
+ * vendor without claiming a tier it has not seen.
184
+ * @param provider - the live route's provider id.
185
+ * @param snapshot - the quota reading, when it belongs to that provider.
186
+ * @returns the badge, or undefined when this route has none.
187
+ */
188
+ export declare function planRouteBadge(provider: string, snapshot?: {
189
+ plan: string;
190
+ source?: QuotaSource;
191
+ }): string | undefined;
192
+ /**
193
+ * Drop the Go / SuperGrok plan name from a quota identity part, keeping the
194
+ * remaining-percent bar. Returns true when a part was rewritten.
195
+ */
196
+ /**
197
+ * @deprecated Legacy surface, removed in 0.9.
198
+ *
199
+ * The default row stopped using this when the footer became the status row
200
+ * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous
201
+ * generation's fitting and formatting, kept exported so 0.8.x does not break an
202
+ * importer. New callers want the budget module's `runtimeStrip`, the meters, or
203
+ * `formatStatusReport` for the spelled-out numbers.
204
+ */
205
+ export declare function dropFooterQuotaPlanName(parts: string[]): boolean;
206
+ /** Human-facing kind for a live LLM route. */
207
+ export declare function describeProviderRoute(provider: string): {
208
+ kind: string;
209
+ short: string;
210
+ };
211
+ /** Routes that authenticate without a harness API-key credential. */
212
+ export declare function providerUsesLocalOAuth(provider: string): boolean;
213
+ /**
214
+ * Whether a provider has a quota surface at all, without asking it.
215
+ *
216
+ * The footer paints a quota widget from the first frame — an empty bar and `?%`
217
+ * until a reading arrives — so it needs to tell "this provider will have one"
218
+ * from "this provider reports a balance instead" (DeepSeek) or nothing at all.
219
+ * Every branch is a synchronous settings read, so a repaint never does IO.
220
+ */
221
+ export declare function providerHasQuotaSurface(provider: string, llmPiAiSection: unknown): boolean;
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Footer layout: the two chrome rows, and the `/status` report that spells the
3
+ * first one out.
4
+ *
5
+ * Layout assembles chips and rows; it does not decide how a chip degrades (that
6
+ * is `footer-budget.ts`) and does not spell a reading (that is
7
+ * `footer-format.ts`). The contract it does own is *where a signal lives*: the
8
+ * status row carries health, activity, speed, quota and context, the workspace
9
+ * row carries metadata, and the counters the status row gave up are answered by
10
+ * {@link formatStatusReport}.
11
+ *
12
+ * @module dsh-ssh-tui/footer-layout
13
+ */
14
+ import { type ColorDepth } from './color-depth.js';
15
+ import { type QuotaPeriod, type QuotaSnapshot } from './quota.js';
16
+ import { type ThroughputView } from './throughput.js';
17
+ import { type SessionStatsRow } from './stats.js';
18
+ import type { DisconnectPolicyName } from './transcript-types.js';
19
+ import { type ContextPressureView } from './footer-format.js';
20
+ import type { FooterActivityKind } from './footer-budget.js';
21
+ export declare const WAIT_INDICATOR_MS = 8000;
22
+ export interface FooterStatsInput {
23
+ turns: number;
24
+ steps: number;
25
+ llmMs: number;
26
+ toolMs: number;
27
+ ttftMs: number;
28
+ ttftSteps: number;
29
+ decodeMs: number;
30
+ decodeTokens: number;
31
+ inputTokens: number;
32
+ outputTokens: number;
33
+ cacheReadTokens: number;
34
+ cacheWriteTokens: number;
35
+ }
36
+ /** Stats groups in drop order (last is dropped first when the row is too wide). */
37
+ /**
38
+ * @deprecated Legacy surface, removed in 0.9.
39
+ *
40
+ * The status row shows one session total and `/status` prints the counters; this
41
+ * group list was the previous footer's first row, and the only thing still
42
+ * composing it is `statsText()` (itself test-only). Kept exported for 0.8.x.
43
+ */
44
+ export declare function footerStatsGroups(stats: FooterStatsInput): string[];
45
+ /**
46
+ * One group in the status strip, with the graphic-only form a narrow terminal
47
+ * keeps. `priority` orders the losses: 0 is the last thing to go.
48
+ */
49
+ /**
50
+ * @deprecated Legacy surface, removed in 0.9.
51
+ *
52
+ * The default row stopped using this when the footer became the status row
53
+ * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous
54
+ * generation's fitting and formatting, kept exported so 0.8.x does not break an
55
+ * importer. New callers want the budget module's `runtimeStrip`, the meters, or
56
+ * `formatStatusReport` for the spelled-out numbers.
57
+ */
58
+ export interface FooterChip {
59
+ id: string;
60
+ /** Glyph plus text: what a wide terminal shows. */
61
+ long: string;
62
+ /** Glyph only, or `''` for a group that is pure text. */
63
+ short: string;
64
+ priority: number;
65
+ }
66
+ /**
67
+ * Fit an ordered strip into `width` cells, losing text before graphics.
68
+ *
69
+ * The old fitter dropped whole groups from the end, so a narrow terminal lost
70
+ * the ⚠ and the context ring — the two signals that say something is wrong —
71
+ * while keeping long numeric groups. Two passes now: every chip trades its text
72
+ * for its glyph in reverse priority order, and only then do whole groups go,
73
+ * again lowest priority first. The result never exceeds the width.
74
+ *
75
+ * Chips may carry SGR accents (the link pips, the ⚠) and the caller may pass a
76
+ * styled separator, so both the measurement and the final cut are ANSI-aware:
77
+ * feeding styled text to a plain-text truncator strips the accent's `ESC` and
78
+ * leaves its `[32m` body on the grid.
79
+ */
80
+ /**
81
+ * @deprecated Legacy surface, removed in 0.9.
82
+ *
83
+ * The default row stopped using this when the footer became the status row
84
+ * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous
85
+ * generation's fitting and formatting, kept exported so 0.8.x does not break an
86
+ * importer. New callers want the budget module's `runtimeStrip`, the meters, or
87
+ * `formatStatusReport` for the spelled-out numbers.
88
+ */
89
+ export declare function fitFooterChips(chips: readonly FooterChip[], width: number, separator?: string): string;
90
+ /**
91
+ * The roster's health as a chip: absent means `/mode` cannot switch presets and
92
+ * the preset-owned tools are missing. It leads the status row and keeps its glyph
93
+ * longest, because it is the one group that reports a broken install.
94
+ */
95
+ export declare function footerHealthChip(missing: boolean, color?: boolean,
96
+ /** Which rows are missing: the 0.1.5 roster, or the 0.1.7 agent plane. */
97
+ kind?: 'roster' | 'agent-plane'): FooterChip | undefined;
98
+ /**
99
+ * @deprecated Legacy surface, removed in 0.9.
100
+ *
101
+ * The default row stopped using this when the footer became the status row
102
+ * (`docs/plans/footer-debt.md`, D-7): what is left of it is the previous
103
+ * generation's fitting and formatting, kept exported so 0.8.x does not break an
104
+ * importer. New callers want the budget module's `runtimeStrip`, the meters, or
105
+ * `formatStatusReport` for the spelled-out numbers.
106
+ */
107
+ export declare function fitFooterStatsLine(chip: string, groups: readonly string[], width: number): string;
108
+ export interface FooterStatusInput {
109
+ running: boolean;
110
+ /**
111
+ * What the running tools are: the tool's own short name (`edit`, `terminal`,
112
+ * `search`), the way the transcript's card titles spell it, or a count when
113
+ * several are open at once.
114
+ */
115
+ toolLabel?: string;
116
+ planReview: boolean;
117
+ waitingQuestion: boolean;
118
+ /** A tool call is stopped on a human yes/no. */
119
+ waitingApproval: boolean;
120
+ compacting: boolean;
121
+ retry?: {
122
+ retry: number;
123
+ maxRetries: number;
124
+ };
125
+ subagents: number;
126
+ tools: number;
127
+ planLeftOpen: boolean;
128
+ planPending: boolean;
129
+ planActive: boolean;
130
+ goalPhase?: 'active' | 'paused' | 'blocked';
131
+ idleMs: number;
132
+ /** Reasoning deltas have arrived for the step in flight. */
133
+ streamingReasoning?: boolean;
134
+ /** Visible text has started arriving for the step in flight. */
135
+ streamingText?: boolean;
136
+ /**
137
+ * Wall clock the current activity began — the open tool call, the running
138
+ * subagents, the compaction, the step's first delta. The row prints it as
139
+ * `· 28s`; without one the chip shows the verb alone rather than borrowing a
140
+ * number that measures something else.
141
+ */
142
+ activityStartedAt?: number;
143
+ model: string;
144
+ effort?: string;
145
+ preset?: string;
146
+ provider: string;
147
+ parentModel: string;
148
+ subModel: string;
149
+ /** Explicit `/submodel` provider override; undefined means "inherit parent". */
150
+ subProvider?: string;
151
+ /** Explicit `/subeffort` override for subagent children. */
152
+ subEffort?: string;
153
+ quotaCode?: string;
154
+ quotaPercent?: number;
155
+ /** Which rolling window `quotaPercent` belongs to: tagged `5Hr`/`1Wk`/`1Mo`. */
156
+ quotaPeriod?: QuotaPeriod;
157
+ /** The provider has a quota surface but no reading yet: paint `░░░░░░░░ ?%`. */
158
+ quotaUnknown?: boolean;
159
+ contextChip?: string;
160
+ balanceText?: string;
161
+ /**
162
+ * The subscription badge for the row's last part (`OpenCode-GO`,
163
+ * `CommandCode-GOAT`), for the routes that have one. See `planRouteBadge`.
164
+ */
165
+ planBadge?: string;
166
+ search?: {
167
+ index: number;
168
+ total: number;
169
+ };
170
+ foldedInput: boolean;
171
+ multiLineInput: boolean;
172
+ queued: number;
173
+ cwdLabel?: string;
174
+ compactView?: boolean;
175
+ }
176
+ export declare function footerActivity(input: FooterStatusInput): {
177
+ kind: FooterActivityKind;
178
+ text: string;
179
+ };
180
+ /**
181
+ * Paint the `sub:` chip on an otherwise muted status line.
182
+ *
183
+ * Only a *foreign* route is accented: cyan when `/submodel` pinned a supplier
184
+ * the parent is not on. A child that follows the parent (or that was pinned
185
+ * back onto the parent's own provider) keeps the identity row's mute — the
186
+ * accent means "this route is not your parent's", and spending it on every
187
+ * child made the following case look pinned.
188
+ *
189
+ * The mute (`90`) is reopened after the chip so the rest of the identity row
190
+ * stays dim.
191
+ */
192
+ export declare function paintFooterSubagentChip(line: string, chip: string, foreign: boolean, muteSgr?: string, depth?: ColorDepth, identitySgr?: string): string;
193
+ export declare function footerSubagentForeign(input: Pick<FooterStatusInput, 'provider' | 'subProvider'>): boolean;
194
+ /**
195
+ * The workspace row is **ordered by priority**, and the fitter drops from the
196
+ * right - one mechanism, not two.
197
+ *
198
+ * The row used to be ordered by what a footer has always shown (preset, cwd,
199
+ * route) while the fitter cut the right-most part, so a narrow terminal deleted a
200
+ * queue count while keeping a preset the header already prints. Sorting the parts
201
+ * by how much they are worth puts both rules in one order:
202
+ *
203
+ * 1. `[compact]` - the view mode, the one marker nothing else repeats;
204
+ * 2. `dir:...` - where this session is working, which the header never says;
205
+ * 3. `bal ...` - a reading the row must not lose: on a balance-only provider
206
+ * (DeepSeek) it is the only one, so it outlives the configuration behind it;
207
+ * 4. `search i/n`, the input's shape, `queued n` - *state*: what the session is
208
+ * doing right now, which is what the reader needs mid-turn;
209
+ * 5. `sub:...` - configuration, inherited by every child and changed only by
210
+ * `/submodel`, so it sits after the state; and
211
+ * 6. the subscription badge (`OpenCode-GO`, `CommandCode-GOAT`) - context rather
212
+ * than action, so it closes the row and is the first part a narrow row gives
213
+ * up. Losing the last part reads as "the row got shorter", never as a hole in
214
+ * the middle of it.
215
+ *
216
+ * The preset is deliberately absent: the header prints it permanently.
217
+ */
218
+ /** Options for {@link footerIdentityParts}. */
219
+ export interface FooterIdentityOptions {
220
+ /**
221
+ * Leave the provider/model/effort out.
222
+ *
223
+ * The header paints the live route permanently, so on the workspace row the
224
+ * parent model is the one piece of information the reader already has — and
225
+ * the piece that used to push the `sub:` chip off a 100-column row.
226
+ */
227
+ omitModel?: boolean;
228
+ }
229
+ /**
230
+ * The workspace row: what this session is, not what it is doing.
231
+ *
232
+ * Quota and context used to live here and now own capacity meters on the row
233
+ * above, so they are deliberately absent: a number painted twice is a number the
234
+ * reader has to check twice. What is left is the configuration the header does
235
+ * not carry — the preset, the working directory, the child route, and the
236
+ * input's own transient state.
237
+ */
238
+ export declare function footerIdentityParts(input: FooterStatusInput, options?: FooterIdentityOptions): string[];
239
+ /**
240
+ * Fit the workspace row under `width`.
241
+ *
242
+ * The metadata is secondary by construction: it follows the activity label at a
243
+ * dimmer intensity and is dropped from the right, one part at a time, rather
244
+ * than being allowed to wrap into the status row above it.
245
+ */
246
+ export declare function fitFooterStatusLine(activity: string, identity: readonly string[], width: number): string;
247
+ export interface StatusReportInput {
248
+ sessionId: string;
249
+ pluginVersion: string;
250
+ provider: string;
251
+ model: string;
252
+ effort?: string;
253
+ agentStatus: string;
254
+ preset: string;
255
+ activeSubagents: number;
256
+ plan: 'off' | 'pending' | 'on';
257
+ paint: string;
258
+ disconnect?: DisconnectPolicyName;
259
+ waitingQuestions: number;
260
+ quota?: QuotaSnapshot;
261
+ context?: ContextPressureView;
262
+ parentModel?: string;
263
+ subProvider?: string;
264
+ subModel: string;
265
+ cwd?: string;
266
+ /** Session counters: the block the default row gave up (B-footer pass). */
267
+ stats?: SessionStatsRow;
268
+ /** Throughput accounting: exact rate and the running estimate. */
269
+ throughput?: ThroughputView;
270
+ }
271
+ /**
272
+ * Lines printed by `/status` — SSH first-boot diagnostics, no extra command.
273
+ *
274
+ * This is where the status row's removed telemetry lives. Turns, steps, model
275
+ * time, tool time, cache hit and the input/output breakdown are session
276
+ * analytics: they answer no question a live operator is asking, which is why
277
+ * they left the default row, and their disappearance would be information loss —
278
+ * so `/status` prints all of them, plus the throughput accounting, in the shapes
279
+ * the row used to use.
280
+ */
281
+ export declare function formatStatusReport(input: StatusReportInput): string[];
282
+ /**
283
+ * The stats block `/status` prints: everything the default row gave up.
284
+ *
285
+ * `tokens/sec` here is the exact per-step rate (`decodeTokens / decodeMs`), not
286
+ * the running estimate, and the line says which span it covers: the last step
287
+ * and the session so far are different questions and only one of them is a
288
+ * benchmark.
289
+ */
290
+ export declare function formatStatusStats(stats: SessionStatsRow): string[];
291
+ /** One line: what the throughput number means, and which span it covers. */
292
+ export declare function formatStatusThroughput(throughput: ThroughputView): string;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The three meter primitives the status row is built from.
3
+ *
4
+ * One component per kind of question, because a health question and a capacity
5
+ * question do not look alike:
6
+ *
7
+ * - {@link healthMeter} — "is this healthy?" Discrete levels, no bar. This is the
8
+ * SSH chip's four pips, generalised.
9
+ * - {@link capacityMeter} — "how much is left / how much is used?" A bar and a
10
+ * percent. The *direction* is a parameter, which is what lets quota (remaining)
11
+ * and context (used) share one implementation.
12
+ * - {@link performanceValue} — "how fast is it going?" A bare number, neutral,
13
+ * with a mark for the estimate and a dimmer form for a finished step.
14
+ *
15
+ * Nothing here knows what a row is, how wide it is, or in what order chips go:
16
+ * that is `footer-budget.ts`, and keeping the two apart is what stops a meter
17
+ * from growing a layout opinion.
18
+ *
19
+ * @module dsh-ssh-tui/footer-meters
20
+ */
21
+ import { type ThresholdLevel } from './theme.js';
22
+ import { type ColorDepth } from './color-depth.js';
23
+ import { type ThroughputView } from './throughput.js';
24
+ /**
25
+ * The row's visual vocabulary, as reusable primitives.
26
+ *
27
+ * Three components, because the row answers three different kinds of question:
28
+ *
29
+ * - {@link healthMeter} — "is this healthy?" Discrete levels, no bar. This is
30
+ * the SSH chip's four pips, generalised; nothing else on the row copies it,
31
+ * because a health question and a capacity question do not look alike.
32
+ * - {@link capacityMeter} — "how much is left / how much is used?" A bar with a
33
+ * percent. The *direction* is a parameter, which is what lets quota (remaining)
34
+ * and context (used) share one implementation.
35
+ * - {@link performanceValue} — "how fast is it going?" A bare number, neutral,
36
+ * with a mark for the estimate and a dimmer form for a finished step.
37
+ *
38
+ * All three take `ascii` and answer with the project's own fallback glyphs, and
39
+ * none of them reach for a threshold colour on their own: the caller decides
40
+ * whether the number it is drawing has bands at all.
41
+ */
42
+ /** How many level marks a health meter has: the four pips the SSH chip always had. */
43
+ export declare const HEALTH_METER_LEVELS = 4;
44
+ /** Full and empty level marks. The empty one is the same cell, hollow. */
45
+ export declare const HEALTH_MARK_FILLED = "\u25CF";
46
+ export declare const HEALTH_MARK_EMPTY = "\u25CB";
47
+ /** ASCII fallback for the same four levels, from the project's own glyph table. */
48
+ export declare const HEALTH_ASCII_FILLED = "*";
49
+ export declare const HEALTH_ASCII_EMPTY = "o";
50
+ /** Filled and empty cells of a capacity meter, matching the existing quota bar. */
51
+ export declare const CAPACITY_CELL_FILLED = "\u2588";
52
+ export declare const CAPACITY_CELL_EMPTY = "\u2591";
53
+ /** ASCII fallback for the capacity meter, from the project's own glyph table. */
54
+ export declare const CAPACITY_ASCII_FILLED = "#";
55
+ export declare const CAPACITY_ASCII_EMPTY = ".";
56
+ /** Segment counts by row budget. Three is the smallest bar that still reads. */
57
+ export declare const CAPACITY_SEGMENTS_WIDE = 8;
58
+ export declare const CAPACITY_SEGMENTS_MEDIUM = 5;
59
+ export declare const CAPACITY_SEGMENTS_NARROW = 3;
60
+ /** Which way a capacity meter reads. */
61
+ export type CapacityBasis = 'used' | 'remaining';
62
+ /** Options for one capacity meter; everything but the percent is optional. */
63
+ export interface CapacityMeterOptions {
64
+ /** Segment count when the row has room. */
65
+ segments?: number;
66
+ /** `used` reads left to right as usage, `remaining` as what is left. */
67
+ basis?: CapacityBasis;
68
+ /** Leading label, e.g. `5Hr` / `CTX`. */
69
+ label?: string;
70
+ /** Trailing text after the percent, e.g. `720K/1M`. */
71
+ detail?: string;
72
+ /** Drop the bar entirely, keeping `CTX 72%`. */
73
+ percentOnly?: boolean;
74
+ /** Quota bands (50 / 25 remaining) instead of context bands (80 / 95 used). */
75
+ quota?: boolean;
76
+ /** Glyphs the terminal can actually draw; defaults to the terminal's own state. */
77
+ ascii?: boolean;
78
+ /** Paint the filled run and the percent with the band's accent. */
79
+ color?: boolean;
80
+ /** Colour depth the accent is downgraded for. */
81
+ depth?: ColorDepth;
82
+ }
83
+ /**
84
+ * Colours for a capacity meter's filled portion.
85
+ *
86
+ * Context and quota answer different questions and have different runways, so
87
+ * they get different bands — but each one reuses a threshold the session already
88
+ * enforces, so the meter can never disagree with the alert beside it:
89
+ * context reuses the pressure levels `contextPressureView` uses (warn at 80%,
90
+ * danger at 95%, the same numbers that drive the compaction prompt), and quota
91
+ * reuses the first two of `QUOTA_ALERT_THRESHOLDS` (50 / 25 remaining), which is
92
+ * where the session starts telling the user about it.
93
+ */
94
+ export declare const CONTEXT_METER_WARN_PERCENT: number;
95
+ export declare const CONTEXT_METER_DANGER_PERCENT: number;
96
+ /** First two entries of `quota.ts`'s `QUOTA_ALERT_THRESHOLDS`, in remaining percent. */
97
+ export declare const QUOTA_METER_WARN_PERCENT = 50;
98
+ export declare const QUOTA_METER_DANGER_PERCENT = 25;
99
+ /** Which band a capacity reading falls in, given what the number means. */
100
+ export declare function capacityLevel(percent: number, basis?: CapacityBasis, quota?: boolean): ThresholdLevel;
101
+ /**
102
+ * One capacity meter: `CTX ███████░░░ 61%`.
103
+ *
104
+ * An unusable percent paints a hollow bar with a question mark rather than `0%`:
105
+ * a reading nobody took is not a reading of zero, and `?%` is the same claim the
106
+ * quota chip has always made before its first reading lands.
107
+ *
108
+ * Colour, when asked for, lands only on the cells that carry the meaning — the
109
+ * filled run and the percent. Tinting the label and the empty cells would make
110
+ * the whole chip one colour and lose the hierarchy it exists to create.
111
+ */
112
+ export declare function capacityMeter(percent: number | undefined, options?: CapacityMeterOptions): {
113
+ text: string;
114
+ level: ThresholdLevel;
115
+ };
116
+ /**
117
+ * One health meter: `●●●●`.
118
+ *
119
+ * Discrete levels, never a bar: a link is one of five states, and four pips say
120
+ * which at a glance. This is the component the SSH chip has always been — it
121
+ * simply became reusable when the quota and context meters arrived, so that the
122
+ * two things that *are* capacities stopped being drawn as pips.
123
+ */
124
+ export declare function healthMeter(level: number, options?: {
125
+ levels?: number;
126
+ ascii?: boolean;
127
+ }): string;
128
+ /** Options for {@link performanceValue}. */
129
+ export interface PerformanceValueOptions {
130
+ /** `live` is the streaming estimate, `settled` the last completed step. */
131
+ state: 'live' | 'settled' | 'unavailable';
132
+ /** `tok/s` in full, `t/s` when the row is out of room. */
133
+ short?: boolean;
134
+ /** Paint the live value at the row's primary intensity. */
135
+ color?: boolean;
136
+ }
137
+ /** Which form of the throughput number can be painted at all. */
138
+ export declare function performanceValueState(throughput: ThroughputView | undefined, running: boolean): PerformanceValueOptions['state'];
139
+ /**
140
+ * The performance value: `160 tok/s`, `~160 tok/s`, `— tok/s`.
141
+ *
142
+ * Tokens per second is neutral telemetry, so this component never reaches for a
143
+ * threshold colour: the harness publishes no performance classification, and the
144
+ * same number means different things on a reasoning model, a small fast one, and
145
+ * a long-context request. `~` marks the live estimate — characters per second
146
+ * scaled by this session's own tokens-per-character — and the settled value has
147
+ * no mark because it is exact. A live value is lifted out of the row's muted
148
+ * style because it is the one number on the row that is *moving*; every theme
149
+ * understands "default intensity", and the mono theme keeps the distinction.
150
+ */
151
+ export declare function performanceValue(throughput: ThroughputView | undefined, running: boolean, options?: Partial<PerformanceValueOptions>): string;