dsh-ssh-tui 0.8.3 → 0.8.4-rc.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 +132 -12
- package/README.en.md +20 -17
- package/README.md +11 -8
- package/docs/release-notes-0.8.4-rc.1.md +271 -0
- package/docs/release-notes-0.8.4-rc.2.md +72 -0
- package/docs/release-notes-0.8.4.md +145 -0
- package/lib/doctor.js +41 -4
- package/lib/doctor.js.map +1 -1
- 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/i18n/en.js +6 -1
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/zh.js +6 -1
- package/lib/i18n/zh.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/preset-rows.js +70 -19
- package/lib/preset-rows.js.map +1 -1
- package/lib/terminal-input.js +27 -13
- package/lib/terminal-input.js.map +1 -1
- package/lib/tui.js +812 -482
- package/lib/tui.js.map +1 -1
- package/lib/types/doctor.d.ts +13 -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/preset-rows.d.ts +22 -7
- package/lib/types/terminal-input.d.ts +15 -1
- package/lib/types/tui.d.ts +210 -18
- 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
package/lib/types/doctor.d.ts
CHANGED
|
@@ -30,7 +30,11 @@ export interface DoctorCheck {
|
|
|
30
30
|
export interface DoctorRouting {
|
|
31
31
|
provider?: string;
|
|
32
32
|
model?: string;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Every route exposed by the live adapter directory or configured under
|
|
35
|
+
* `llm-pi-ai.providers`. Presence is registration/configuration evidence,
|
|
36
|
+
* not a network probe.
|
|
37
|
+
*/
|
|
34
38
|
routes: readonly string[];
|
|
35
39
|
/** Model ids the selected provider's route lists. */
|
|
36
40
|
routeModels: readonly string[];
|
|
@@ -50,6 +54,14 @@ export interface DoctorFacts {
|
|
|
50
54
|
roster: boolean;
|
|
51
55
|
/** `codeRuntime` — what the PTC preset mounts. */
|
|
52
56
|
codeRuntime: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* `tools.get('ask_user_question')` — the actual registration, not the row
|
|
59
|
+
* text. Absent on snapshots taken before this field existed, where the
|
|
60
|
+
* textual check remains the fallback.
|
|
61
|
+
*/
|
|
62
|
+
askUserTool?: boolean;
|
|
63
|
+
/** `tools.get('present')` — the other half of the 0.1.7 agent plane. */
|
|
64
|
+
presentTool?: boolean;
|
|
53
65
|
};
|
|
54
66
|
/** The profile patch. `readable` is false when it does not exist yet. */
|
|
55
67
|
patch: {
|
|
@@ -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
|
+
}
|
|
@@ -133,7 +133,14 @@ export declare function analyzePatch(text: string): PatchAnalysis;
|
|
|
133
133
|
* @returns the later occurrences, in file order.
|
|
134
134
|
*/
|
|
135
135
|
export declare function duplicatePatchRows(rows: readonly PatchRowRef[]): PatchRowRef[];
|
|
136
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Whether a patch already addresses one roster row, by id or by module name.
|
|
138
|
+
*
|
|
139
|
+
* Every kind counts: an override or a disable targets the row just as an insert
|
|
140
|
+
* does. A repair must leave an intentional `- id: present` / `disabled: true`
|
|
141
|
+
* alone instead of mounting the same id a second time; the later entry would
|
|
142
|
+
* either collide with it or silently undo the user's choice.
|
|
143
|
+
*/
|
|
137
144
|
export declare function patchNamesRow(rows: readonly PatchRowRef[], row: RosterRow): boolean;
|
|
138
145
|
/** The roster rows a patch does not declare yet. */
|
|
139
146
|
export declare function missingRosterRows(rows: readonly PatchRowRef[], candidates?: readonly RosterRow[]): RosterRow[];
|
|
@@ -159,6 +166,8 @@ export interface PatchRepair {
|
|
|
159
166
|
* @param missing - the roster rows to mount; defaults to the 0.1.5 set.
|
|
160
167
|
* @param generation - which host line the profile boots on.
|
|
161
168
|
* @returns the new text, or `undefined` when nothing has to change.
|
|
169
|
+
* @throws when `existing` is not valid YAML; appending to a broken patch would
|
|
170
|
+
* make it longer without making it bootable.
|
|
162
171
|
*/
|
|
163
172
|
export declare function planRosterRepair(existing: string, missing?: readonly RosterRow[], generation?: HostGeneration): PatchRepair | undefined;
|
|
164
173
|
/**
|
|
@@ -175,26 +184,32 @@ export declare function planDuplicateRepair(existing: string): PatchRepair | und
|
|
|
175
184
|
/**
|
|
176
185
|
* Mount the roster in one profile, unless it is already composed.
|
|
177
186
|
*
|
|
178
|
-
* Idempotent: a profile whose patch already
|
|
179
|
-
* `@deepseek-ai/dsh-web-app`, for example) is left untouched.
|
|
180
|
-
*
|
|
187
|
+
* Idempotent: a profile whose patch already addresses the roster row (one
|
|
188
|
+
* bundling `@deepseek-ai/dsh-web-app`, for example) is left untouched. An
|
|
189
|
+
* existing file is backed up before the small whole-file replace; a
|
|
181
190
|
* half-written patch would only be seen by the next launch.
|
|
182
191
|
* @param home - the harness home carrying `profiles/`.
|
|
183
192
|
* @param profile - the profile to patch.
|
|
184
193
|
* @param missing - the rows to mount; defaults to the 0.1.5 set.
|
|
185
194
|
* @param generation - which host line the profile boots on.
|
|
195
|
+
* @param stamp - filename-safe backups stamp, injectable for tests.
|
|
186
196
|
* @returns `present` when the row already exists, else `written`.
|
|
197
|
+
* @throws when the existing patch cannot be read, is not valid YAML, or cannot
|
|
198
|
+
* be backed up; a file that cannot be preserved is not overwritten.
|
|
187
199
|
*/
|
|
188
|
-
export declare function ensureRosterRows(home: string, profile: string, missing?: readonly RosterRow[], generation?: HostGeneration): Promise<'present' | 'written'>;
|
|
200
|
+
export declare function ensureRosterRows(home: string, profile: string, missing?: readonly RosterRow[], generation?: HostGeneration, stamp?: string): Promise<'present' | 'written'>;
|
|
189
201
|
/**
|
|
190
202
|
* Write a repaired patch next to a copy of the previous content.
|
|
191
203
|
*
|
|
192
|
-
* `/doctor`
|
|
193
|
-
* before anything is replaced. A
|
|
204
|
+
* `/doctor` and the automatic repair change a file the user owns, so the old
|
|
205
|
+
* text is kept beside it before anything is replaced. A source that vanished
|
|
206
|
+
* between the read and the copy needs no backup; any other backup failure
|
|
207
|
+
* aborts before the write, leaving the original untouched.
|
|
194
208
|
* @param path - the patch file to replace.
|
|
195
209
|
* @param text - the new content.
|
|
196
210
|
* @param stamp - filename-safe timestamp for the backup, injectable for tests.
|
|
197
211
|
* @returns the backup path, or `undefined` when there was nothing to back up.
|
|
212
|
+
* @throws when the backup cannot be written; the destination is not touched.
|
|
198
213
|
*/
|
|
199
214
|
export declare function writePatchWithBackup(path: string, text: string, stamp?: string): Promise<string | undefined>;
|
|
200
215
|
/** The patch text the roster block would produce on its own, for callers that report it. */
|
|
@@ -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). */
|