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,75 @@
1
+ /** The four states a tool card may show for the approval that guarded it. */
2
+ export type ApprovalState = 'waiting' | 'approved' | 'rejected' | 'unknown';
3
+ /** How well the state is known. Ordered from strongest to weakest. */
4
+ export type ApprovalProvenance = 'live' | 'policy' | 'durable' | 'inferred';
5
+ /** The approval field of one tool row. Display-only: never persisted by us. */
6
+ export interface ToolApproval {
7
+ state: ApprovalState;
8
+ provenance: ApprovalProvenance;
9
+ /** True when a rule/reviewer decided it rather than a person (`policy`). */
10
+ auto?: boolean;
11
+ /** Why — the classifier's or the reviewer's reason, when one was given. */
12
+ reason?: string;
13
+ }
14
+ /**
15
+ * One approval fact onto a card's current field.
16
+ *
17
+ * `next` wins on state when it is at least as strong as what is already there. A
18
+ * live decision is not overwritten by the durable event of the *same* decision
19
+ * (which arrives right behind it on the live path and says strictly less), and an
20
+ * inference never replaces anything but an inference.
21
+ * @param current - the field as it stands, if any.
22
+ * @param next - the fact being applied.
23
+ * @returns the field the card should carry.
24
+ */
25
+ export declare function mergeApproval(current: ToolApproval | undefined, next: ToolApproval): ToolApproval;
26
+ /**
27
+ * A recorded outcome as a card state.
28
+ *
29
+ * `cancelled` and `unavailable` are deliberately **unknown**, not `rejected`: the
30
+ * reader's Esc, or a Host that had no approval channel, is not the human refusing
31
+ * the tool, and printing a refusal they did not make is exactly the kind of
32
+ * comfortable lie this field exists to prevent.
33
+ * @param outcome - the `approval/decided` outcome, as recorded.
34
+ * @returns the state the card shows.
35
+ */
36
+ export declare function approvalStateFromOutcome(outcome: string): ApprovalState;
37
+ /**
38
+ * Whether a tool result proves an approval was **refused** when no audit events
39
+ * survived.
40
+ *
41
+ * Only the Harness's own refusal sentences count. A generic denial (`denied by
42
+ * policy`, `blocked`) does *not* prove an approval was ever needed, and the whole
43
+ * point of the `inferred` provenance is that it is used only where the text can
44
+ * carry it: the phrase below is what `dsh-tools` writes when `approval.request()`
45
+ * came back `rejected` or `cancelled`, so it cannot appear for a tool that was
46
+ * refused by a rule instead.
47
+ *
48
+ * It can never produce `approved` — running successfully is not evidence that
49
+ * anyone approved anything (a tool with no approval requirement runs too).
50
+ * @param text - the tool result's text.
51
+ * @returns `rejected` when the text is that refusal, undefined otherwise.
52
+ */
53
+ export declare function inferredApprovalFromResult(text: string): ApprovalState | undefined;
54
+ /** A one-word label for the card chip. */
55
+ export declare function approvalStateKey(state: ApprovalState): string;
56
+ /**
57
+ * The provenance note shown beside the state.
58
+ *
59
+ * `live` and `durable` say nothing extra: a person's own decision and the log of
60
+ * it are the same fact at two moments. `policy` and `inferred` do, because the
61
+ * reader is being told something weaker and has to be able to tell.
62
+ */
63
+ export declare function approvalProvenanceKey(approval: ToolApproval): string | undefined;
64
+ /**
65
+ * The expanded card's own line for the approval.
66
+ *
67
+ * It names the state, the strength of the evidence, and the reason when one
68
+ * survived: `live`/`durable` need no apology, `policy` is marked automatic, and
69
+ * `inferred` says outright that the card is reading the log rather than quoting it.
70
+ * @param approval - the card's approval field.
71
+ * @returns the sentence, already translated.
72
+ */
73
+ export declare function approvalDetailText(approval: ToolApproval): string;
74
+ /** Whether two approval fields would render identically. */
75
+ export declare function sameApproval(left: ToolApproval | undefined, right: ToolApproval | undefined): boolean;
@@ -76,3 +76,28 @@ export declare function classifyAuthFailure(message: string): AuthFailure | unde
76
76
  * @returns true when this is that failure.
77
77
  */
78
78
  export declare function isReasoningReplayFailure(message: string): boolean;
79
+ /**
80
+ * A gateway that refused the request itself, without saying why.
81
+ *
82
+ * The sibling of the failure above, and the one a reader actually meets most: the
83
+ * same thinking-mode route answers a bare envelope with a trace id and nothing else.
84
+ * Measured in one long session (2026-10-03, command-code, `effort: max`):
85
+ *
86
+ * 13 of 14 failed turns were this shape, 12 of them bare
87
+ * they arrived in runs — eight consecutive turns over twelve minutes, then the
88
+ * same route, model and key answered normally again
89
+ * the harness made 14 attempts and 0 retries (a 400 is outside its retryable set)
90
+ *
91
+ * Those three facts decide what the plugin may say and may do. It may say the
92
+ * request body was refused (not the credential) and that the known member of this
93
+ * family is the reasoning-replay defect. It may *not* claim that sending again will
94
+ * work: the envelope carries no reason, and a run that deep may equally be a request
95
+ * the API will keep refusing. What it must not do is retry on its own — the sibling's
96
+ * case below is deterministic, and this one re-sends a whole context to find out.
97
+ *
98
+ * The envelope is deliberately narrow: a 400 whose body names a field
99
+ * (`unknown field "summary"`) is a different fix and is not this.
100
+ * @param message - the failure text the host attached to the turn.
101
+ * @returns true when this is a bare 400 rejection.
102
+ */
103
+ export declare function isRequestRejectedFailure(message: string): boolean;
@@ -48,3 +48,24 @@ export declare function basicSgrFor(r: number, g: number, b: number): number;
48
48
  * @returns the parameter list to emit, possibly empty.
49
49
  */
50
50
  export declare function downgradeSgr(code: string, depth: ColorDepth): string;
51
+ /**
52
+ * Paint one run of text in one role's colour.
53
+ *
54
+ * The one helper the footer's whole colour grammar is built on — the meter
55
+ * primitives paint their filled cell with it and the status row paints the link's
56
+ * pips — so it lives here, beside `downgradeSgr`, rather than in a module of its
57
+ * own. Three rules it keeps:
58
+ *
59
+ * - it wraps *one* run, closes itself, and stops there;
60
+ * - re-opening the surrounding muted style is the *row owner's* job
61
+ * (`muteFooterLine` in `tui.ts` reopens it after every reset), because doing it
62
+ * here as well produced two identical `90m` sequences per accent and made the
63
+ * output depend on which separator the caller passed;
64
+ * - a terminal with no colour left answers with the text unchanged, so the
65
+ * `no-color` and `mono` paths need no branch of their own.
66
+ * @param text - the run to paint.
67
+ * @param token - SGR parameters for the role, as `theme.ts` emits them.
68
+ * @param depth - what the terminal can take.
69
+ * @returns the run, painted when there is colour to paint it with.
70
+ */
71
+ export declare function accent(text: string, token: string, depth: ColorDepth): string;
@@ -3,7 +3,24 @@
3
3
  */
4
4
  import type { CollapsibleBlock, Row } from './transcript-types.js';
5
5
  export declare function copyTextFromRow(row: Row | CollapsibleBlock | undefined): string;
6
- export declare function copyTextFromTranscript(rows: readonly Row[], focused: Row | CollapsibleBlock | null): {
6
+ /** What a copy is aimed at: the highlighted card, or the model's last reply. */
7
+ export type CopyTarget = 'highlight' | 'reply';
8
+ /** The newest model reply with text, or `''` when the session has not produced one. */
9
+ export declare function latestReplyText(rows: readonly Row[]): string;
10
+ /**
11
+ * The text one copy target holds.
12
+ *
13
+ * The two targets are deliberately exclusive. They used to be one rule —
14
+ * "the highlighted card, else the newest reply" — and the fallback made the
15
+ * reply unreachable: any card left highlighted (one click on a tool card is
16
+ * enough) silently redirected `/copy`, with nothing on screen saying the reply
17
+ * had been passed over. `/copy` now names its target and defaults to the reply;
18
+ * `highlight` is the old rule with its own, honest, empty case.
19
+ * @param rows - the rows the reader can see (a `/clear` cutoff is not searched).
20
+ * @param focused - the highlighted row, if any.
21
+ * @param target - what the reader asked for; `reply` is the command's default.
22
+ */
23
+ export declare function copyTextFromTranscript(rows: readonly Row[], focused: Row | CollapsibleBlock | null, target?: CopyTarget): {
7
24
  text: string;
8
25
  source: 'focused' | 'assistant' | 'empty';
9
26
  };
@@ -52,9 +52,25 @@ export interface QuestionDialog {
52
52
  }): void;
53
53
  reject(error: unknown): void;
54
54
  }
55
+ /**
56
+ * @deprecated Superseded by a Screen (`ScreenState`, `kind: 'setup'`) in B2.6.
57
+ *
58
+ * Kept as a *shape* so 0.8.x does not break an importer that names the type: nothing
59
+ * in this plugin constructs one any more, and the wizard has no dialog path left to
60
+ * reach it through. A dialog handed to the dialog path is still rejected as a
61
+ * question rather than silently queued.
62
+ */
55
63
  export interface OnboardingDialog {
56
64
  kind: 'onboarding';
57
65
  }
66
+ /**
67
+ * @deprecated Superseded by `ScreenState` (`screen.ts`) in B2.1.
68
+ *
69
+ * Kept as a *shape* rather than a dialog so the single judgement entry
70
+ * (`screenFromDialog`) can recognise one if it is ever handed to the dialog path,
71
+ * and so 0.8.x does not break an importer that names the type. Nothing in this
72
+ * plugin constructs one any more: every caller opens a Screen.
73
+ */
58
74
  export interface InspectDialog {
59
75
  kind: 'inspect';
60
76
  title: string;
@@ -81,7 +97,80 @@ export interface InspectDialog {
81
97
  */
82
98
  notice?: string;
83
99
  }
84
- export type Dialog = ConfirmDialog | QuestionDialog | OnboardingDialog | InspectDialog;
100
+ /**
101
+ * A dialog that owns the keyboard for the length of one human action.
102
+ *
103
+ * Deliberately does **not** include the inspect shape: since B2.1 an inspect body
104
+ * is a *Screen*, not a dialog — it replaces the workspace instead of borrowing rows
105
+ * from it, and it never enters `dialogQueue` (`screenFromDialog` converts the legacy
106
+ * shape, and `docs/decisions/b2-architecture-decisions.md` AD-1…AD-3 freeze why).
107
+ */
108
+ export type Dialog = ConfirmDialog | QuestionDialog | OnboardingDialog;
109
+ /**
110
+ * The dialogs the *live* dialog path can actually receive.
111
+ *
112
+ * `OnboardingDialog` is in the union only for 0.8.x importers; the wizard is a Screen
113
+ * now, so the queue, the role and the painter only ever see these two (B2.6 §17).
114
+ */
115
+ export type SurfaceDialog = Extract<Dialog, {
116
+ kind: 'confirm' | 'questions';
117
+ }>;
118
+ /** Whether this is a dialog the live path can own (see {@link SurfaceDialog}). */
119
+ export declare function isSurfaceDialog(dialog: Dialog): dialog is SurfaceDialog;
120
+ /**
121
+ * What kind of human action a surface is asking for.
122
+ *
123
+ * The dialog *shape* deliberately does not say this: `/model` and
124
+ * `ask_user_question` both open a `questions` dialog, and the two mean opposite
125
+ * things — one changes the operating environment, the other is the task stopping
126
+ * to wait for a person. Reading the shape as the meaning is what made the status
127
+ * row claim the agent was waiting while a picker was open, and call an approval
128
+ * idle. The opener declares the role instead.
129
+ */
130
+ export type InteractionKind = 'question' | 'plan-review' | 'approval' | 'confirm';
131
+ /**
132
+ * Who owns the keyboard, and what that means for the task.
133
+ *
134
+ * - `interaction` — the agent (or the action the reader asked for) is blocked on a
135
+ * human decision, and only `ask` distinguishes the three that stall a turn from
136
+ * a confirmation that blocks a local command;
137
+ * - `picker` — control plane: it changes the environment and leaves the agent
138
+ * exactly as busy or idle as it already was.
139
+ *
140
+ * There used to be a third, `dedicated`, for "a surface with the whole screen and its
141
+ * own state machine". B2.6 gave the two things that meant — the inspect overlay and
142
+ * the setup wizard — the Screen contract instead, and with no producer left the role
143
+ * was removed rather than kept as a path nothing could reach.
144
+ */
145
+ export type SurfaceRole = {
146
+ kind: 'interaction';
147
+ ask: InteractionKind;
148
+ } | {
149
+ kind: 'picker';
150
+ };
151
+ /** The roles as values, so no call site invents its own object. */
152
+ export declare const PICKER_ROLE: SurfaceRole;
153
+ export declare function interactionRole(ask: InteractionKind): SurfaceRole;
154
+ /**
155
+ * Whether a role means the task is stopped waiting for the human.
156
+ *
157
+ * A local `confirm` (deleting a preset, repairing the profile) owns the keyboard
158
+ * too, but the agent is not waiting on it — reporting it as a wait is the same
159
+ * class of mistake as reporting a picker as one.
160
+ */
161
+ export declare function stallsTask(role: SurfaceRole | undefined): boolean;
162
+ /**
163
+ * Which surface gets the keyboard first when more than one is waiting (B1.2).
164
+ *
165
+ * Only one transient surface is ever drawn, so this is not a z-order: it decides
166
+ * who is *next in line*. A task interaction outranks a picker (someone is blocked on
167
+ * it, and the agent cannot proceed), and a picker outranks whatever the reader was
168
+ * about to type. Equal ranks keep their arrival order, so a queue of two pickers
169
+ * still behaves exactly as it did before.
170
+ *
171
+ * A Screen is not in this ordering at all: it is not a surface (B2.6 §1).
172
+ */
173
+ export declare function surfacePriority(role: SurfaceRole): number;
85
174
  export interface DialogAnswer {
86
175
  selected: string[];
87
176
  custom?: string;
@@ -165,3 +254,24 @@ export declare function questionSubmit(dialog: QuestionDialog, input: string): Q
165
254
  export declare function confirmAnswer(text: string): 'y' | 'n' | 'cancel' | undefined;
166
255
  /** The inspect overlay closes on any of these, and ignores the rest. */
167
256
  export declare function inspectClosesOn(text: string): boolean;
257
+ /**
258
+ * Fit an interaction's rendered lines into the rows the frame can give it.
259
+ *
260
+ * The interaction layer is composed *over* the transcript (see `paintFrame`), and
261
+ * the space above the composer is whatever the terminal has left after the footer,
262
+ * the composer and the header. On a short terminal that can be fewer rows than a
263
+ * question list needs — and clipping the *bottom* would hide the options and the
264
+ * key hint, which is exactly the part that has to stay reachable. So the window
265
+ * keeps the focused row visible and marks what it dropped.
266
+ *
267
+ * @param lines - the interaction as its renderer drew it, top to bottom.
268
+ * @param cap - how many rows the frame has for it.
269
+ * @param focusLine - index of the row that must stay visible (the highlighted
270
+ * option, or the prompt when there is no list).
271
+ * @returns the rows to place, top to bottom, at most `cap` of them.
272
+ */
273
+ export declare function windowInteractionLines(lines: readonly string[], cap: number, focusLine?: number): {
274
+ lines: string[];
275
+ hiddenAbove: number;
276
+ hiddenBelow: number;
277
+ };
@@ -223,6 +223,19 @@ export declare class DisplayHost {
223
223
  */
224
224
  private probeTerminal;
225
225
  private settleProbe;
226
+ /**
227
+ * How many bytes are queued for the terminal and still undelivered.
228
+ *
229
+ * The Host composes every frame, and on a resize it is the only one that can
230
+ * decide whether there is room for another one: the socket the frames travel on
231
+ * belongs to this object. Without the number the Host can only *guess* how fast
232
+ * the link is (from the measured round trip) and pace itself conservatively,
233
+ * which is what made a drag lag behind the pointer on a fast link. With it, the
234
+ * rule becomes the honest one: paint the new size now, unless the previous frame
235
+ * has not left yet.
236
+ * @returns queued bytes, or undefined when no display is attached.
237
+ */
238
+ pendingBytes(): number | undefined;
226
239
  sendStdout(bytes: Buffer | string): boolean;
227
240
  sendGoodbye(): void;
228
241
  /**
@@ -422,6 +435,8 @@ export interface DisplayRelayOptions {
422
435
  /** Re-measure cadences (tests); defaults to {@link RTT_RECHECK_FIRST_MS} / {@link RTT_RECHECK_MS}. */
423
436
  rttFirstRecheckMs?: number;
424
437
  rttRecheckMs?: number;
438
+ /** Terminal-size poll cadence (tests); default {@link SIZE_RECHECK_MS}. */
439
+ sizeRecheckMs?: number;
425
440
  /** Typing captured before this relay existed; sent to the Host after HELLO. */
426
441
  seed?: string;
427
442
  /**
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The one accent helper the footer's colour grammar is built on.
3
+ *
4
+ * It lives alone because two modules need it and neither may own the other: the
5
+ * meter primitives paint their filled cell with it, and the budget paints the
6
+ * link's pips. A helper this small is not worth a module in general; here it is
7
+ * worth avoiding a cycle, and it gives the colour grammar one place to be
8
+ * described:
9
+ *
10
+ * - an accent wraps exactly one run of text, closes itself, and stops there;
11
+ * - re-opening the row's muted style is the *row owner's* job
12
+ * (`muteFooterLine` in `tui.ts` reopens it after every reset), because doing
13
+ * it here as well produced two identical `90m` sequences per accent and made
14
+ * the primitive's output depend on which separator the caller passed;
15
+ * - a terminal with no colour left answers with the text unchanged, so the
16
+ * `no-color` and `mono` paths need no branch of their own.
17
+ *
18
+ * @module dsh-ssh-tui/footer-accents
19
+ */
20
+ import { type ColorDepth } from './color-depth.js';
21
+ /** Wrap one run in one accent, or return it unchanged when the terminal has none. */
22
+ export declare function accent(text: string, token: string, depth: ColorDepth): string;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Responsive budget policy: how much of the row survives at which width.
3
+ *
4
+ * The row is one ordered list of chips, and this module owns the *degradation
5
+ * ladder* every chip answers to — a single integer level, tried from 0 upward,
6
+ * where each step removes cells in the order of the information priority the row
7
+ * was designed around. Nothing outside this module may decide that a chip is
8
+ * dropped, shortened, or spelled differently.
9
+ *
10
+ * @module dsh-ssh-tui/footer-budget
11
+ */
12
+ import { type ColorDepth } from './color-depth.js';
13
+ import { type PaintLinkKind } from './paint.js';
14
+ import type { QuotaPeriod } from './quota.js';
15
+ import type { ThroughputView } from './throughput.js';
16
+ import { type ContextPressureView } from './footer-format.js';
17
+ export type FooterActivityKind = 'plan-review' | 'waiting' | 'approval' | 'compacting' | 'retry' | 'agent' | 'tools' | 'plan-open' | 'plan-pending' | 'goal' | 'thinking' | 'reply' | 'waiting-llm' | 'idle';
18
+ /**
19
+ * How the default row paints its link.
20
+ *
21
+ * Deliberately the same four-pip vocabulary `formatLinkQualityChip` established:
22
+ * the thresholds behind it (`linkQualityOf`) were validated against real SSH
23
+ * probes, and a UI pass that re-derived its own numbers would put two
24
+ * contradictory opinions about the same link on the same screen.
25
+ */
26
+ export interface FooterStripLink {
27
+ kind: PaintLinkKind;
28
+ intervalMs: number;
29
+ /** Measured round-trip, when the CSI-6n probe answered. */
30
+ rttMs?: number;
31
+ probed: boolean;
32
+ }
33
+ /**
34
+ * The activity chip's input: the verb, and when it started.
35
+ *
36
+ * Deliberately *not* the subject — not the command being run, not the file being
37
+ * edited, not the path being searched. The status row answers "what is happening"
38
+ * (`终端`, `edit`, `thinking`); *which* command, on *which* file, is the
39
+ * transcript card's job, one line up, where the reader can actually read it. A
40
+ * command in the chip also spent the row's cells on the least load-bearing thing
41
+ * on it: a `pytest -x tests/ --maxfail=1` pushed the quota and context meters
42
+ * towards the drop edge, and `/status`-like detail in a status row is exactly the
43
+ * telemetry dump this row was rebuilt to stop being.
44
+ */
45
+ export interface FooterStripActivity {
46
+ kind: FooterActivityKind;
47
+ text: string;
48
+ /** Wall clock the action started; drives the `· 28s` suffix. */
49
+ startedAt?: number;
50
+ now?: number;
51
+ }
52
+ export interface FooterStripQuota {
53
+ /** 0–100, remaining. `undefined` paints the empty bar with `?%`. */
54
+ remainingPercent?: number;
55
+ period?: QuotaPeriod;
56
+ }
57
+ /** Everything the default status row paints, in the order a glance reads it. */
58
+ export interface FooterStripInput {
59
+ link: FooterStripLink;
60
+ activity: FooterStripActivity;
61
+ throughput?: ThroughputView;
62
+ running: boolean;
63
+ quota?: FooterStripQuota;
64
+ context?: ContextPressureView;
65
+ /**
66
+ * Session-cumulative tokens, **only** when the harness published the total
67
+ * itself. Absent means the chip is not painted at all: a total assembled from
68
+ * the billed parts is a different accounting, and `/status` is where the two are
69
+ * told apart (`sessionTokenTotal` returns which one it had).
70
+ */
71
+ totalTokens?: number;
72
+ /**
73
+ * The install warning that leads the row, when the profile is missing rows.
74
+ *
75
+ * It is on the *same* ladder as everything else rather than being fitted around
76
+ * it: it used to be composed into a chip strip by `fitFooterChips` and the rest
77
+ * of the row fitted into what was left, which meant two convergence algorithms
78
+ * on one line and a width hand-computed at the call site. It keeps the highest
79
+ * priority, so it is still the last thing a narrow terminal gives up, and only
80
+ * its text degrades — to the glyph, and off the row only at the very end.
81
+ */
82
+ warning?: {
83
+ long: string;
84
+ short: string;
85
+ };
86
+ /**
87
+ * The Host-local echo: the last thing this Host confirmed to the reader
88
+ * (`/theme` switched, a preset saved, a copy landed).
89
+ *
90
+ * Not history (AD-13) and not telemetry: it is an acknowledgement, so it is the
91
+ * lowest-priority chip on the row — the first thing a narrow terminal gives up, and
92
+ * never at the cost of the link, the activity, the quota or the context reading.
93
+ */
94
+ echo?: string;
95
+ /** Whether the terminal must be painted with ASCII glyphs. */
96
+ ascii?: boolean;
97
+ /** Colour depth the accents are downgraded for. */
98
+ depth?: ColorDepth;
99
+ /** Paint accents at all. False is the `no-color` terminal. */
100
+ color?: boolean;
101
+ }
102
+ /**
103
+ * A chip on the default row: one signal, painted at one of nine densities.
104
+ *
105
+ * The level is a *degradation ladder*, not a size class. Level 0 is the full
106
+ * form a 160-column terminal shows; each step buys cells from the least
107
+ * load-bearing part of the row. The order is the whole design: the session
108
+ * total goes first, then the activity's subject, then the meters give up cells
109
+ * before they give up their number, and the link keeps its dots almost to the
110
+ * end — because those dots are the one component that says whether the session
111
+ * is alive. A narrow terminal ends up *quieter*, never denser.
112
+ */
113
+ export interface FooterStripChip {
114
+ id: string;
115
+ /** Component order. Lower is more load-bearing. */
116
+ priority: number;
117
+ /** The chip at one degradation level; `''` drops the chip at that level. */
118
+ render: (level: number) => string;
119
+ }
120
+ /**
121
+ * The deepest degradation level any chip understands.
122
+ *
123
+ * Sixteen levels, one per cell of budget, in the priority order the row was
124
+ * designed around. A coarser ladder left the 72–99 range showing the same row as
125
+ * a 100-column terminal, which is the failure mode this exists to avoid: the
126
+ * narrow terminal must get *less*, not merely a compressed copy.
127
+ */
128
+ export declare const FOOTER_STRIP_MAX_LEVEL = 17;
129
+ /** The default row's group separator, matching the pre-strip footer. */
130
+ export declare const FOOTER_STRIP_SEPARATOR = " \u2502 ";
131
+ /**
132
+ * The activity chip's text: `⠹ edit · 28s`, `⠹ thinking · 18s`, `idle`.
133
+ *
134
+ * The spinner is passed in rather than computed here so the row has exactly one
135
+ * clock (`SPINNER` in `tui.ts`), which is what keeps the glyph in step with the
136
+ * transcript's own live rows.
137
+ * @param activity - the current operational state.
138
+ * @param spinner - the frame to show in front of a working state, if any.
139
+ * @param level - degradation level; the glyph and the clock go before the verb.
140
+ */
141
+ export declare function activityMeter(activity: FooterStripActivity, spinner: string, level?: number): string;
142
+ /**
143
+ * Build the default status row: the ordered chips, degraded to fit `width`.
144
+ *
145
+ * Order is the information priority the row is built around — link health, what
146
+ * is happening, how fast tokens are arriving, what quota is left, how full the
147
+ * context is, and finally how big the session has grown. Turns, steps, model
148
+ * time, tool time and cache hit left this row on purpose: they are session
149
+ * analytics, they answer no question a live operator is asking, and they are all
150
+ * still in `/status`.
151
+ *
152
+ * Nothing here paints threshold colour on throughput: the harness publishes no
153
+ * performance classification, and inventing one would paint a healthy reasoning
154
+ * model red for being deliberate.
155
+ * @param input - the signals, already resolved by the caller.
156
+ * @param width - cells available on the row.
157
+ * @param separator - the (already accented) group separator.
158
+ * @param spinner - current animation frame for working states.
159
+ * @returns the row, never wider than `width`.
160
+ */
161
+ export declare function runtimeStrip(input: FooterStripInput, width: number, separator?: string, spinner?: string): string;
162
+ /**
163
+ * What a Screen's compact runtime strip reports, in the order it is read.
164
+ *
165
+ * A Screen replaces the whole workspace, so the footer's two rows are gone with
166
+ * it. This is the subset that has to survive: the reader must still be able to
167
+ * answer "is the agent running", "is anything waiting", "is the link alive". It
168
+ * deliberately carries no throughput, no session total, no cwd and no subagent
169
+ * metadata — those are workspace facts about *work*, and a Screen is a place you
170
+ * visit while the work continues (AD-8).
171
+ */
172
+ export interface ScreenStripInput {
173
+ activity: FooterStripActivity;
174
+ /** Human interactions waiting behind this screen: a question or an approval. */
175
+ waiting: boolean;
176
+ /** Messages the reader queued for the agent. */
177
+ queued: number;
178
+ /** Agent-initiated questions the reader has not answered yet. */
179
+ queuedQuestions: number;
180
+ link: FooterStripLink;
181
+ context?: ContextPressureView;
182
+ quota?: FooterStripQuota;
183
+ ascii?: boolean;
184
+ color?: boolean;
185
+ depth?: ColorDepth;
186
+ }
187
+ /**
188
+ * The Screen strip: the status row's own chips, a subset, fitted from the back.
189
+ *
190
+ * Rendered with the *same* chip functions the status row uses (`linkChipText`,
191
+ * `activityMeter`, `contextChip`, `quotaChip`), so a Screen cannot drift into a
192
+ * second visual language or a second reading of the same state. Fitting is the
193
+ * opposite of the workspace row's: that row spends its cells on the tail first and
194
+ * degrades everything together, while the Screen strip holds its highest-priority
195
+ * group and drops whole groups from the end (quota, context, link, queued) — the
196
+ * reader is here for the report, and a strip that re-read as it scrolled would be
197
+ * worse than a shorter one.
198
+ * @param input - the runtime facts, all read from the same fields the footer reads.
199
+ * @param width - cells available on the row.
200
+ * @param separator - the (already accented) group separator.
201
+ * @param spinner - current animation frame for working states.
202
+ * @returns the row, never wider than `width`.
203
+ */
204
+ export declare function screenRuntimeStrip(input: ScreenStripInput, width: number, separator?: string, spinner?: string): string;