dsh-ssh-tui 0.8.3 → 0.8.4-rc.1
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 +68 -0
- package/README.en.md +17 -15
- package/README.md +11 -8
- package/docs/release-notes-0.8.4-rc.1.md +271 -0
- package/lib/dsh-compat.js +22 -0
- package/lib/dsh-compat.js.map +1 -1
- package/lib/footer-budget.js +1 -7
- package/lib/footer-budget.js.map +1 -1
- package/lib/input-reader.js +415 -0
- package/lib/input-reader.js.map +1 -0
- package/lib/input-router.js +82 -0
- package/lib/input-router.js.map +1 -0
- package/lib/terminal-input.js +14 -10
- package/lib/terminal-input.js.map +1 -1
- package/lib/tui.js +652 -467
- package/lib/tui.js.map +1 -1
- package/lib/types/dsh-compat.d.ts +20 -1
- package/lib/types/footer-budget.d.ts +0 -9
- package/lib/types/input-reader.d.ts +198 -0
- package/lib/types/input-router.d.ts +73 -0
- package/lib/types/terminal-input.d.ts +15 -1
- package/lib/types/tui.d.ts +178 -14
- package/package.json +88 -87
- package/lib/footer-accents.js +0 -28
- package/lib/footer-accents.js.map +0 -1
- package/lib/types/footer-accents.d.ts +0 -22
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* here picks the API that is actually present so one build runs on either
|
|
10
10
|
* host line.
|
|
11
11
|
*/
|
|
12
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
12
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
13
14
|
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
14
|
-
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
15
|
+
import type { SessionId, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
15
16
|
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
16
17
|
import type z from '@deepseek-ai/schemastery';
|
|
17
18
|
/**
|
|
@@ -294,3 +295,21 @@ export declare function streamChunkOf(frame: unknown, fallback?: {
|
|
|
294
295
|
export declare function listenHostEvent(ctx: {
|
|
295
296
|
on: (event: never, handler: never) => unknown;
|
|
296
297
|
}, event: string, handler: (...args: unknown[]) => unknown): () => void;
|
|
298
|
+
/**
|
|
299
|
+
* Wind down the selected direct children of one parent, under both names the
|
|
300
|
+
* subagent service has shipped.
|
|
301
|
+
*
|
|
302
|
+
* 0.2.1-rc.2 renamed `drainContinuableChildren(parent, childIds)` to
|
|
303
|
+
* `drainChildren(parent, childIds)` — same contract (release the selected
|
|
304
|
+
* resident direct children of one exact live parent, leaving its other children
|
|
305
|
+
* admitted). Both 0.2.0 rcs and 0.2.1-rc.2 are declared, so the call site asks
|
|
306
|
+
* for whichever spelling the host has instead of pinning one and failing to
|
|
307
|
+
* compile on the other. A composition without the service at all resolves: the
|
|
308
|
+
* caller has already handled "no service", and this keeps the helper total.
|
|
309
|
+
*
|
|
310
|
+
* @param subagents - the host's subagent service.
|
|
311
|
+
* @param parent - the exact live parent whose children are being released.
|
|
312
|
+
* @param childIds - durable direct-child ids to release when resident.
|
|
313
|
+
* @returns once every selected activation released its execution handle.
|
|
314
|
+
*/
|
|
315
|
+
export declare function drainChildSubagents(subagents: Context['subagents'], parent: Agent, childIds: readonly SessionId[]): Promise<void>;
|
|
@@ -83,15 +83,6 @@ export interface FooterStripInput {
|
|
|
83
83
|
long: string;
|
|
84
84
|
short: string;
|
|
85
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
86
|
/** Whether the terminal must be painted with ASCII glyphs. */
|
|
96
87
|
ascii?: boolean;
|
|
97
88
|
/** Colour depth the accents are downgraded for. */
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a read that could still grow into a sequence is held.
|
|
3
|
+
*
|
|
4
|
+
* The window is what makes a lone `ESC` (the reader pressed Escape) different from
|
|
5
|
+
* the first byte of a sequence the terminal is still sending. W-1 moved the timer,
|
|
6
|
+
* not the number: changing it changes how Escape feels, and that is a separate
|
|
7
|
+
* decision with its own evidence.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ESCAPE_HOLD_MS = 60;
|
|
10
|
+
/**
|
|
11
|
+
* One framed unit of input.
|
|
12
|
+
*
|
|
13
|
+
* The vocabulary is deliberately small — four kinds carry text, one carries the
|
|
14
|
+
* released Escape, one carries a device reply — because the Reader's job is to say
|
|
15
|
+
* *what arrived*, not what it means. `key` and `burst` reach the same routing today;
|
|
16
|
+
* they are distinguished because the Router (W-2) has to decide "one key" versus
|
|
17
|
+
* "several keys in one read" (the read-coalescing gaps G-1/G-2 live exactly there).
|
|
18
|
+
*/
|
|
19
|
+
export type InputFrame =
|
|
20
|
+
/** One complete terminal unit: a control byte, a full CSI sequence, or a lone ESC. */
|
|
21
|
+
{
|
|
22
|
+
kind: 'key';
|
|
23
|
+
sequence: string;
|
|
24
|
+
}
|
|
25
|
+
/** A read of ordinary typing. It may carry several keys — W-3 splits it. */
|
|
26
|
+
| {
|
|
27
|
+
kind: 'burst';
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
/** Text to be typed exactly as it is (a sequence tail, an Alt remainder, a release). */
|
|
31
|
+
| {
|
|
32
|
+
kind: 'literal';
|
|
33
|
+
text: string;
|
|
34
|
+
}
|
|
35
|
+
/** The payload between paste markers. */
|
|
36
|
+
| {
|
|
37
|
+
kind: 'paste';
|
|
38
|
+
text: string;
|
|
39
|
+
}
|
|
40
|
+
/** The held read turned out to be the Escape key itself. */
|
|
41
|
+
| {
|
|
42
|
+
kind: 'escape';
|
|
43
|
+
}
|
|
44
|
+
/** A device reply (cursor position) — input to the prober, never to an owner. */
|
|
45
|
+
| {
|
|
46
|
+
kind: 'reply';
|
|
47
|
+
sequence: string;
|
|
48
|
+
};
|
|
49
|
+
export interface InputReaderOptions {
|
|
50
|
+
/** A read that survived decoding and reply filtering. */
|
|
51
|
+
onRead: (text: string) => void;
|
|
52
|
+
/** A frame the Reader produced on its own: paste traffic, or a released hold. */
|
|
53
|
+
onFrame: (frame: InputFrame) => void;
|
|
54
|
+
/**
|
|
55
|
+
* How long a stalled paste may hold the reader before it is abandoned.
|
|
56
|
+
*
|
|
57
|
+
* A seam for tests only — the default is the release value. A test that waited the
|
|
58
|
+
* real two seconds kept a whole file's process alive alongside the others and was
|
|
59
|
+
* enough to unbalance a timer-sensitive paint test on a Windows runner.
|
|
60
|
+
*/
|
|
61
|
+
pasteMarkerHoldMs?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* How long a paste may sit with half a marker before the reader gives up on it.
|
|
65
|
+
*
|
|
66
|
+
* This is a watchdog, not a key hold: a paste is a stream, and its close marker may
|
|
67
|
+
* arrive in a second chunk after a slow terminal, a ConPTY or a loaded runner — on
|
|
68
|
+
* Windows CI the gap was over 200 ms, and expiring that early typed the marker's tail
|
|
69
|
+
* into the draft and left the session inside the paste. So the window is generous.
|
|
70
|
+
* When it does expire the reader does not just release the tail as payload, it
|
|
71
|
+
* **leaves the paste**: a paste that never closed must not keep the keyboard as its
|
|
72
|
+
* payload for the rest of the session.
|
|
73
|
+
*/
|
|
74
|
+
export declare const PASTE_MARKER_HOLD_MS = 2000;
|
|
75
|
+
export declare class InputReader {
|
|
76
|
+
private readonly options;
|
|
77
|
+
private readonly decoder;
|
|
78
|
+
private readonly guard;
|
|
79
|
+
/** A read that may still grow (escape assembly). */
|
|
80
|
+
private held;
|
|
81
|
+
private holdTimer;
|
|
82
|
+
/** Inside `ESC[200~ … ESC[201~`. */
|
|
83
|
+
private inPaste;
|
|
84
|
+
/**
|
|
85
|
+
* The tail of a read that ended *inside* a paste marker (`…ESC[20`), waiting for
|
|
86
|
+
* its other half.
|
|
87
|
+
*/
|
|
88
|
+
private pasteTail;
|
|
89
|
+
private pasteTailTimer;
|
|
90
|
+
constructor(options: InputReaderOptions);
|
|
91
|
+
/** One read of bytes: decode it, drop device replies, hand the rest on. */
|
|
92
|
+
push(chunk: Buffer): void;
|
|
93
|
+
/** The read with any held prefix in front of it. The hold window is over. */
|
|
94
|
+
assemble(text: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* The frames of one read, whichever kind it is.
|
|
97
|
+
*
|
|
98
|
+
* A read is what the terminal happened to send, not what the reader pressed: a
|
|
99
|
+
* fast typist, a slow link or ConPTY can put several keys in one read. This is the
|
|
100
|
+
* one place that decides where one unit ends and the next begins; everything above
|
|
101
|
+
* it (Router, owners) sees complete units only, and nothing above it re-parses the
|
|
102
|
+
* bytes (AD-20 §4.4, §6).
|
|
103
|
+
*/
|
|
104
|
+
read(text: string): InputFrame[];
|
|
105
|
+
/**
|
|
106
|
+
* Frame one paste read, holding back a marker it cut in half.
|
|
107
|
+
*
|
|
108
|
+
* A terminal writes bytes, not reads: `ESC[201~` can arrive as `…ESC[20` and `1~`.
|
|
109
|
+
* Framed naively, the first read swallows `ESC[20` as payload and the second one
|
|
110
|
+
* carries no marker at all — so `inPaste` never clears and **every later key is
|
|
111
|
+
* pasted text**: Esc stops closing a dialog, `/exit` stops exiting. Found by
|
|
112
|
+
* `scripts/tui-paste-probe.mjs` on CI, where the pty chunked the write exactly
|
|
113
|
+
* there. The fix is the same rule the escape ladder already uses — a read that
|
|
114
|
+
* could still grow is held, and released as payload if its other half never comes.
|
|
115
|
+
*/
|
|
116
|
+
private framePaste;
|
|
117
|
+
private armPasteTail;
|
|
118
|
+
private cancelPasteTail;
|
|
119
|
+
/**
|
|
120
|
+
* One non-paste read, split into complete units.
|
|
121
|
+
*
|
|
122
|
+
* The rules, and why each one exists:
|
|
123
|
+
*
|
|
124
|
+
* - **A complete CSI/SS3 sequence is one unit.** `ESC [ 5 ~ ESC [ 5 ~` is two page
|
|
125
|
+
* keys, not one unnameable read (G-1). A sequence the read cuts short stays one
|
|
126
|
+
* piece: `hold()` has already claimed it if it could still grow, and a trailing
|
|
127
|
+
* `ESC [` must reach the hold again rather than be split into `ESC` + `[`.
|
|
128
|
+
* - **An Alt chord is one unit.** `ESC` plus exactly one printable character is
|
|
129
|
+
* Alt+that key, so `ESC 1 x` is the chord Alt+1 *and then* `x` — never the text
|
|
130
|
+
* `1x` (G-3), and never a chord that swallows the character behind it.
|
|
131
|
+
* - **A control byte is one unit, CR/LF pair as one Enter.** `\x10\x10` is two
|
|
132
|
+
* presses of a rebound key (G-2/G-8), and Windows' CRLF is one key.
|
|
133
|
+
* - **A run of ordinary characters is one unit.** Splitting typing per character
|
|
134
|
+
* would make a pasted `yes` answer a confirm dialog character by character,
|
|
135
|
+
* which is the boundary W-0 pinned (`yes-typed-at-dialog`). Keymap lookup on a
|
|
136
|
+
* whole run is still exactly what a run means: text.
|
|
137
|
+
* - **A burst the paste heuristic claims stays whole.** A console without
|
|
138
|
+
* bracketed paste sends a paste as ordinary bytes with line breaks in them;
|
|
139
|
+
* that read has to reach the paste path intact (`looksLikePasteBurst`).
|
|
140
|
+
*/
|
|
141
|
+
units(text: string): InputFrame[];
|
|
142
|
+
/**
|
|
143
|
+
* Whether a read is paste traffic: inside a paste, or carrying a marker.
|
|
144
|
+
*
|
|
145
|
+
* Asked before anything else, exactly as the inline check it replaces was: while
|
|
146
|
+
* inside a paste, CR/LF are literal characters rather than submit.
|
|
147
|
+
*/
|
|
148
|
+
isPasteTraffic(combined: string): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* The frames of one paste read, in order: payload inside the markers as `paste`,
|
|
151
|
+
* everything outside them as `literal` (which is what the inline version typed
|
|
152
|
+
* directly, without a keymap lookup).
|
|
153
|
+
*
|
|
154
|
+
* Only complete markers are handled here: `framePaste` holds back a read that ends
|
|
155
|
+
* inside one, because a marker whose two halves land in two reads would otherwise
|
|
156
|
+
* leave the reader inside the paste for the rest of the session.
|
|
157
|
+
*/
|
|
158
|
+
frames(combined: string): InputFrame[];
|
|
159
|
+
/**
|
|
160
|
+
* Keep a read that could still grow into a sequence.
|
|
161
|
+
* @returns true when it was held (the caller stops there), false when the read is
|
|
162
|
+
* complete as it stands.
|
|
163
|
+
*/
|
|
164
|
+
hold(combined: string): boolean;
|
|
165
|
+
/**
|
|
166
|
+
* Whether one read is a paste from a console that cannot say so.
|
|
167
|
+
*
|
|
168
|
+
* A burst carrying more than one line break is not a human pressing Enter twice —
|
|
169
|
+
* they cannot press it twice inside one read. This is the fallback's one rule, and
|
|
170
|
+
* it lives here so every caller applies the same one.
|
|
171
|
+
*/
|
|
172
|
+
static looksLikePasteBurst(text: string): boolean;
|
|
173
|
+
/** Whether a complete read is a device reply rather than input. */
|
|
174
|
+
isDeviceReply(text: string): boolean;
|
|
175
|
+
/** Classify one complete read by shape. */
|
|
176
|
+
classify(text: string): InputFrame;
|
|
177
|
+
/**
|
|
178
|
+
* Hand over a held reply tail now.
|
|
179
|
+
*
|
|
180
|
+
* The guard releases on its own window; this is for a caller that already knows
|
|
181
|
+
* the tail is stale — `scripts/bench-b2.mjs` drives keystrokes faster than the
|
|
182
|
+
* hold, and a measurement that waited for the window would be measuring the wait.
|
|
183
|
+
*/
|
|
184
|
+
releaseReplyTail(): void;
|
|
185
|
+
/** Drop the holds and the reply filter (display detached, teardown). */
|
|
186
|
+
stop(): void;
|
|
187
|
+
private cancelHold;
|
|
188
|
+
/**
|
|
189
|
+
* What a held read turned out to be.
|
|
190
|
+
*
|
|
191
|
+
* Verbatim from the inline timer it replaces: a lone `ESC` is the Escape key, an
|
|
192
|
+
* `ESC O` without its final byte is Alt+O, an escape sequence that never completed
|
|
193
|
+
* is consumed silently (its `ESC` must not read as a cancel), and anything else is
|
|
194
|
+
* typed. The last branch is unreachable for the shapes `isEscapePrefix` accepts —
|
|
195
|
+
* it is kept because "unreachable today" is not a reason to change a release.
|
|
196
|
+
*/
|
|
197
|
+
private releaseHold;
|
|
198
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The InputRouter: one unit in, exactly one owner out.
|
|
3
|
+
*
|
|
4
|
+
* W-1 taught the TUI *what arrived* (`InputFrame`). This module answers the other
|
|
5
|
+
* question — *who takes it* — and it is the only place that may. The four owners
|
|
6
|
+
* (Screen, Surface, Composer, Workspace) each have an adapter in `tui.ts`; an
|
|
7
|
+
* adapter may read its own state, but it may not decide cross-owner ownership
|
|
8
|
+
* again, and it may not hand a unit on to the next owner when it does not use it
|
|
9
|
+
* (AD-20 §4.3, §4.5 F-1/F-6).
|
|
10
|
+
*
|
|
11
|
+
* The decision is a pure function of `(unit kind, unit, ownership snapshot)`: no
|
|
12
|
+
* app state is read or written, nothing is painted, no transcript row is pushed,
|
|
13
|
+
* and the snapshot is a plain object the caller keeps and overwrites, so a
|
|
14
|
+
* keystroke costs no allocation (W-2 is a hot-path migration).
|
|
15
|
+
*
|
|
16
|
+
* The rules are the ones `tui.ts` already applied inline, moved here verbatim —
|
|
17
|
+
* including one asymmetry that is **recorded debt, not a decision**:
|
|
18
|
+
*
|
|
19
|
+
* 1. `Ctrl+L` is handled by the Screen when a Screen is up and by the Composer
|
|
20
|
+
* otherwise; the repaint has two implementations and W-2 does not merge them
|
|
21
|
+
* (AD-20 §6: only a test that proves both owners repaint identically may).
|
|
22
|
+
* W-3b.2 owns that one.
|
|
23
|
+
*
|
|
24
|
+
* W-3b.1 closed the other asymmetry this module used to record:
|
|
25
|
+
* `SURFACE_COMPOSER_KEYS` sent `Home`/`End`/`Delete`/kitty-`Ctrl+Shift+C` to the
|
|
26
|
+
* composer while a Surface was up. The Surface owns them now, like every other unit,
|
|
27
|
+
* and its adapter decides what they mean — the field it borrowed, or nothing.
|
|
28
|
+
*
|
|
29
|
+
* @module dsh-ssh-tui/input-router
|
|
30
|
+
*/
|
|
31
|
+
/** Who takes a unit. Exactly one of these, per unit, decided here. */
|
|
32
|
+
export type Owner = 'screen' | 'surface' | 'composer' | 'workspace';
|
|
33
|
+
/**
|
|
34
|
+
* The unit classes the ladder dispatches.
|
|
35
|
+
*
|
|
36
|
+
* `text` and `control` are the composer's own shapes, `sequence` is a complete
|
|
37
|
+
* CSI/SS3 unit, `paste` is a payload with its markers already off, and `escape` is
|
|
38
|
+
* the released lone Escape. `burst` frames never reach this module as a whole: the
|
|
39
|
+
* ladder types them character by character, so every character is routed on its own
|
|
40
|
+
* (which is also what lets a Surface answer `y` and the composer take the `es`).
|
|
41
|
+
*/
|
|
42
|
+
export type UnitKind = 'text' | 'control' | 'sequence' | 'paste' | 'escape';
|
|
43
|
+
/** The read-only ownership state a decision may look at. */
|
|
44
|
+
export interface OwnershipSnapshot {
|
|
45
|
+
/** The Screen that is up, if any (its kind, for reports). */
|
|
46
|
+
screenKind: string | undefined;
|
|
47
|
+
/** The Surface (dialog) that is up, if any. */
|
|
48
|
+
surfaceKind: string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* …and whether that Surface owns a borrowed text field.
|
|
51
|
+
*
|
|
52
|
+
* Part of the contract's vocabulary, and no rule branches on it since W-3b.1:
|
|
53
|
+
* whether a Surface borrowed a field decides what its *adapter* does with an
|
|
54
|
+
* editing key, not who owns the key.
|
|
55
|
+
*/
|
|
56
|
+
surfaceHasField: boolean;
|
|
57
|
+
/** The composer's field is empty — the focus ring's precondition. */
|
|
58
|
+
composerEmpty: boolean;
|
|
59
|
+
/** The transcript has collapsible cards — the focus ring's other precondition. */
|
|
60
|
+
workspaceCards: boolean;
|
|
61
|
+
}
|
|
62
|
+
export declare class InputRouter {
|
|
63
|
+
/** The one cross-owner decision. One unit in, one owner out. */
|
|
64
|
+
route(kind: UnitKind, unit: string, view: OwnershipSnapshot): Owner;
|
|
65
|
+
/**
|
|
66
|
+
* With nothing layered over the workspace, the Composer owns text and its editing
|
|
67
|
+
* keys, and the Workspace owns the transcript: the mouse, the page keys, and the
|
|
68
|
+
* focus ring — which an arrow reaches only while the field is empty and there is a
|
|
69
|
+
* card to land on (otherwise the same arrow browses history, and history is the
|
|
70
|
+
* composer's).
|
|
71
|
+
*/
|
|
72
|
+
private composerOrWorkspace;
|
|
73
|
+
}
|
|
@@ -37,10 +37,24 @@ export declare function stripCursorReplies(text: string): {
|
|
|
37
37
|
* typed. A read that ends in the middle of a possible reply is held back (see
|
|
38
38
|
* `flush()`) so `ESC [ 1` + `7;1R` cannot become visible digits either.
|
|
39
39
|
*/
|
|
40
|
+
export interface TerminalInputFilterOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Whether a read is bracketed-paste traffic.
|
|
43
|
+
*
|
|
44
|
+
* The filter asks instead of sniffing the markers itself: paste framing has one
|
|
45
|
+
* owner (the Reader), and a marker literal here would be a second one. It matters
|
|
46
|
+
* because a paste's close marker ends in `ESC[20` — the shape of a half-arrived
|
|
47
|
+
* cursor reply — so holding that tail cuts the marker in half and the Reader stays
|
|
48
|
+
* inside the paste for the rest of the session.
|
|
49
|
+
*/
|
|
50
|
+
isPasteTraffic?: (text: string) => boolean;
|
|
51
|
+
}
|
|
40
52
|
export declare class TerminalInputFilter {
|
|
53
|
+
private readonly options;
|
|
41
54
|
private held;
|
|
42
55
|
/** The most recent cursor reply this filter swallowed, if any. */
|
|
43
56
|
lastReply: string | undefined;
|
|
57
|
+
constructor(options?: TerminalInputFilterOptions);
|
|
44
58
|
push(text: string): {
|
|
45
59
|
forward: string;
|
|
46
60
|
replies: number;
|
|
@@ -60,7 +74,7 @@ export declare class TerminalInputGuard {
|
|
|
60
74
|
private readonly holdMs;
|
|
61
75
|
private readonly filter;
|
|
62
76
|
private timer;
|
|
63
|
-
constructor(emit: (text: string) => void, holdMs?: number);
|
|
77
|
+
constructor(emit: (text: string) => void, holdMs?: number, options?: TerminalInputFilterOptions);
|
|
64
78
|
/** Feed one decoded read; anything that is not a reply reaches `emit`. */
|
|
65
79
|
push(text: string): void;
|
|
66
80
|
/** Hand a half-arrived sequence over (its window passed; it is typing). */
|
package/lib/types/tui.d.ts
CHANGED
|
@@ -229,7 +229,6 @@ export declare class SshTui {
|
|
|
229
229
|
private input;
|
|
230
230
|
private cursor;
|
|
231
231
|
private inputFolded;
|
|
232
|
-
private inPaste;
|
|
233
232
|
private history;
|
|
234
233
|
private historyIndex;
|
|
235
234
|
/** Live input parked while browsing history with ↑. Restored by ↓ past the newest item. */
|
|
@@ -375,7 +374,6 @@ export declare class SshTui {
|
|
|
375
374
|
private readonly onReattach;
|
|
376
375
|
private readonly onDetach;
|
|
377
376
|
private renderTimer;
|
|
378
|
-
private readonly decoder;
|
|
379
377
|
private readonly color;
|
|
380
378
|
private readonly maxToolOutputLines;
|
|
381
379
|
private readonly showReasoning;
|
|
@@ -650,8 +648,6 @@ export declare class SshTui {
|
|
|
650
648
|
* `turn/start` is the only thing that resets it.
|
|
651
649
|
*/
|
|
652
650
|
private reasoningExpandedChoice;
|
|
653
|
-
private escapeBuffer;
|
|
654
|
-
private escapeTimer;
|
|
655
651
|
/**
|
|
656
652
|
* How long an acknowledgement or a warning stays on the chrome.
|
|
657
653
|
*
|
|
@@ -666,11 +662,19 @@ export declare class SshTui {
|
|
|
666
662
|
private feedbackTtlMs;
|
|
667
663
|
private feedbackTimer;
|
|
668
664
|
/**
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
665
|
+
* The input Reader (AD-21): decoding, reply filtering, escape assembly, paste
|
|
666
|
+
* framing and unit classification live there; the frames come back through
|
|
667
|
+
* `handleInputText` / `consumeFrame`, which own the routing and the semantics.
|
|
672
668
|
*/
|
|
673
|
-
private readonly
|
|
669
|
+
private readonly reader;
|
|
670
|
+
/**
|
|
671
|
+
* The Router (AD-22, W-2): the one place a unit's owner is decided.
|
|
672
|
+
*
|
|
673
|
+
* `ownershipView` is the read-only snapshot it answers from — one object, rewritten
|
|
674
|
+
* per decision, so a keystroke allocates nothing (W-2 is a hot-path migration).
|
|
675
|
+
*/
|
|
676
|
+
private readonly router;
|
|
677
|
+
private readonly ownershipView;
|
|
674
678
|
private thinkingStartedAt;
|
|
675
679
|
private waitStartedAt;
|
|
676
680
|
/**
|
|
@@ -700,6 +704,15 @@ export declare class SshTui {
|
|
|
700
704
|
private cwdChipRow;
|
|
701
705
|
/** Set when a card expand/collapse moves chrome; next paint full-redraws. */
|
|
702
706
|
private forceFullPaint;
|
|
707
|
+
/**
|
|
708
|
+
* How many times the Screen has been dismissed.
|
|
709
|
+
*
|
|
710
|
+
* A read is not a key: its units are delivered one at a time, and a Screen owns the
|
|
711
|
+
* read it was sent — once it is gone the rest is dropped (0.8.3's rule, W-3a keeps
|
|
712
|
+
* it). Counting dismissals rather than comparing the Screen object keeps that check
|
|
713
|
+
* out of the owner-read ratchet, which exists to stop new *ownership* reads.
|
|
714
|
+
*/
|
|
715
|
+
private screenDismissals;
|
|
703
716
|
/** Highest row the last budgeted frame could not paint, resumed next tick. */
|
|
704
717
|
private paintResume;
|
|
705
718
|
/** How many times a display has come back after a drop in this Host's life. */
|
|
@@ -1765,6 +1778,16 @@ export declare class SshTui {
|
|
|
1765
1778
|
private setField;
|
|
1766
1779
|
/** Insert at the caret of whichever field owns the keyboard. */
|
|
1767
1780
|
private insertIntoField;
|
|
1781
|
+
/**
|
|
1782
|
+
* Move the caret of whichever field owns the keyboard to one end (Home/End).
|
|
1783
|
+
*
|
|
1784
|
+
* One implementation for the two owners that carry these keys (W-3b.1): the composer
|
|
1785
|
+
* edits the draft, a Surface with a borrowed field edits the answer, and a Surface
|
|
1786
|
+
* without one has nothing to move — `textIsEditable` is what says which of those the
|
|
1787
|
+
* keyboard is on. Ctrl+A/Ctrl+E reach the same two positions through `composerUnit`,
|
|
1788
|
+
* which asks the same question.
|
|
1789
|
+
*/
|
|
1790
|
+
private moveFieldCursor;
|
|
1768
1791
|
/** One card's key: a call's batch can repeat a question id across calls. */
|
|
1769
1792
|
private questionCardKey;
|
|
1770
1793
|
/**
|
|
@@ -2156,9 +2179,125 @@ export declare class SshTui {
|
|
|
2156
2179
|
private scrollScreenTo;
|
|
2157
2180
|
/** The copy key inside a Screen: what the Screen shows is what the reader means. */
|
|
2158
2181
|
private copyFromScreen;
|
|
2182
|
+
/**
|
|
2183
|
+
* One read, as the Reader framed it: paste traffic is dispatched per frame,
|
|
2184
|
+
* everything else is routed (W-2 lifts the routing itself into a Router).
|
|
2185
|
+
*/
|
|
2159
2186
|
private handleInputText;
|
|
2160
|
-
/**
|
|
2161
|
-
private
|
|
2187
|
+
/** One framed unit, at the owner that takes it. */
|
|
2188
|
+
private consumeFrame;
|
|
2189
|
+
/**
|
|
2190
|
+
* One read from the Reader, as the ladder has always taken it.
|
|
2191
|
+
*
|
|
2192
|
+
* Two kinds of table live here and they are not the same thing:
|
|
2193
|
+
*
|
|
2194
|
+
* - **binding** — which byte means which action (the keymap's rebindable actions,
|
|
2195
|
+
* the Alt chords, the SS3 tail). Those stay; AD-20 F-5 wants one table per key.
|
|
2196
|
+
* - **ownership** — who takes the unit. That question is the Router's now, and it
|
|
2197
|
+
* is asked exactly once per unit, before any binding table is consulted: a
|
|
2198
|
+
* Screen owns the read outright, so the tables below are for the unlayered case,
|
|
2199
|
+
* which is the order the ladder always had.
|
|
2200
|
+
*
|
|
2201
|
+
* The parsing branches that are neither (a read that could still grow — the Reader
|
|
2202
|
+
* holds it; an escape sequence nobody claimed — dropped) stay here as well.
|
|
2203
|
+
*/
|
|
2204
|
+
private route;
|
|
2205
|
+
/**
|
|
2206
|
+
* The Router's read-only view, rewritten per decision (no allocation).
|
|
2207
|
+
*
|
|
2208
|
+
* The card question is the only expensive one — `collapsibleRows()` walks the
|
|
2209
|
+
* visible transcript — so it is answered only for the arrow that asks it, and only
|
|
2210
|
+
* when nothing is layered over the workspace.
|
|
2211
|
+
*/
|
|
2212
|
+
private observe;
|
|
2213
|
+
/**
|
|
2214
|
+
* Whether a read is one of the sequence shapes the ladder claims as a whole.
|
|
2215
|
+
*
|
|
2216
|
+
* These are matched *before* the Reader's hold window — the order the ladder always
|
|
2217
|
+
* had (`ESC [ 5 ~` is a page key, not a prefix that might still grow).
|
|
2218
|
+
*/
|
|
2219
|
+
private isSequenceShape;
|
|
2220
|
+
/** The unit class of one read, by shape (the Reader's classification, allocation-free). */
|
|
2221
|
+
private readKind;
|
|
2222
|
+
/** The unit class of one character — what `handleChar` routes. */
|
|
2223
|
+
private charKind;
|
|
2224
|
+
/** One unit, one owner: the Router decides, the owner adapter runs it. */
|
|
2225
|
+
private dispatch;
|
|
2226
|
+
/** Run a unit at the owner the Router already named. */
|
|
2227
|
+
private dispatchWith;
|
|
2228
|
+
/**
|
|
2229
|
+
* A paste payload: the Router decides, the paste path executes.
|
|
2230
|
+
*
|
|
2231
|
+
* A paste is text, so the field that owns the keyboard takes it — and a Screen that
|
|
2232
|
+
* owns the interaction terminates it (its own field or nothing), never a field
|
|
2233
|
+
* behind it (AD-20 §4.3 priority 1). `handlePasteText` still re-asks that per owner;
|
|
2234
|
+
* that is the compat path W-3 folds into the Router (marked there).
|
|
2235
|
+
*/
|
|
2236
|
+
private dispatchPaste;
|
|
2237
|
+
/**
|
|
2238
|
+
* The Screen adapter: a Screen owns the keyboard outright (AD-20 §4.3 priority 3).
|
|
2239
|
+
*
|
|
2240
|
+
* One unit goes in — the Reader split the read, so a Screen sees the same keys the
|
|
2241
|
+
* composer would (W-3a) and `handleScreenInput` is where the paste-burst rule stays.
|
|
2242
|
+
* Its own `screen === undefined` guard stays too: a release raced by a teardown
|
|
2243
|
+
* reaches it, and refusing there is the adapter's own business rather than a second
|
|
2244
|
+
* ownership decision.
|
|
2245
|
+
*/
|
|
2246
|
+
private ownerScreen;
|
|
2247
|
+
/**
|
|
2248
|
+
* The Surface adapter: a dialog owns the keyboard while it is up.
|
|
2249
|
+
*
|
|
2250
|
+
* Its arrows move a question's cursor (or nothing — an arrow a Surface does not use
|
|
2251
|
+
* is not the transcript's), the page keys are not its business, and its mouse reports
|
|
2252
|
+
* take the same path they always did so the refusal happens where it always did, down
|
|
2253
|
+
* to the remembered press. Its own editing keys are `Home`/`End`/`Delete` (W-3b.1):
|
|
2254
|
+
* they act on the field the Surface borrowed — the one row the reader can see — and a
|
|
2255
|
+
* Surface that borrowed none has no field for them, so it consumes them. The kitty
|
|
2256
|
+
* copy key falls out with the other sequences below: a Surface has no copy contract,
|
|
2257
|
+
* and this round did not invent one. Everything else goes to the shared
|
|
2258
|
+
* composer/Surface body, whose `dialog` branches are the Surface's own behaviour.
|
|
2259
|
+
*/
|
|
2260
|
+
private ownerSurface;
|
|
2261
|
+
/**
|
|
2262
|
+
* The Composer adapter.
|
|
2263
|
+
*
|
|
2264
|
+
* Text — one character or many — is typed: the burst rule first, then one character
|
|
2265
|
+
* at a time through the Router, which is what lets ownership change inside a single
|
|
2266
|
+
* read (a Surface answering `y` with the composer taking the `es` behind it). The
|
|
2267
|
+
* sequence shapes the composer owns are the arrows (the suggestion list, else
|
|
2268
|
+
* history) and the caret keys — which it now only ever gets while it owns the
|
|
2269
|
+
* keyboard: with a Surface up, the Router hands those keys to the Surface (W-3b.1).
|
|
2270
|
+
*/
|
|
2271
|
+
private ownerComposer;
|
|
2272
|
+
/**
|
|
2273
|
+
* The Workspace adapter: the transcript owns the mouse, the page keys and the focus
|
|
2274
|
+
* ring — and the ring only when the Router said so (empty field, a card to land on).
|
|
2275
|
+
*/
|
|
2276
|
+
private ownerWorkspace;
|
|
2277
|
+
/** An arrow the composer keeps: the suggestion list while it is up, else history. */
|
|
2278
|
+
private arrowHistory;
|
|
2279
|
+
/** Press, motion and release: selection, links and card toggles — the transcript's. */
|
|
2280
|
+
private mouseGesture;
|
|
2281
|
+
/**
|
|
2282
|
+
* The composer/Surface body — the switch and the tail the ladder always had.
|
|
2283
|
+
*
|
|
2284
|
+
* It is shared by two adapters on purpose: its `dialog` guards *are* the Surface's
|
|
2285
|
+
* behaviour for those keys (a borrowed field takes them, a field-less dialog refuses
|
|
2286
|
+
* them), and splitting them is W-3's job. What the Router guarantees is that a Screen
|
|
2287
|
+
* never reaches it, so every key below can only spend itself on a field the reader
|
|
2288
|
+
* has in front of them — the composer's or a borrowed one. The unreachable second
|
|
2289
|
+
* Screen gate the old body carried is gone with it (G-6).
|
|
2290
|
+
*/
|
|
2291
|
+
private composerUnit;
|
|
2292
|
+
/**
|
|
2293
|
+
* Ctrl+L's repaint, in one place for the owners that share it (G-6).
|
|
2294
|
+
*
|
|
2295
|
+
* The Screen has its own implementation (`screenNeedsFullPaint`, because it repaints
|
|
2296
|
+
* a Screen's frame); this one drops the cached rows so the next full frame is rebuilt
|
|
2297
|
+
* from scratch. W-2 does not merge them — only a test that proves both owners repaint
|
|
2298
|
+
* identically may (AD-20 §6) — but it does give the composer's copy a name.
|
|
2299
|
+
*/
|
|
2300
|
+
private repaintNow;
|
|
2162
2301
|
/**
|
|
2163
2302
|
* Insert pasted text into the field that owns the keyboard; CR/LF are literal
|
|
2164
2303
|
* newlines.
|
|
@@ -2199,6 +2338,14 @@ export declare class SshTui {
|
|
|
2199
2338
|
*/
|
|
2200
2339
|
private textIsEditable;
|
|
2201
2340
|
private handlePlainText;
|
|
2341
|
+
/**
|
|
2342
|
+
* One character, from a read or from the typing loop: the Router decides, again.
|
|
2343
|
+
*
|
|
2344
|
+
* It is asked per character rather than per read because ownership can change inside
|
|
2345
|
+
* one read — a Surface that answers the first character of `yes` is gone for the rest
|
|
2346
|
+
* (pinned by the `yes-typed-at-dialog` trace). A Screen never reaches the body below:
|
|
2347
|
+
* the Router answered `screen` and the Screen adapter took the character.
|
|
2348
|
+
*/
|
|
2202
2349
|
private handleChar;
|
|
2203
2350
|
/**
|
|
2204
2351
|
* Run one rebindable action.
|
|
@@ -2382,13 +2529,21 @@ export declare class SshTui {
|
|
|
2382
2529
|
* inspect overlay owns the keyboard and takes no scroll at all: the transcript
|
|
2383
2530
|
* behind a dialog must not move.
|
|
2384
2531
|
*/
|
|
2385
|
-
private
|
|
2532
|
+
private scrollWorkspace;
|
|
2386
2533
|
private handleCtrlC;
|
|
2387
2534
|
private submit;
|
|
2388
2535
|
private runCommand;
|
|
2389
|
-
/**
|
|
2536
|
+
/**
|
|
2537
|
+
* Grapheme cluster immediately before `cursor`; cursor-internal positions delete it whole.
|
|
2538
|
+
*
|
|
2539
|
+
* The cluster is measured in the field that owns the keyboard, like `backspace` and
|
|
2540
|
+
* `deleteAtCursor` above, which is what makes their ranges mean the same thing as the
|
|
2541
|
+
* cursor they were handed (W-3b.1b): segmenting the *draft* while editing a borrowed
|
|
2542
|
+
* answer gave offsets from another string, so a borrowed `a🚀` lost one surrogate and
|
|
2543
|
+
* kept the caret inside the pair.
|
|
2544
|
+
*/
|
|
2390
2545
|
private graphemeBefore;
|
|
2391
|
-
/** Grapheme cluster containing or following `cursor
|
|
2546
|
+
/** Grapheme cluster containing or following `cursor`, in the field that owns the keyboard. */
|
|
2392
2547
|
private graphemeAfter;
|
|
2393
2548
|
/** Delete one grapheme back, in whichever field owns the keyboard. */
|
|
2394
2549
|
private backspace;
|
|
@@ -2398,7 +2553,16 @@ export declare class SshTui {
|
|
|
2398
2553
|
private moveCursor;
|
|
2399
2554
|
private historyBack;
|
|
2400
2555
|
private historyForward;
|
|
2401
|
-
/**
|
|
2556
|
+
/**
|
|
2557
|
+
* Typing while browsing history detaches from the saved item.
|
|
2558
|
+
*
|
|
2559
|
+
* The browse is the **composer's**, so only the composer's own field may end it
|
|
2560
|
+
* (W-3b.3a). Every editing key a Surface borrowed the field for arrives here —
|
|
2561
|
+
* `backspace`, `deleteAtCursor`, `handlePasteText`, and `composerUnit`'s Ctrl+U /
|
|
2562
|
+
* Ctrl+K — and detaching on that path wrote the hidden composer's state from the
|
|
2563
|
+
* visible answer: the recalled entry replaced `historyDraft`, so the unsent draft
|
|
2564
|
+
* the browse was holding could never be reached again with `↓`.
|
|
2565
|
+
*/
|
|
2402
2566
|
private leaveHistoryBrowse;
|
|
2403
2567
|
}
|
|
2404
2568
|
/**
|