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
package/lib/types/footer.d.ts
CHANGED
|
@@ -1,276 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*/
|
|
4
|
-
import { type ColorDepth } from './color-depth.js';
|
|
5
|
-
import { type QuotaPeriod, type QuotaSnapshot, type QuotaSource } from './quota.js';
|
|
6
|
-
import type { DisconnectPolicyName } from './transcript-types.js';
|
|
7
|
-
export declare const WAIT_INDICATOR_MS = 8000;
|
|
8
|
-
/** Compact token count, matching the web stats line (517 / 12.2K / 1.2M). */
|
|
9
|
-
export declare function formatTokens(n: number): string;
|
|
10
|
-
/**
|
|
11
|
-
* Prompt occupancy of the next request, from DSH `contextPressure`.
|
|
12
|
-
* Provider-agnostic: uses the routed model's advertised window, not a
|
|
13
|
-
* hardcoded xAI size. Compaction-basic still owns in-turn pressure at 80%.
|
|
14
|
-
*/
|
|
15
|
-
export declare const CONTEXT_PRESSURE_WARN_RATIO = 0.8;
|
|
16
|
-
export declare const CONTEXT_PRESSURE_DANGER_RATIO = 0.95;
|
|
17
|
-
/** Idle auto-compact starts here so recovery finishes before the 80% in-turn trigger. */
|
|
18
|
-
export declare const CONTEXT_IDLE_COMPACT_RATIO = 0.72;
|
|
19
|
-
export interface ContextPressureSample {
|
|
20
|
-
usedTokens: number;
|
|
21
|
-
contextWindow: number;
|
|
22
|
-
}
|
|
23
|
-
export interface ContextPressureView {
|
|
24
|
-
usedTokens: number;
|
|
25
|
-
contextWindow: number;
|
|
26
|
-
percent: number;
|
|
27
|
-
level: 'ok' | 'warn' | 'danger';
|
|
28
|
-
}
|
|
29
|
-
/** Prompt-side occupancy of one usage sample: uncached input plus cache traffic. */
|
|
30
|
-
export declare function promptPressureTokens(usage: {
|
|
31
|
-
inputTokens: number;
|
|
32
|
-
cacheReadTokens?: number;
|
|
33
|
-
cacheWriteTokens?: number;
|
|
34
|
-
}): number;
|
|
35
|
-
/** Prefer the next-request projection; fall back to last-request pressure. */
|
|
36
|
-
export declare function contextPressureUsedTokens(pressure: {
|
|
37
|
-
projectedTokens?: number;
|
|
38
|
-
pressureTokens?: number;
|
|
39
|
-
} | undefined): number | undefined;
|
|
40
|
-
export declare function parseContextPressure(value: unknown): ContextPressureSample | undefined;
|
|
41
|
-
export declare function contextPressureView(sample: ContextPressureSample): ContextPressureView;
|
|
42
|
-
/**
|
|
43
|
-
* 8-segment Braille ring. Empty `⣀`; full `⣿`. Width is always 1 cell.
|
|
44
|
-
* Index is `ceil(percent / 12.5)` clamped to 0..8.
|
|
45
|
-
*/
|
|
46
|
-
export declare const CONTEXT_RING_EMPTY = "\u28C0";
|
|
47
|
-
export declare const CONTEXT_RING_SEGMENTS: readonly ["⣀", "⠉", "⠋", "⠛", "⠞", "⠟", "⠿", "⡿", "⣿"];
|
|
48
|
-
/** The full cell of the same Braille family, for bars built like the ring. */
|
|
49
|
-
export declare const CONTEXT_RING_FULL = "\u28FF";
|
|
50
|
-
export declare function formatContextPressureRing(percent: number): string;
|
|
51
|
-
export declare function contextPressureRingColor(level: ContextPressureView['level']): string;
|
|
52
|
-
export declare function formatContextPressureChip(view: ContextPressureView, color?: boolean): string;
|
|
53
|
-
export declare function formatContextPressureStatusLine(view: ContextPressureView | undefined): string;
|
|
54
|
-
export declare function contextPressureAlertText(view: ContextPressureView): string;
|
|
55
|
-
export declare function shouldIdleAutoCompact(view: ContextPressureView | undefined): boolean;
|
|
56
|
-
/** Compact duration, matching the web stats line (45.2s / 2m42s). */
|
|
57
|
-
export declare function formatDuration(ms: number): string;
|
|
58
|
-
export declare function formatTokensPerSecond(tokensPerSecond: number): string;
|
|
59
|
-
export declare function providerShortCode(provider: string): string;
|
|
60
|
-
export interface FooterStatsInput {
|
|
61
|
-
turns: number;
|
|
62
|
-
steps: number;
|
|
63
|
-
llmMs: number;
|
|
64
|
-
toolMs: number;
|
|
65
|
-
ttftMs: number;
|
|
66
|
-
ttftSteps: number;
|
|
67
|
-
decodeMs: number;
|
|
68
|
-
decodeTokens: number;
|
|
69
|
-
inputTokens: number;
|
|
70
|
-
outputTokens: number;
|
|
71
|
-
cacheReadTokens: number;
|
|
72
|
-
cacheWriteTokens: number;
|
|
73
|
-
}
|
|
74
|
-
/** Stats groups in drop order (last is dropped first when the row is too wide). */
|
|
75
|
-
export declare function footerStatsGroups(stats: FooterStatsInput): string[];
|
|
76
|
-
/**
|
|
77
|
-
* One group in the status strip, with the graphic-only form a narrow terminal
|
|
78
|
-
* keeps. `priority` orders the losses: 0 is the last thing to go.
|
|
79
|
-
*/
|
|
80
|
-
export interface FooterChip {
|
|
81
|
-
id: string;
|
|
82
|
-
/** Glyph plus text: what a wide terminal shows. */
|
|
83
|
-
long: string;
|
|
84
|
-
/** Glyph only, or `''` for a group that is pure text. */
|
|
85
|
-
short: string;
|
|
86
|
-
priority: number;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Fit an ordered strip into `width` cells, losing text before graphics.
|
|
2
|
+
* The footer, in four parts — and the module every caller has always imported.
|
|
90
3
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* again lowest priority first. The result never exceeds the width.
|
|
4
|
+
* `tui.ts` paints from here, `/status` reports from here, and a dozen tests
|
|
5
|
+
* reach in through this path. The pass that turned the strip into a status row
|
|
6
|
+
* split the file along the four responsibilities that had grown together inside
|
|
7
|
+
* it; this module stays as the barrel so that split cost no call site:
|
|
96
8
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*/
|
|
102
|
-
export declare function fitFooterChips(chips: readonly FooterChip[], width: number, separator?: string): string;
|
|
103
|
-
/**
|
|
104
|
-
* The roster's health as a chip: absent means `/mode` cannot switch presets and
|
|
105
|
-
* the preset-owned tools are missing. It leads the strip and keeps its glyph
|
|
106
|
-
* longest, because it is the one group that reports a broken install.
|
|
107
|
-
*/
|
|
108
|
-
export declare function footerHealthChip(missing: boolean, color?: boolean,
|
|
109
|
-
/** Which rows are missing: the 0.1.5 roster, or the 0.1.7 agent plane. */
|
|
110
|
-
kind?: 'roster' | 'agent-plane'): FooterChip | undefined;
|
|
111
|
-
export declare function fitFooterStatsLine(chip: string, groups: readonly string[], width: number): string;
|
|
112
|
-
export type FooterActivityKind = 'plan-review' | 'waiting' | 'compacting' | 'retry' | 'subagents' | 'tools' | 'plan-open' | 'plan-pending' | 'goal' | 'waiting-llm' | 'idle';
|
|
113
|
-
export interface FooterStatusInput {
|
|
114
|
-
running: boolean;
|
|
115
|
-
planReview: boolean;
|
|
116
|
-
waitingQuestion: boolean;
|
|
117
|
-
compacting: boolean;
|
|
118
|
-
retry?: {
|
|
119
|
-
retry: number;
|
|
120
|
-
maxRetries: number;
|
|
121
|
-
};
|
|
122
|
-
subagents: number;
|
|
123
|
-
tools: number;
|
|
124
|
-
planLeftOpen: boolean;
|
|
125
|
-
planPending: boolean;
|
|
126
|
-
planActive: boolean;
|
|
127
|
-
goalPhase?: 'active' | 'paused' | 'blocked';
|
|
128
|
-
idleMs: number;
|
|
129
|
-
model: string;
|
|
130
|
-
effort?: string;
|
|
131
|
-
preset?: string;
|
|
132
|
-
provider: string;
|
|
133
|
-
parentModel: string;
|
|
134
|
-
subModel: string;
|
|
135
|
-
/** Explicit `/submodel` provider override; undefined means "inherit parent". */
|
|
136
|
-
subProvider?: string;
|
|
137
|
-
/** Explicit `/subeffort` override for subagent children. */
|
|
138
|
-
subEffort?: string;
|
|
139
|
-
quotaCode?: string;
|
|
140
|
-
quotaPercent?: number;
|
|
141
|
-
/** Which rolling window `quotaPercent` belongs to: tagged `5Hr`/`1Wk`/`1Mo`. */
|
|
142
|
-
quotaPeriod?: QuotaPeriod;
|
|
143
|
-
/** The provider has a quota surface but no reading yet: paint `░░░░░░░░ ?%`. */
|
|
144
|
-
quotaUnknown?: boolean;
|
|
145
|
-
contextChip?: string;
|
|
146
|
-
balanceText?: string;
|
|
147
|
-
search?: {
|
|
148
|
-
index: number;
|
|
149
|
-
total: number;
|
|
150
|
-
};
|
|
151
|
-
foldedInput: boolean;
|
|
152
|
-
multiLineInput: boolean;
|
|
153
|
-
queued: number;
|
|
154
|
-
cwdLabel?: string;
|
|
155
|
-
compactView?: boolean;
|
|
156
|
-
}
|
|
157
|
-
export declare function footerActivity(input: FooterStatusInput): {
|
|
158
|
-
kind: FooterActivityKind;
|
|
159
|
-
text: string;
|
|
160
|
-
};
|
|
161
|
-
/** Short remaining-quota bar: 8 pips, filled from the left. */
|
|
162
|
-
export declare function formatQuotaBar(remainingPercent: number, width?: number): string;
|
|
163
|
-
/**
|
|
164
|
-
* `sub:<model>` chip for the identity row: `sub:grok-4.5`, or
|
|
165
|
-
* `sub:grok-4.5(xhigh)` when an explicit `/subeffort` is set.
|
|
9
|
+
* - `footer-format.ts` — a value in, text out (and the pressure reading).
|
|
10
|
+
* - `footer-meters.ts` — the three meter primitives.
|
|
11
|
+
* - `footer-budget.ts` — the responsive degradation ladder and the row.
|
|
12
|
+
* - `footer-layout.ts` — the two chrome rows, and `/status`.
|
|
166
13
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*/
|
|
171
|
-
export declare function subagentRouteLabel(model: string, provider?: string, effort?: string): string;
|
|
172
|
-
/**
|
|
173
|
-
* Paint the `sub:` chip on an otherwise muted status line.
|
|
14
|
+
* The one helper they all share — `accent`, the "paint this run in this role's
|
|
15
|
+
* colour" primitive — lives in `color-depth.ts` beside `downgradeSgr`, which is
|
|
16
|
+
* where "what can this terminal take" is already answered.
|
|
174
17
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* back onto the parent's own provider) keeps the identity row's mute — the
|
|
178
|
-
* accent means "this route is not your parent's", and spending it on every
|
|
179
|
-
* child made the following case look pinned.
|
|
180
|
-
*
|
|
181
|
-
* The mute (`90`) is reopened after the chip so the rest of the identity row
|
|
182
|
-
* stays dim.
|
|
183
|
-
*/
|
|
184
|
-
export declare function paintFooterSubagentChip(line: string, chip: string, foreign: boolean, muteSgr?: string, depth?: ColorDepth, identitySgr?: string): string;
|
|
185
|
-
export declare function footerSubagentForeign(input: Pick<FooterStatusInput, 'provider' | 'subProvider'>): boolean;
|
|
186
|
-
/**
|
|
187
|
-
* The model as the footer shows it: the model's own name, without a
|
|
188
|
-
* `provider/model` route prefix.
|
|
189
|
-
*
|
|
190
|
-
* Some providers carry the vendor in the model id (`xai/grok-4.6`,
|
|
191
|
-
* `deepseek-official/deepseek-v4-flash`). The route is worth a row in the header
|
|
192
|
-
* and in `/status`; in the status line it spent cells on something the reader
|
|
193
|
-
* already knows, and it pushed the quota badge towards the drop edge.
|
|
194
|
-
* `sub:` routes keep their provider on purpose — that is a *different* route
|
|
195
|
-
* from the parent's, and hiding it would make the two indistinguishable.
|
|
196
|
-
*/
|
|
197
|
-
export declare function shortModelName(model: string): string;
|
|
198
|
-
export declare function footerIdentityParts(input: FooterStatusInput): string[];
|
|
199
|
-
/**
|
|
200
|
-
* `SuperGrok 5Hr ███████░ 82%` — short plan badge, the window the number belongs
|
|
201
|
-
* to, then the bar.
|
|
202
|
-
*
|
|
203
|
-
* The tags are fixed technical labels (`5Hr` / `1Wk` / `1Mo`) rather than
|
|
204
|
-
* localized words: they sit in a footer that is already fighting for cells, and
|
|
205
|
-
* a window tag that changes with the locale would be unreadable in a screenshot
|
|
206
|
-
* or a bug report. Everything else about the window (label, reset time, detail)
|
|
207
|
-
* stays in `/quota`.
|
|
208
|
-
*/
|
|
209
|
-
export declare function formatFooterQuota(percent: number, code?: string, period?: QuotaPeriod): string;
|
|
210
|
-
/**
|
|
211
|
-
* The quota widget before the first reading arrives, or while the provider
|
|
212
|
-
* cannot be reached: an empty bar and a question mark.
|
|
213
|
-
*
|
|
214
|
-
* Deliberately not `0%`: an unreachable quota API is not a used-up quota, and a
|
|
215
|
-
* number the footer cannot back up is worse than no number. The bar is there
|
|
216
|
-
* from the first frame so the row does not jump when the reading lands.
|
|
217
|
-
*/
|
|
218
|
-
export declare function formatQuotaUnknown(): string;
|
|
219
|
-
/** Fixed-width tag for the window a footer quota number belongs to. */
|
|
220
|
-
export declare function quotaWindowTag(period: QuotaPeriod | undefined): string;
|
|
221
|
-
/**
|
|
222
|
-
* The badge the footer shows instead of the billing plan's own label:
|
|
223
|
-
* `SuperGrok`, `OC·GO` (OpenCode Go), `CC·GOAT` (Command Code Goat).
|
|
224
|
-
*
|
|
225
|
-
* The long names live in `/quota`; this one has to survive next to a model id.
|
|
226
|
-
* Without a `source` (a hand-built snapshot) the plan label is matched instead,
|
|
227
|
-
* so old callers and tests keep a sensible badge.
|
|
228
|
-
*/
|
|
229
|
-
export declare function shortQuotaPlanName(snapshot: {
|
|
230
|
-
plan: string;
|
|
231
|
-
source?: QuotaSource;
|
|
232
|
-
}): string;
|
|
233
|
-
/**
|
|
234
|
-
* Drop the Go / SuperGrok plan name from a quota identity part, keeping the
|
|
235
|
-
* remaining-percent bar. Returns true when a part was rewritten.
|
|
236
|
-
*/
|
|
237
|
-
export declare function dropFooterQuotaPlanName(parts: string[]): boolean;
|
|
238
|
-
export declare function fitFooterStatusLine(activity: string, identity: readonly string[], width: number): string;
|
|
239
|
-
export interface StatusReportInput {
|
|
240
|
-
sessionId: string;
|
|
241
|
-
pluginVersion: string;
|
|
242
|
-
provider: string;
|
|
243
|
-
model: string;
|
|
244
|
-
effort?: string;
|
|
245
|
-
agentStatus: string;
|
|
246
|
-
preset: string;
|
|
247
|
-
activeSubagents: number;
|
|
248
|
-
plan: 'off' | 'pending' | 'on';
|
|
249
|
-
paint: string;
|
|
250
|
-
disconnect?: DisconnectPolicyName;
|
|
251
|
-
waitingQuestions: number;
|
|
252
|
-
quota?: QuotaSnapshot;
|
|
253
|
-
context?: ContextPressureView;
|
|
254
|
-
parentModel?: string;
|
|
255
|
-
subProvider?: string;
|
|
256
|
-
subModel: string;
|
|
257
|
-
cwd?: string;
|
|
258
|
-
}
|
|
259
|
-
/** Lines printed by `/status` — SSH first-boot diagnostics, no extra command. */
|
|
260
|
-
export declare function formatStatusReport(input: StatusReportInput): string[];
|
|
261
|
-
/** Human-facing kind for a live LLM route. */
|
|
262
|
-
export declare function describeProviderRoute(provider: string): {
|
|
263
|
-
kind: string;
|
|
264
|
-
short: string;
|
|
265
|
-
};
|
|
266
|
-
/** Routes that authenticate without a harness API-key credential. */
|
|
267
|
-
export declare function providerUsesLocalOAuth(provider: string): boolean;
|
|
268
|
-
/**
|
|
269
|
-
* Whether a provider has a quota surface at all, without asking it.
|
|
18
|
+
* Import from here unless you are inside the footer; the four modules import
|
|
19
|
+
* each other in exactly that order and never back.
|
|
270
20
|
*
|
|
271
|
-
*
|
|
272
|
-
* until a reading arrives — so it needs to tell "this provider will have one"
|
|
273
|
-
* from "this provider reports a balance instead" (DeepSeek) or nothing at all.
|
|
274
|
-
* Every branch is a synchronous settings read, so a repaint never does IO.
|
|
21
|
+
* @module dsh-ssh-tui/footer
|
|
275
22
|
*/
|
|
276
|
-
export
|
|
23
|
+
export * from './footer-format.js';
|
|
24
|
+
export * from './footer-meters.js';
|
|
25
|
+
export * from './footer-budget.js';
|
|
26
|
+
export * from './footer-layout.js';
|
package/lib/types/paint.d.ts
CHANGED
|
@@ -37,6 +37,35 @@ export declare function ignoreFurtherHangupSignals(): void;
|
|
|
37
37
|
export declare function waitUntilIdleOrTimeout(isIdle: () => boolean, timeoutMs: number, now?: () => number, wait?: (ms: number) => Promise<void>): Promise<'idle' | 'timeout'>;
|
|
38
38
|
/** Map a CSI-6n round-trip to a paint cadence. Unknown RTT uses the SSH default. */
|
|
39
39
|
export declare function paintIntervalForRtt(rttMs: number | undefined): number;
|
|
40
|
+
/**
|
|
41
|
+
* Where the paint cadence came from: a measured round trip, an explicit
|
|
42
|
+
* `DSH_TUI_PAINT_MS`, a local TTY's constant, or a probe that never answered.
|
|
43
|
+
*/
|
|
44
|
+
export type PaintCadenceSource = 'measured' | 'configured' | 'local' | 'unprobed';
|
|
45
|
+
/** The redraw budget a drag assumes on a link whose round trip was never measured. */
|
|
46
|
+
export declare const RESIZE_UNKNOWN_LINK_BUDGET_MS = 200;
|
|
47
|
+
/**
|
|
48
|
+
* How long one frame takes to reach the terminal, for pacing a drag.
|
|
49
|
+
*
|
|
50
|
+
* Two of the four sources are an *answer*: `paintIntervalForRtt` already turned a
|
|
51
|
+
* measured round trip into a cadence, and an explicit `DSH_TUI_PAINT_MS` is the
|
|
52
|
+
* user's own. The other two are not, and they are not the same kind of guess:
|
|
53
|
+
*
|
|
54
|
+
* - a local TTY's cadence is a constant about a link that is not a link, so it is
|
|
55
|
+
* used as it stands;
|
|
56
|
+
* - an **unprobed** link's interval is the SSH default — a number nobody measured
|
|
57
|
+
* — and pacing frames by it composes them faster than the link can deliver them,
|
|
58
|
+
* which queues stale geometries behind each other and reads as a drag that
|
|
59
|
+
* freezes and jumps. That case gets a deliberately slow budget instead.
|
|
60
|
+
*
|
|
61
|
+
* Nothing here claims a round trip: the status row keeps painting `SSH ○○○○ 160ms`
|
|
62
|
+
* when the probe never answered, and this budget stays an internal pacing floor.
|
|
63
|
+
* @param link - which kind of link the session is on.
|
|
64
|
+
* @param source - where the cadence came from.
|
|
65
|
+
* @param intervalMs - the cadence in force.
|
|
66
|
+
* @returns milliseconds one frame needs, at least.
|
|
67
|
+
*/
|
|
68
|
+
export declare function linkRedrawBudgetMs(link: PaintLinkKind, source: PaintCadenceSource, intervalMs: number): number;
|
|
40
69
|
/**
|
|
41
70
|
* How many measurements the link's reported round-trip is taken over.
|
|
42
71
|
*
|
|
@@ -104,6 +133,25 @@ export interface PaintOptions {
|
|
|
104
133
|
cursorColumn: number;
|
|
105
134
|
/** Keep the cursor hidden (session picker). Default shows it on the input. */
|
|
106
135
|
hideCursor?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Never touch a row above this index, even when the size changed.
|
|
138
|
+
*
|
|
139
|
+
* A window drag repaints the whole screen on every event, and a frame's size is
|
|
140
|
+
* proportional to the width, so the *growing* direction is the expensive one:
|
|
141
|
+
* each frame is bigger than the last while the terminal drains at a fixed rate,
|
|
142
|
+
* the backlog compounds, and the screen keeps moving after the pointer has
|
|
143
|
+
* stopped (measured against a byte-rate-limited consumer: 30–44 ms of leftover
|
|
144
|
+
* drain growing, against 0–10 ms shrinking). The transcript is not what that
|
|
145
|
+
* frame is needed for — the terminal reflows the grid it already holds — so a
|
|
146
|
+
* drag paints the chrome and leaves the transcript to the terminal: a frame
|
|
147
|
+
* about 1/6 the size, flat in the column count. `paintTailBudget` already
|
|
148
|
+
* decides when the transcript may be left behind; this is the frame's half of
|
|
149
|
+
* the same decision.
|
|
150
|
+
*
|
|
151
|
+
* A frame with `dirtyFrom` never clears the screen: `\x1b[H\x1b[J` is precisely
|
|
152
|
+
* what it must not do.
|
|
153
|
+
*/
|
|
154
|
+
dirtyFrom?: number;
|
|
107
155
|
}
|
|
108
156
|
export interface PaintFrame {
|
|
109
157
|
/** The single stdout write for this frame. */
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan artifact, projected from durable session events (B2.5).
|
|
3
|
+
*
|
|
4
|
+
* A plan is an **artifact**, not a row: it has an identity that outlives the card
|
|
5
|
+
* drawn for it, a revision history, and a state that comes from the session log
|
|
6
|
+
* rather than from whatever the renderer last painted. Everything here is a pure
|
|
7
|
+
* fold over events, so a replay produces exactly the artifacts a live session did —
|
|
8
|
+
* the same ids, the same revisions, the same states. That is the whole point of the
|
|
9
|
+
* module: the transcript, the dock and the review Surface read *this*, and none of
|
|
10
|
+
* them keeps plan state of its own (AD-15).
|
|
11
|
+
*
|
|
12
|
+
* What is durable (measured; see `docs/plans/b2-5-plan-artifact-audit.md`):
|
|
13
|
+
*
|
|
14
|
+
* `plan/mode { active }` — plan mode entered / left (plan-mode service)
|
|
15
|
+
* `todo/write { todos }` — a whole-list snapshot (the `todo_write` tool)
|
|
16
|
+
* `exit_plan_mode` `tool/call` — `arguments.plan` is the plan body; the call is
|
|
17
|
+
* the review's identity
|
|
18
|
+
* `exit_plan_mode` `tool/result` — success = approved; the harness's own refusal
|
|
19
|
+
* sentences = rejected / dismissed
|
|
20
|
+
* `command/run` + `command/done` — a `/plan` switch that is queued for the next step
|
|
21
|
+
*
|
|
22
|
+
* What is **not** durable, and therefore is never claimed here:
|
|
23
|
+
*
|
|
24
|
+
* - the plan-review *question*: `dsh-plan-mode` asks through
|
|
25
|
+
* `ctx.get('userQuestions').ask(...)`, which appends nothing. The tool call is the
|
|
26
|
+
* review's only durable trace — which is also enough to recognise it on replay,
|
|
27
|
+
* something B2.4 could not do from the question alone;
|
|
28
|
+
* - any plan name or id: derived below, deterministically, from event order;
|
|
29
|
+
* - "completed" and "abandoned": no event says either. They are folded from the last
|
|
30
|
+
* snapshot (every step completed) and from the mode switch, and they carry
|
|
31
|
+
* {@link PlanProvenance} `inferred` so a reader is never told them as fact.
|
|
32
|
+
*
|
|
33
|
+
* @module dsh-ssh-tui/plan-projection
|
|
34
|
+
*/
|
|
35
|
+
import type { PlanTodoItem } from './transcript-types.js';
|
|
36
|
+
/** How the artifact's state is known. `durable` = an event says it; `inferred` = a reading. */
|
|
37
|
+
export type PlanProvenance = 'durable' | 'inferred';
|
|
38
|
+
/** What the artifact is doing. `unknown` is a real answer, not a placeholder. */
|
|
39
|
+
export type PlanState = 'draft' | 'reviewing' | 'approved' | 'rejected' | 'executing' | 'completed' | 'abandoned' | 'unknown';
|
|
40
|
+
/** One whole-list snapshot, with the durable sequence that produced it. */
|
|
41
|
+
export interface PlanRevision {
|
|
42
|
+
/** `plan@<openSeq>#<writeSeq>` — deterministic, and unique within a session. */
|
|
43
|
+
id: string;
|
|
44
|
+
/** `seq` of the `todo/write` that produced this revision. */
|
|
45
|
+
seq: number;
|
|
46
|
+
steps: PlanTodoItem[];
|
|
47
|
+
}
|
|
48
|
+
/** The review a plan was put through, and what came back. */
|
|
49
|
+
export interface PlanReview {
|
|
50
|
+
/** The `exit_plan_mode` call id: the review's identity, durable and replay-stable. */
|
|
51
|
+
id: string;
|
|
52
|
+
seq: number;
|
|
53
|
+
/** The plan body as it was reviewed (`arguments.plan`). */
|
|
54
|
+
body: string;
|
|
55
|
+
/**
|
|
56
|
+
* `pending` is a *live* state: a request is out and nobody has answered yet. A
|
|
57
|
+
* replayed log that ends here says `unknown` instead — the Host that asked is gone,
|
|
58
|
+
* and B2.4 set that precedent for approvals.
|
|
59
|
+
*/
|
|
60
|
+
outcome: 'pending' | 'approved' | 'rejected' | 'dismissed' | 'unknown';
|
|
61
|
+
/** The reader's own words, when they refused with feedback. */
|
|
62
|
+
feedback?: string;
|
|
63
|
+
}
|
|
64
|
+
/** One plan, folded from the events that describe it. */
|
|
65
|
+
export interface PlanArtifact {
|
|
66
|
+
/** `plan@<seq of the opening event>` — session-local, replay-stable. */
|
|
67
|
+
id: string;
|
|
68
|
+
/** What opened it: plan mode, or a standing todo list. */
|
|
69
|
+
openedBy: 'plan-mode' | 'todos';
|
|
70
|
+
openSeq: number;
|
|
71
|
+
state: PlanState;
|
|
72
|
+
provenance: PlanProvenance;
|
|
73
|
+
/** The plan body, when the session recorded one. */
|
|
74
|
+
body?: string;
|
|
75
|
+
/** The latest revision's steps. */
|
|
76
|
+
steps: PlanTodoItem[];
|
|
77
|
+
revisions: PlanRevision[];
|
|
78
|
+
review?: PlanReview;
|
|
79
|
+
/** Plan mode is on, as of the end of the fold. */
|
|
80
|
+
active: boolean;
|
|
81
|
+
/** A `/plan` switch is queued for the next step (derived, not read from the Host). */
|
|
82
|
+
pending: boolean;
|
|
83
|
+
/** `seq` of the event that closed the artifact, when one did. */
|
|
84
|
+
closedSeq?: number;
|
|
85
|
+
}
|
|
86
|
+
/** The slice of a session event this fold reads. */
|
|
87
|
+
export interface PlanEvent {
|
|
88
|
+
type: string;
|
|
89
|
+
seq?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Whether this event arrived from the running Host (as opposed to a log read).
|
|
92
|
+
*
|
|
93
|
+
* Only a live `exit_plan_mode` can be `reviewing`: a resumed process never carries
|
|
94
|
+
* on a review the Host that asked it did not finish, so a replayed unresolved call
|
|
95
|
+
* is `unknown`. Recorded per event because one log holds both.
|
|
96
|
+
*/
|
|
97
|
+
live?: boolean;
|
|
98
|
+
data?: {
|
|
99
|
+
active?: unknown;
|
|
100
|
+
todos?: unknown;
|
|
101
|
+
name?: unknown;
|
|
102
|
+
args?: unknown;
|
|
103
|
+
commandId?: unknown;
|
|
104
|
+
kind?: unknown;
|
|
105
|
+
arguments?: unknown;
|
|
106
|
+
callId?: unknown;
|
|
107
|
+
message?: {
|
|
108
|
+
source?: {
|
|
109
|
+
callId?: unknown;
|
|
110
|
+
};
|
|
111
|
+
content?: unknown;
|
|
112
|
+
isError?: unknown;
|
|
113
|
+
};
|
|
114
|
+
error?: {
|
|
115
|
+
code?: unknown;
|
|
116
|
+
message?: unknown;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The harness's own refusal sentences for a plan review.
|
|
122
|
+
*
|
|
123
|
+
* Matched exactly, because they are the *only* durable evidence that a review was
|
|
124
|
+
* refused rather than never asked: `keep planning` (with or without feedback) and the
|
|
125
|
+
* dismissal that happens when the reader speaks instead of answering.
|
|
126
|
+
*/
|
|
127
|
+
export declare function reviewRefusal(text: string): {
|
|
128
|
+
outcome: 'rejected' | 'dismissed';
|
|
129
|
+
feedback?: string;
|
|
130
|
+
} | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Fold the durable events of one session into its plan artifacts.
|
|
133
|
+
*
|
|
134
|
+
* Deterministic by construction: ids are derived from event `seq` alone, and every
|
|
135
|
+
* decision reads only events that precede it. The same log (live or replayed) folds
|
|
136
|
+
* to the same list, which is what lets one artifact feed the transcript, the dock and
|
|
137
|
+
* the review Surface without any of them keeping state.
|
|
138
|
+
*
|
|
139
|
+
* @param events - the session's events, in order. Unknown types are ignored.
|
|
140
|
+
* @param options - `live` marks a fold whose last events come from a running Host, so
|
|
141
|
+
* an unresolved review may be called `reviewing`; a replay leaves it `unknown`.
|
|
142
|
+
* @returns every artifact the session accounts for, oldest first.
|
|
143
|
+
*/
|
|
144
|
+
export declare function foldPlanArtifacts(events: readonly PlanEvent[], options?: {
|
|
145
|
+
live?: boolean;
|
|
146
|
+
}): PlanArtifact[];
|
|
147
|
+
/** The artifact a workspace should dock: the newest one that is still live. */
|
|
148
|
+
export declare function livePlanArtifact(artifacts: readonly PlanArtifact[]): PlanArtifact | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* Whether the artifact still describes work in progress.
|
|
151
|
+
*
|
|
152
|
+
* The same question `planIsLive` asked of a row, asked of the projection instead:
|
|
153
|
+
* plan mode on, a switch queued, a step not finished, or a review awaiting its
|
|
154
|
+
* answer. Everything else is history, and history belongs in the transcript.
|
|
155
|
+
*/
|
|
156
|
+
export declare function artifactIsLive(artifact: PlanArtifact): boolean;
|
|
157
|
+
/** `3/7`-style progress over the latest revision. */
|
|
158
|
+
export declare function artifactProgress(artifact: PlanArtifact): {
|
|
159
|
+
done: number;
|
|
160
|
+
total: number;
|
|
161
|
+
current?: PlanTodoItem;
|
|
162
|
+
};
|
package/lib/types/plan.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { type Theme } from './theme.js';
|
|
5
5
|
import { type TextSegment } from './term-text.js';
|
|
6
|
+
import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-questions';
|
|
6
7
|
import type { DiffDisplayLine, DisplayKind, PlanTodoItem, Row, SubagentLogEntry } from './transcript-types.js';
|
|
7
8
|
export declare const MAX_SUBAGENT_LOGS = 80;
|
|
8
9
|
export declare const TODO_STATUS_MARK: Record<PlanTodoItem['status'], string>;
|
|
@@ -80,6 +81,18 @@ export declare function parsePlanTodos(value: unknown): PlanTodoItem[];
|
|
|
80
81
|
export declare function todoSummary(value: unknown): string;
|
|
81
82
|
/** Compact ask_user_question summary from tool arguments. */
|
|
82
83
|
export declare function askSummary(value: unknown): string;
|
|
84
|
+
/**
|
|
85
|
+
* The questions one `ask_user_question` call recorded, for the durable fold.
|
|
86
|
+
*
|
|
87
|
+
* The Session's own reader (`questionsOf` in `@deepseek-ai/dsh-user-questions`)
|
|
88
|
+
* validates against the tool's schema and returns nothing when it does not match.
|
|
89
|
+
* This one only has to be faithful enough to draw a card from a log entry this
|
|
90
|
+
* process did not witness, so it takes the fields it can use and drops the rest
|
|
91
|
+
* rather than refusing the whole call.
|
|
92
|
+
* @param value - the tool call's arguments, raw JSON or already parsed.
|
|
93
|
+
* @returns the questions, or an empty list when the shape is unusable.
|
|
94
|
+
*/
|
|
95
|
+
export declare function askQuestions(value: unknown): readonly AskUserQuestionItem[];
|
|
83
96
|
/** First non-empty line, collapsed to a single scan line. */
|
|
84
97
|
export declare function firstDisplayLine(text: string): string;
|
|
85
98
|
/** Collapse a child-session blob to one short chip/wait-card line. */
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable half of an ask-user question, as the Session reports it.
|
|
3
|
+
*
|
|
4
|
+
* A question used to exist only as a live request: the dialog the answerer opened
|
|
5
|
+
* and the row it pushed. Nothing rebuilt it from the log, so `--resume` showed the
|
|
6
|
+
* `ask_user_question` *tool* card and nothing that said what was asked or answered —
|
|
7
|
+
* the same session read differently depending on whether it was lived or replayed.
|
|
8
|
+
*
|
|
9
|
+
* The Session already records the truth: `tool/call` carries the questions and
|
|
10
|
+
* `tool/result` (or a late `user-question-reply` message) carries the answers, and
|
|
11
|
+
* `@deepseek-ai/dsh-user-questions` folds both into a projection every Client can
|
|
12
|
+
* read (`ctx.sessionProjections.stateOf(session, 'userQuestions')`). This module
|
|
13
|
+
* turns that view into the records the transcript paints, and it is deliberately
|
|
14
|
+
* pure: the caller supplies the questions, because the projection's *settled* half
|
|
15
|
+
* names only the call and the answers.
|
|
16
|
+
*
|
|
17
|
+
* Two limits are the Harness's, not this module's, and both are why the caller
|
|
18
|
+
* keeps its live path as well:
|
|
19
|
+
*
|
|
20
|
+
* - the projection tracks only **timed** `ask_user_question` calls — a call made
|
|
21
|
+
* while the blocking legacy schema was in effect appears in neither half, so a
|
|
22
|
+
* session that only ever used it folds to an empty view;
|
|
23
|
+
* - it needs the projection service to be registered at all, which a bare Context
|
|
24
|
+
* in a test (or an embedder without `dsh-session-projection`) does not provide.
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-ssh-tui/question-state
|
|
27
|
+
*/
|
|
28
|
+
import type { AskUserQuestionAnswerItem, AskUserQuestionItem } from '@deepseek-ai/dsh-user-questions';
|
|
29
|
+
/**
|
|
30
|
+
* The projection's two halves, declared here as the *shape this plugin reads*
|
|
31
|
+
* rather than imported by name.
|
|
32
|
+
*
|
|
33
|
+
* `PendingUserQuestion`, `SettledUserQuestion` and `UserQuestionProjectionView` are
|
|
34
|
+
* named exports of `dsh-user-questions` only from 0.2.0-rc.2 on: the same module on
|
|
35
|
+
* 0.1.7-rc.x and 0.2.0-rc.1 exports a different surface, and importing the names made
|
|
36
|
+
* the plugin fail to *compile* there — which is how three CI legs went red at 0.8.2's
|
|
37
|
+
* first push. Nothing here needs the host's names: every field below is already read
|
|
38
|
+
* defensively (`questionViewOf` takes `unknown` and parses it), and the two item types
|
|
39
|
+
* are exported by every line the plugin declares.
|
|
40
|
+
*/
|
|
41
|
+
export interface QuestionViewPending {
|
|
42
|
+
readonly callId: string;
|
|
43
|
+
readonly questions: readonly AskUserQuestionItem[];
|
|
44
|
+
/** `open` while the window is live, `continued` once it closed but still accepts. */
|
|
45
|
+
readonly state?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface QuestionViewSettled {
|
|
48
|
+
readonly callId: string;
|
|
49
|
+
readonly answers?: readonly AskUserQuestionAnswerItem[];
|
|
50
|
+
}
|
|
51
|
+
/** Both halves of one Session's timed `ask_user_question` state. */
|
|
52
|
+
export interface QuestionView {
|
|
53
|
+
readonly active: readonly QuestionViewPending[];
|
|
54
|
+
readonly settled: readonly QuestionViewSettled[];
|
|
55
|
+
}
|
|
56
|
+
/** One question as the transcript should show it, live or replayed. */
|
|
57
|
+
export interface DurableQuestionRecord {
|
|
58
|
+
/** The `ask_user_question` call this question belongs to. */
|
|
59
|
+
callId: string;
|
|
60
|
+
question: AskUserQuestionItem;
|
|
61
|
+
/**
|
|
62
|
+
* `open` is a foreground wait nobody has answered yet; `continued` is one whose
|
|
63
|
+
* window closed but which the Session still accepts an answer for; the other two
|
|
64
|
+
* are terminal.
|
|
65
|
+
*/
|
|
66
|
+
state: 'open' | 'continued' | 'answered' | 'cancelled';
|
|
67
|
+
/** What the human chose; empty while the question is unanswered. */
|
|
68
|
+
answers: readonly AskUserQuestionAnswerItem[];
|
|
69
|
+
}
|
|
70
|
+
/** The projection view, or the empty one when nothing is registered. */
|
|
71
|
+
export declare function questionViewOf(value: unknown): QuestionView | undefined;
|
|
72
|
+
/** Whether the projection has anything to say about this session's questions. */
|
|
73
|
+
export declare function questionViewIsEmpty(view: QuestionView | undefined): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Every question the Session log can still account for, in ask order then
|
|
76
|
+
* settlement order.
|
|
77
|
+
*
|
|
78
|
+
* A settled call names only its answers, so a call whose questions this process
|
|
79
|
+
* never saw (a resumed session whose `tool/call` predates the log window it read)
|
|
80
|
+
* contributes nothing rather than a half-record: an answer with no question is not
|
|
81
|
+
* something the transcript can draw.
|
|
82
|
+
* @param view - the projection view, or undefined when nothing is registered.
|
|
83
|
+
* @param questionsOf - the questions recorded for one call, when known.
|
|
84
|
+
* @returns one record per question, newest last.
|
|
85
|
+
*/
|
|
86
|
+
export declare function durableQuestionRecords(view: QuestionView | undefined, questionsOf: (callId: string) => readonly AskUserQuestionItem[] | undefined): DurableQuestionRecord[];
|
|
87
|
+
/**
|
|
88
|
+
* How one answered question reads on its card.
|
|
89
|
+
*
|
|
90
|
+
* Mirrors what the live dialog writes when it settles a card, so a session that
|
|
91
|
+
* was replayed shows the same sentence as one that was lived: the typed answer
|
|
92
|
+
* when there was one, the chosen labels otherwise.
|
|
93
|
+
* @param answers - the recorded answers for this question.
|
|
94
|
+
* @returns the summary text.
|
|
95
|
+
*/
|
|
96
|
+
export declare function answerSummaryText(answers: readonly AskUserQuestionAnswerItem[]): string;
|