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.
- package/CHANGELOG.md +158 -0
- package/README.en.md +118 -15
- package/README.md +78 -13
- package/docs/release-notes-0.7.4.md +91 -0
- package/docs/release-notes-0.8.0.md +56 -0
- package/docs/release-notes-0.8.1.md +79 -0
- package/docs/release-notes-0.8.2-rc.1.md +159 -0
- package/docs/release-notes-0.8.2.md +62 -0
- package/docs/screenshots/reconnect.gif +0 -0
- package/docs/terminals.md +23 -0
- package/docs/windows.md +13 -0
- package/lib/approval-state.js +148 -0
- package/lib/approval-state.js.map +1 -0
- package/lib/auth-failure.js +38 -0
- package/lib/auth-failure.js.map +1 -1
- package/lib/color-depth.js +26 -0
- package/lib/color-depth.js.map +1 -1
- package/lib/copy-text.js +30 -8
- package/lib/copy-text.js.map +1 -1
- package/lib/dialogs.js +81 -0
- package/lib/dialogs.js.map +1 -1
- package/lib/display-sock.js +138 -23
- package/lib/display-sock.js.map +1 -1
- package/lib/footer-accents.js +28 -0
- package/lib/footer-accents.js.map +1 -0
- package/lib/footer-budget.js +328 -0
- package/lib/footer-budget.js.map +1 -0
- package/lib/footer-format.js +433 -0
- package/lib/footer-format.js.map +1 -0
- package/lib/footer-layout.js +407 -0
- package/lib/footer-layout.js.map +1 -0
- package/lib/footer-meters.js +185 -0
- package/lib/footer-meters.js.map +1 -0
- package/lib/footer.js +19 -558
- package/lib/footer.js.map +1 -1
- package/lib/i18n/en.js +65 -5
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +67 -5
- package/lib/i18n/zh.js.map +1 -1
- package/lib/paint.js +53 -8
- package/lib/paint.js.map +1 -1
- package/lib/plan-projection.js +291 -0
- package/lib/plan-projection.js.map +1 -0
- package/lib/plan.js +50 -0
- package/lib/plan.js.map +1 -1
- package/lib/question-state.js +83 -0
- package/lib/question-state.js.map +1 -0
- package/lib/representation.js +283 -0
- package/lib/representation.js.map +1 -0
- package/lib/screen.js +105 -0
- package/lib/screen.js.map +1 -0
- package/lib/selection.js +5 -3
- package/lib/selection.js.map +1 -1
- package/lib/startup.js +49 -0
- package/lib/startup.js.map +1 -1
- package/lib/stats.js +101 -3
- package/lib/stats.js.map +1 -1
- package/lib/term-text.js +154 -55
- package/lib/term-text.js.map +1 -1
- package/lib/theme.js +20 -3
- package/lib/theme.js.map +1 -1
- package/lib/throughput.js +305 -0
- package/lib/throughput.js.map +1 -0
- package/lib/tool-present.js +57 -7
- package/lib/tool-present.js.map +1 -1
- package/lib/tui.js +3776 -1105
- package/lib/tui.js.map +1 -1
- package/lib/types/approval-state.d.ts +75 -0
- package/lib/types/auth-failure.d.ts +25 -0
- package/lib/types/color-depth.d.ts +21 -0
- package/lib/types/copy-text.d.ts +18 -1
- package/lib/types/dialogs.d.ts +111 -1
- package/lib/types/display-sock.d.ts +15 -0
- package/lib/types/footer-accents.d.ts +22 -0
- package/lib/types/footer-budget.d.ts +204 -0
- package/lib/types/footer-format.d.ts +221 -0
- package/lib/types/footer-layout.d.ts +292 -0
- package/lib/types/footer-meters.d.ts +151 -0
- package/lib/types/footer.d.ts +19 -269
- package/lib/types/paint.d.ts +48 -0
- package/lib/types/plan-projection.d.ts +162 -0
- package/lib/types/plan.d.ts +13 -0
- package/lib/types/question-state.d.ts +96 -0
- package/lib/types/representation.d.ts +498 -0
- package/lib/types/screen.d.ts +126 -0
- package/lib/types/startup.d.ts +24 -0
- package/lib/types/stats.d.ts +64 -1
- package/lib/types/term-text.d.ts +45 -1
- package/lib/types/throughput.d.ts +163 -0
- package/lib/types/tool-present.d.ts +41 -0
- package/lib/types/transcript-types.d.ts +63 -2
- package/lib/types/tui.d.ts +862 -11
- 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;
|