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.
@@ -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). */
@@ -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
- * Cursor-position replies removed from the relay's stdin stream. A launcher
670
- * from an older release (or a reply that raced its own probe) would otherwise
671
- * type `[17;1R` into the prompt or cancel a dialog with a bare ESC.
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 inputGuard;
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
- /** Handle one data chunk that may contain bracketed-paste markers. */
2161
- private processPasteChunk;
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 scrollActiveSurface;
2532
+ private scrollWorkspace;
2386
2533
  private handleCtrlC;
2387
2534
  private submit;
2388
2535
  private runCommand;
2389
- /** Grapheme cluster immediately before `cursor`; cursor-internal positions delete it whole. */
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
- /** Typing while browsing history detaches from the saved item. */
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
  /**