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,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;
|
package/lib/types/copy-text.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/lib/types/dialogs.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|