@aragon-agent/cli 0.6.27 → 0.6.29
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 +164 -0
- package/README.md +2 -2
- package/dist/agent/reducer.js +47 -2
- package/dist/input/control-sequences.d.ts +71 -0
- package/dist/input/control-sequences.js +222 -0
- package/dist/input/limits.d.ts +20 -0
- package/dist/input/limits.js +20 -0
- package/dist/input/paste-parse.d.ts +21 -9
- package/dist/input/paste-parse.js +23 -11
- package/dist/input/stdin-filter.js +123 -9
- package/dist/tools/terminal-output.d.ts +8 -0
- package/dist/tools/terminal-output.js +10 -7
- package/dist/ui/App.js +34 -6
- package/dist/ui/AutocompletePopup.js +1 -1
- package/dist/ui/Header.js +7 -2
- package/dist/ui/Markdown.js +30 -3
- package/dist/ui/PromptInput.js +41 -3
- package/dist/ui/Transcript.js +7 -0
- package/dist/ui/display-tab.d.ts +51 -0
- package/dist/ui/display-tab.js +82 -0
- package/dist/ui/editor-navigation.d.ts +11 -1
- package/dist/ui/editor-navigation.js +14 -3
- package/dist/ui/entries/QueuedEntry.js +4 -1
- package/dist/ui/entries/UserEntry.js +8 -1
- package/dist/ui/layout/Gutter.js +1 -1
- package/dist/ui/layout/OverlayFrame.d.ts +15 -0
- package/dist/ui/layout/OverlayFrame.js +18 -0
- package/dist/ui/layout/ScrollIndicator.js +10 -3
- package/dist/ui/layout/budget.d.ts +15 -0
- package/dist/ui/layout/budget.js +15 -0
- package/dist/ui/layout/header-hints.d.ts +9 -0
- package/dist/ui/layout/header-hints.js +14 -1
- package/dist/ui/layout/status-layout.js +63 -2
- package/dist/ui/layout/virtual-window.js +44 -11
- package/dist/ui/markdown-blocks.d.ts +26 -5
- package/dist/ui/markdown-blocks.js +110 -33
- package/dist/ui/paste-frames.d.ts +5 -1
- package/dist/ui/paste-frames.js +7 -2
- package/dist/ui/transcript-text.js +11 -2
- package/dist/ui/use-wheel-routing.d.ts +10 -7
- package/dist/ui/use-wheel-routing.js +11 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -77,6 +77,170 @@ were written.
|
|
|
77
77
|
|
|
78
78
|
### Fixed
|
|
79
79
|
|
|
80
|
+
- **A long answer lost two characters at every hard wrap point - silently, in
|
|
81
|
+
exactly the content a user copies out.** The transcript's content column had no
|
|
82
|
+
width of its own, so Yoga laid each line out at the width it computed from the
|
|
83
|
+
parent while `virtual-window` estimated heights against `cols - GUTTER_WIDTH`:
|
|
84
|
+
two authorities for one number, and the clip kept the difference. A 240-digit
|
|
85
|
+
run in a 100-column transcript rendered as 97 + 97 + 42 = 236 digits, dropping
|
|
86
|
+
the characters at column 97 and 194 - URLs, git hashes, base64, paths and code
|
|
87
|
+
lines broke where nothing looked broken, because a paragraph with spaces re-wraps
|
|
88
|
+
at word boundaries and hides it. The column now takes `flexBasis={0}
|
|
89
|
+
flexGrow={1}` and keeps `flexShrink={0}` (invariant I-2: without it
|
|
90
|
+
`measureElement` can squeeze the column to the viewport and scrolling dies with
|
|
91
|
+
no error), so the available width is computed once, by yoga, from the parent.
|
|
92
|
+
Measured on the shipped component: 97 + 97 + 46 = 240/240, first divergence at
|
|
93
|
+
index -1.
|
|
94
|
+
- **A bracketed paste could type its own markers into the draft as `[200~hello
|
|
95
|
+
world`.** The startup `ESC[?2004h` is correct, but when a read boundary landed
|
|
96
|
+
inside the six-byte marker - ordinary on a Windows console, over SSH, or in tmux
|
|
97
|
+
under load - the 12 ms pending flush released the lone `ESC` as the Esc key (it
|
|
98
|
+
must never be held hostage), and the `[200~` that followed had nothing left to
|
|
99
|
+
recognise: `indexOf` needs the ESC. The filter now remembers a marker head it has
|
|
100
|
+
just released and, on the NEXT chunk only, splices it back in front for one
|
|
101
|
+
re-scan inside the same `feed()` - so the marker is consumed before its bytes
|
|
102
|
+
become text, `PENDING_FLUSH_MS` stays at 12 ms, and a new
|
|
103
|
+
`PASTE_MARKER_RESCAN_TTL_MS` (250 ms, structural, in `input/limits.ts`) bounds
|
|
104
|
+
when that is still believable. The visible garbage is gone in all three draft
|
|
105
|
+
measurements (`> hello world_`); the one residual is stated rather than hidden -
|
|
106
|
+
a write cannot be retracted, so the Esc keypress the release already produced
|
|
107
|
+
still happens, and a marker torn into three parts still leaves its middle
|
|
108
|
+
fragment behind (both recorded in `docs/diagnoses/.../analysis.md` §D-2).
|
|
109
|
+
- **A turn that only called a tool left a bare `●` bubble behind - on screen, in
|
|
110
|
+
the height table and in the saved session.** `turnStart` always appends an
|
|
111
|
+
assistant entry, so a tool-only turn, or one the user interrupted mid-stream,
|
|
112
|
+
left a bubble that `AssistantEntry` then refused to draw: the row was estimated
|
|
113
|
+
anyway (two for an aborted one, of which nothing drew the second), the scroll
|
|
114
|
+
position counted it, and `/save` exported it, so `/resume` brought it back. The
|
|
115
|
+
reducer now prunes a settled assistant entry with no text and no thinking, at
|
|
116
|
+
the three settle points only (`turnEnd`, `runEnd`, `abortMark`) - pruning earlier
|
|
117
|
+
would delete the live target of every `textDelta`, and the entry a `streamRestart`
|
|
118
|
+
is still filling in must survive. `abortMark` is the one settle path that does not
|
|
119
|
+
clear `streamingId` itself, so the helper returns the id and the caller writes it
|
|
120
|
+
back (I-L1-1); `runEnd` and `turnEnd` clear it anyway. A render-layer guard stays
|
|
121
|
+
as the second line of defence for a session file that already contains one.
|
|
122
|
+
- **An empty transcript still drew a full-height scrollbar track.** One rail
|
|
123
|
+
column of track glyphs from the second row down told the user there was more
|
|
124
|
+
below when there was nothing, and cost a column of width. `thumb === null` now
|
|
125
|
+
means no cells at all - the one-column BOX is still rendered, because the
|
|
126
|
+
reserved column (not the glyphs) is what keeps the content width constant; hiding
|
|
127
|
+
the column instead would re-wrap every line, change the measured height and feed
|
|
128
|
+
that back into the decision to show it.
|
|
129
|
+
- **Under `--no-color` the command palette's selected row was invisible, and a
|
|
130
|
+
bare `@` never opened file completion.** The selection was expressed only as
|
|
131
|
+
`inverse`, which is the identity at colour level 0 - and every `theme.*` colour is
|
|
132
|
+
`undefined` there too - so six commands looked identical and Enter was a guess
|
|
133
|
+
(`/clear` sits next to `/cwd`). The selected row now carries the same
|
|
134
|
+
`glyphs.caret` prefix the `/model` picker uses (`>` in the ASCII tier), with two
|
|
135
|
+
cells of padding on the unselected rows so no row changes width. Separately,
|
|
136
|
+
`README` has always promised that `@` opens a file-path completion popup while
|
|
137
|
+
the code required one more character: the `>= 1` gate and the `< 1` early return
|
|
138
|
+
are gone, and an empty `@` query lists the shallowest paths first (depth, then
|
|
139
|
+
length, then alphabetical) instead of the first twenty the filesystem happened to
|
|
140
|
+
hand back.
|
|
141
|
+
- **The completion menu was pinned at six rows on every terminal.** `App` handed
|
|
142
|
+
the popup a literal `6` while the frame budget had already computed the slot
|
|
143
|
+
capacity: a 30-row terminal showed 6 of 57 commands and 14 more hidden, so
|
|
144
|
+
browsing meant holding an arrow key. The cap is now derived from
|
|
145
|
+
`frameBudget.popupMaxHeight` through `POPUP_CAPACITY_SHARE` (2) in
|
|
146
|
+
`layout/budget.ts` - 6 rows at 20 lines, 11 at 30, 16 at 40. Handing over the
|
|
147
|
+
whole capacity was measured and rejected: `popupCapacity` reserves exactly one
|
|
148
|
+
message row, so at 20 rows it grants 13, the transcript collapses to a single row
|
|
149
|
+
and the TODO rail disappears. A divisor keeps the menu derived from the frame
|
|
150
|
+
budget instead of becoming a second authority beside it, and since it does not
|
|
151
|
+
depend on `popupRows` it cannot oscillate with it.
|
|
152
|
+
- **A fenced code block's language tag rendered as a body row, indented one cell
|
|
153
|
+
past the code.** The chip background and its padding spaces drew ` | ts` above
|
|
154
|
+
code rows starting `| export ...`, so the fence header read as an extra line of
|
|
155
|
+
source. It is now a muted bold TITLE on the same rail, aligned with the code, and
|
|
156
|
+
it deliberately keeps its own row: the height estimator counts raw markdown
|
|
157
|
+
lines and has no code-block branch, so the fence line must still occupy exactly
|
|
158
|
+
one. The fence grammar was too narrow as well - `(\w*)` rejected `c++`,
|
|
159
|
+
`objective-c` and four-backtick fences, degrading the whole block to prose and
|
|
160
|
+
swallowing the closing fence, and `split('\n')` left a `\r` on every CRLF code
|
|
161
|
+
line; a block now opens for 3+ backticks with any non-backtick info string, only
|
|
162
|
+
a run at least as long as its opener closes it, and the document is split
|
|
163
|
+
CRLF-safe.
|
|
164
|
+
- **A Markdown table rendered as space-separated plain text.** The `|---|` divider
|
|
165
|
+
was consumed by the parser to read alignment and never drawn, so a table had no
|
|
166
|
+
header marker, no column boundary and no rule - and under `--no-color`, where
|
|
167
|
+
bold is dropped too, nothing at all distinguished the header. `formatTable` now
|
|
168
|
+
returns `[header, divider, ...body]`, draws the rule from the caller's glyph and
|
|
169
|
+
separates columns with a visible `|` (both injected by `Markdown.tsx` from
|
|
170
|
+
`pickGlyphs`, so the parser stays ASCII-only and the pure default is `|`/`-`),
|
|
171
|
+
and pads by DISPLAY columns rather than `String.length`, which misaligned every
|
|
172
|
+
CJK cell. The separator costs one column per boundary; a table wide enough to
|
|
173
|
+
exceed the transcript is truncated rather than wrapped, which is the deliberate
|
|
174
|
+
trade recorded in §D-6.
|
|
175
|
+
- **A command that succeeded said nothing, and its notice then displaced
|
|
176
|
+
`Idle` / `Running` until the next one.** `/theme light` changed the SGR, `/todo
|
|
177
|
+
panel on` changed the layout, `/expand 1` with nothing to expand did nothing -
|
|
178
|
+
and the only feedback was the echoed command, so "it worked", "the argument was
|
|
179
|
+
wrong" and "the key was never received" looked identical. Worse, the body had
|
|
180
|
+
been reaching `status-layout` all along but only rendered on the second status
|
|
181
|
+
row, behind Ctrl+G and off by default, while the phase word was REPLACED by the
|
|
182
|
+
feedback label: after `/copy` the row said `Copied` and never went back to
|
|
183
|
+
`Idle`, which is the one thing that row exists to answer. Notification kinds
|
|
184
|
+
(`notice`, `copied`, `copy-sent`, `copying`) now keep the phase word and add the
|
|
185
|
+
body as their own field, clipped to 32 display cells; action kinds (`confirm`,
|
|
186
|
+
`stopping`, `force-stop`, `exit`, `clear-input`, `copy-error`, `copy-cleanup`)
|
|
187
|
+
still take the phase slot, because "what the next key does" IS the phase. The
|
|
188
|
+
body is dropped before the phase word would be, and a copy error no longer paints
|
|
189
|
+
`Idle` red.
|
|
190
|
+
- **`/help` turned 8 rows per PgDn on a 19-row window - 42% of a page.** Reading
|
|
191
|
+
the 93-row shortcut list took twelve presses and overlapped eleven rows every
|
|
192
|
+
time, while PgUp/PgDn in the transcript moved a whole page. The step is now
|
|
193
|
+
`overlayPageSize(overlayBodyRows(...))` - one window minus the two context rows
|
|
194
|
+
`scroll.pageSize` already keeps - and it reaches BOTH consumers, the keyboard
|
|
195
|
+
branch and the Shift+wheel notch (`getOverlayPage`), from the one function
|
|
196
|
+
`OverlayFrame` renders its window with, so the two cannot disagree.
|
|
197
|
+
- **On a narrow terminal the header's right-hand hints collided with the
|
|
198
|
+
truncated caption.** The left half is `wrap="truncate"` and draws its ellipsis in
|
|
199
|
+
the last cell it owns, and the hint column started in the very next cell, so 52
|
|
200
|
+
columns read `aragon...Details off` as one string and 60 columns read
|
|
201
|
+
`glm-5.^L branch`. The row now spends a new `HEADER_HINT_LIMITS.separatorCells`
|
|
202
|
+
(2) between the halves, taken out of both the left half's width and the planner's
|
|
203
|
+
budget - a separate constant from `gapCells`, which spaces two hint clauses
|
|
204
|
+
inside the right column, because one constant carrying two meanings is how a
|
|
205
|
+
later spacing change silently re-lays-out the whole row. The `^I Index`
|
|
206
|
+
thresholds move with it: 54 columns collapsed and 64 expanded, two later than
|
|
207
|
+
before.
|
|
208
|
+
- **A large paste arrived as runs of digits the moment it was sent - on screen,
|
|
209
|
+
in the model's input, in the prompt history and in the session file.**
|
|
210
|
+
`sanitisePaste` deleted the C0/C1 *introducer byte* and kept the parameter
|
|
211
|
+
bytes, so `ESC[38;2;18;52;86m` reached the transcript as the literal text
|
|
212
|
+
`[38;2;18;52;86m`. The composer collapses a large paste into
|
|
213
|
+
`[Pasted text #N +20 lines]`, so nothing was visible until submit expanded
|
|
214
|
+
it: a measured 20-line coloured log produced 80 runs of digits, and the same
|
|
215
|
+
string was what `/context`, the ↑ history and the session checkpoint stored.
|
|
216
|
+
Cleaning is now SEQUENCE-based and has one owner (new
|
|
217
|
+
`input/control-sequences.ts`, ASCII-only, no new dependency): a complete
|
|
218
|
+
escape goes as a unit, a truncated one loses its introducer and parameters
|
|
219
|
+
but never the character after them, an unterminated OSC/DCS/APC/PM/SOS goes
|
|
220
|
+
to the end of its line and never across it, and bare text that merely looks
|
|
221
|
+
like a sequence body (`[38;2m` with no ESC) is left exactly as it is.
|
|
222
|
+
`tools/terminal-output.ts`'s `stripAnsi` becomes a thin wrapper over the same
|
|
223
|
+
syntax, so a tool row and a paste cannot drift apart (both directions are
|
|
224
|
+
pinned by a cross-check test, and `terminal-output.test.ts` is unchanged).
|
|
225
|
+
A **single-line** paste carrying ESC - which `classifyChunk` sent down the
|
|
226
|
+
keystroke path with no cleaning at all - is now framed and cleaned like any
|
|
227
|
+
other paste; measured on the real component that path either put a live ESC
|
|
228
|
+
plus `[38;2;18;52;86m` into the draft or threw an uncaught `TypeError` inside
|
|
229
|
+
Ink's key parser. win32-input-mode records inside a frame are translated
|
|
230
|
+
rather than deleted, so a `?9001h` paste yields its characters instead of 15
|
|
231
|
+
visible digits per key (or, with cleaning but no translation, nothing at all).
|
|
232
|
+
- **A pasted TAB-indented block or TSV table lined up differently before and
|
|
233
|
+
after sending.** One byte had three readings: the composer painted
|
|
234
|
+
four-column tab stops, the transcript handed the raw `\t` to the terminal
|
|
235
|
+
(eight), and the exit replay counted code units. New `ui/display-tab.ts`
|
|
236
|
+
expands a TAB by DISPLAY column (`string-width`, so a `CJK<TAB>X` line lands
|
|
237
|
+
where the terminal paints it); the composer's stop is the terminal's eight, and
|
|
238
|
+
`UserEntry`, `QueuedEntry` and the exit replay expand at paint time only -
|
|
239
|
+
the stored payload keeps its `\t`, so the model still receives the original
|
|
240
|
+
indentation. `wrappedRows` counts display columns too (with an ASCII fast
|
|
241
|
+
path, without which a 10 000-entry transcript timed out), because an estimate
|
|
242
|
+
that reads a TAB as one column keeps the row short while the painted row
|
|
243
|
+
grows - the under-estimate that file names as the unsafe direction.
|
|
80
244
|
- **`Ctrl+L` opened a window that failed with a PowerShell `ParserError` on
|
|
81
245
|
Windows.** The script handed to `-EncodedCommand` was the argv joined with
|
|
82
246
|
quotes but with no call operator, and PowerShell reads a quoted string in command
|
package/README.md
CHANGED
|
@@ -539,7 +539,7 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
|
|
|
539
539
|
| `Ctrl+W`, `Alt+Backspace` | Delete the previous word |
|
|
540
540
|
| `Ctrl+U` / `Ctrl+K` | Kill to line start / end |
|
|
541
541
|
| `Up` / `Down` | 按视觉行移动;空草稿或正在浏览历史时,才在边界召回历史,避免覆盖新草稿 |
|
|
542
|
-
| Paste | Inserted
|
|
542
|
+
| Paste | Inserted up to 6 lines / 400 chars; larger collapses to `[Pasted text #1 +15 lines]`, which deletes as one unit and expands in full when you send. Escape sequences are removed as sequences (so a coloured log arrives as its text, not as `[38;2;18;52;86m`), and a TAB is kept in the payload but laid out at the terminal's 8-column tab stops, so the draft and the echoed message line up. `--no-paste` restores the pre-0.6.3 behaviour |
|
|
543
543
|
| `?` | Open help (empty input) |
|
|
544
544
|
|
|
545
545
|
In **full-screen mode**, PgUp/PgDn, Shift+Up/Down and the right-edge scrollbar
|
|
@@ -2219,7 +2219,7 @@ env / `.env` → CLI flags**.
|
|
|
2219
2219
|
| `planModeDefault` | `false` | Start every session in plan mode. `--no-plan` still overrides it for a single run. |
|
|
2220
2220
|
| `planModeMaxAskRounds` | `4` | `ask_user` rounds allowed per user message, clamped to `[1, 10]`. The counter resets on your next message; steering does **not** reset it. |
|
|
2221
2221
|
| `planModeHumanTimeoutMs` | `1800000` | Ceiling on a single question / plan wait, clamped to `[60000, 7200000]`. Thirty minutes is far outside "went to get coffee", but a ceiling has to exist so an unattended terminal cannot wedge a CI job forever. |
|
|
2222
|
-
| `paste` | `true` | Recognise pasted text before Ink's key parser sees it: line breaks become buffer newlines instead of fifteen separate messages,
|
|
2222
|
+
| `paste` | `true` | Recognise pasted text before Ink's key parser sees it: line breaks become buffer newlines instead of fifteen separate messages, escape SEQUENCES are removed whole (deleting only the introducer byte is what put `[38;2;18;52;86m` on screen and in the model's input) as are stray control bytes, and a paste above 6 lines or 400 characters collapses to a one-line placeholder that expands when you send. `false` treats pasted bytes as keystrokes, which is what 0.6.2 did. |
|
|
2223
2223
|
| `historyEnabled` | `true` | Record submitted prompts to `prompt-history.jsonl` for `↑` recall. Setting it to `false` stops new writes; it does not delete what is already stored (that is `aragon history clear --yes`) and does not stop reading. See [Prompt history](#prompt-history). |
|
|
2224
2224
|
| `team.enabled` | `true` | Master switch for team subagents. `false` means the `task` tool is never registered — `/team on` in such a session saves the setting for next launch and says so, rather than advertising a tool that is not there. |
|
|
2225
2225
|
| `team.maxSubagents` | `5` | Subagents per dispatch, clamped to `[1, 10]`. The 10 is the hard ceiling and is enforced twice — on the config value and again on what the model asks for — so hand-editing this file cannot raise it. |
|
package/dist/agent/reducer.js
CHANGED
|
@@ -402,6 +402,36 @@ function releaseLiveTails(entries) {
|
|
|
402
402
|
});
|
|
403
403
|
return touched ? next : entries;
|
|
404
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Drop settled assistant entries that render nothing at all (defect 5).
|
|
407
|
+
*
|
|
408
|
+
* `turnStart` ALWAYS creates an entry, so a turn that only called a tool leaves a
|
|
409
|
+
* bubble behind that `AssistantEntry` then refuses to draw. A rendered absence is
|
|
410
|
+
* not the same thing as an absent entry: `virtual-window.ts:305` still estimates a
|
|
411
|
+
* row for it (two when `aborted`, and nothing draws the second), and `/save`
|
|
412
|
+
* exports a blank bubble. Settle is the only safe moment to prune — before it the
|
|
413
|
+
* entry is still the target of every `textDelta` / `thinkingDelta`.
|
|
414
|
+
*
|
|
415
|
+
* I-L1-1 — `streamingId` COMES BACK WITH THE ARRAY. If the dropped entry is the
|
|
416
|
+
* one that id pointed at, the id is cleared: leaving it set would point the whole
|
|
417
|
+
* reducer at an entry no `mapEntry` can ever match again, silently, because a
|
|
418
|
+
* `mapEntry` on a missing id is a no-op. `turnEnd` and `runEnd` clear the id
|
|
419
|
+
* themselves; `abortMark` does not, which is why the caller writes this back.
|
|
420
|
+
*
|
|
421
|
+
* Returns the SAME array when nothing was dropped, so every settled entry keeps
|
|
422
|
+
* its identity and `EntryView`'s memo boundary stays intact.
|
|
423
|
+
*/
|
|
424
|
+
function pruneEmptyAssistant(entries, streamingId) {
|
|
425
|
+
const isEmptySettledAssistant = (entry) => entry.kind === 'assistant' &&
|
|
426
|
+
!entry.streaming &&
|
|
427
|
+
entry.text.trim() === '' &&
|
|
428
|
+
(entry.thinking ?? '').trim() === '';
|
|
429
|
+
const kept = entries.filter((entry) => !isEmptySettledAssistant(entry));
|
|
430
|
+
if (kept.length === entries.length)
|
|
431
|
+
return { entries, streamingId };
|
|
432
|
+
const liveGone = streamingId !== undefined && !kept.some((entry) => entry.id === streamingId);
|
|
433
|
+
return { entries: kept, streamingId: liveGone ? undefined : streamingId };
|
|
434
|
+
}
|
|
405
435
|
function findToolEntryId(state, toolCallId) {
|
|
406
436
|
for (let i = state.entries.length - 1; i >= 0; i -= 1) {
|
|
407
437
|
const e = state.entries[i];
|
|
@@ -673,7 +703,10 @@ export function viewReducer(state, action) {
|
|
|
673
703
|
// branch ever had. Two branches writing one number is what P0-1 was.
|
|
674
704
|
usageTotal: addUsage(state.usageTotal, action.usage, action.costDelta),
|
|
675
705
|
// The turn produced an answer, so whatever it had to retry, it recovered.
|
|
676
|
-
|
|
706
|
+
// Defect 5: prune AFTER the mapEntry above has settled the entry, so an
|
|
707
|
+
// assistant bubble this turn never filled in is gone before the retry card
|
|
708
|
+
// is settled and before the height table ever sees it.
|
|
709
|
+
...settleRetryCard(state, pruneEmptyAssistant(entries, state.streamingId).entries, 'recovered'),
|
|
677
710
|
};
|
|
678
711
|
}
|
|
679
712
|
case 'runEnd': {
|
|
@@ -681,6 +714,12 @@ export function viewReducer(state, action) {
|
|
|
681
714
|
let entries = state.streamingId
|
|
682
715
|
? mapEntry(state.entries, state.streamingId, (e) => e.kind === 'assistant' ? { ...e, streaming: false, ...sealThinkingMs(e) } : e)
|
|
683
716
|
: state.entries;
|
|
717
|
+
/*
|
|
718
|
+
* Defect 5, and BEFORE the silent-failure guard below so the guard and the
|
|
719
|
+
* retry settle both see the pruned array. `streamingId: undefined` is set in
|
|
720
|
+
* the returned state either way, so the returned id is not needed here.
|
|
721
|
+
*/
|
|
722
|
+
entries = pruneEmptyAssistant(entries, state.streamingId).entries;
|
|
684
723
|
let seq = state.seq;
|
|
685
724
|
let droppedEntries = state.droppedEntries;
|
|
686
725
|
// Silent-failure guard (R2): a swallowed throw shows nothing otherwise.
|
|
@@ -750,13 +789,19 @@ export function viewReducer(state, action) {
|
|
|
750
789
|
const marked = state.streamingId
|
|
751
790
|
? mapEntry(state.entries, state.streamingId, (e) => e.kind === 'assistant' ? { ...e, streaming: false, aborted: true } : e)
|
|
752
791
|
: state.entries;
|
|
792
|
+
// Defect 5 / I-L1-1: prune AFTER the entry is settled, and write the
|
|
793
|
+
// returned id back below -- `abortMark` is the one settle path that does
|
|
794
|
+
// NOT clear `streamingId`, so a prune without that write-back would leave
|
|
795
|
+
// it naming an entry this branch has just removed.
|
|
796
|
+
const pruned = pruneEmptyAssistant(marked, state.streamingId);
|
|
753
797
|
return {
|
|
754
798
|
...state,
|
|
755
799
|
aborted: true,
|
|
800
|
+
streamingId: pruned.streamingId,
|
|
756
801
|
// An abort during a tool call is the COMMON way a tail is stranded --
|
|
757
802
|
// `Esc` while `npm test` runs -- so this is the clause manual row 9
|
|
758
803
|
// exercises (D-35 / AC-40).
|
|
759
|
-
...settleRetryCard(state, releaseLiveTails(
|
|
804
|
+
...settleRetryCard(state, releaseLiveTails(pruned.entries), 'interrupted'),
|
|
760
805
|
};
|
|
761
806
|
}
|
|
762
807
|
// `teamEntryId` is dropped with the entries it pointed at. Keeping it would
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control-SEQUENCE removal for text that crosses a boundary where a terminal
|
|
3
|
+
* escape must never become content (paste-control-sequence-residue, fix A'-1).
|
|
4
|
+
*
|
|
5
|
+
* WHY A DELETE-THE-WHOLE-SEQUENCE RULE EXISTS. Deleting only the introducer
|
|
6
|
+
* byte turns an escape into visible text: `ESC [ 3 8 ; 2 m` loses its ESC and
|
|
7
|
+
* leaves `[38;2m`, which is a run of digits and semicolons somebody then reads
|
|
8
|
+
* on screen and sends to the model. The bytes were never content; the sequence
|
|
9
|
+
* has to go as a unit.
|
|
10
|
+
*
|
|
11
|
+
* WHY IT IS NOT `tools/terminal-output.ts`'s REGEXES MOVED HERE. Those four
|
|
12
|
+
* regexes answer "what does this look like" for a terminal row that is still
|
|
13
|
+
* being written, and they are fed text whose unfinished tail has ALREADY been
|
|
14
|
+
* cut off and parked in a carry by the caller (`sanitizeChunk`,
|
|
15
|
+
* `toDisplayRow`). Pasted text is one-shot: there is no next chunk, so a
|
|
16
|
+
* truncated sequence needs a rule of its own. Measured against the stream
|
|
17
|
+
* version of the same input, `ESC [ 3 8 ; 2 ; 1 8 ; 5 2 ; 8 6` followed by
|
|
18
|
+
* prose loses the first letter of the prose (`n`), and an unterminated OSC
|
|
19
|
+
* leaves its whole body behind.
|
|
20
|
+
*
|
|
21
|
+
* THE FOUR DECISIONS THIS MODULE ENCODES:
|
|
22
|
+
*
|
|
23
|
+
* 1. A sequence introduced by a real introducer byte (`0x1B`, `0x9B`, `0x9D`,
|
|
24
|
+
* ...) is removed as a unit; bare text that merely LOOKS like the inside of
|
|
25
|
+
* one (`[38;2;18;52;86m` typed or pasted without an ESC) is left exactly as
|
|
26
|
+
* it is, character for character.
|
|
27
|
+
* 2. A string-type sequence (OSC / DCS / SOS / PM / APC) runs to BEL or ST, and
|
|
28
|
+
* an unterminated one is removed UP TO THE END OF ITS LINE. It never crosses
|
|
29
|
+
* `\n` and never guesses at a terminator: a truncated sequence is the most
|
|
30
|
+
* common shape in a clipboard, and the only safe reading of it is "the rest
|
|
31
|
+
* of this line was never text".
|
|
32
|
+
* 3. A truncated CSI loses its introducer and its parameter bytes and NOTHING
|
|
33
|
+
* ELSE. `ESC [ 3 1 m` split mid-parameter is parameters, not prose, but the
|
|
34
|
+
* byte after them is the user's own character and must survive.
|
|
35
|
+
* 4. A C1 CSI (`0x9B`) with no parameter byte at all is treated as the stray
|
|
36
|
+
* control byte it is most likely to be rather than as a sequence, because a
|
|
37
|
+
* lone `0x9B` followed by one letter is otherwise indistinguishable from a
|
|
38
|
+
* non-UTF-8 file's stray byte followed by content -- and consuming that
|
|
39
|
+
* letter would be the very data loss this module exists to prevent.
|
|
40
|
+
*
|
|
41
|
+
* Pure, ASCII-only, React-free, Ink-free, and TOTAL: it never throws and it
|
|
42
|
+
* never reorders or mangles the bytes it keeps. No new dependency.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Remove every escape/control sequence, keeping all other bytes in order.
|
|
46
|
+
*
|
|
47
|
+
* KEEPS `\t`, `\n` AND `\r`: they are line structure, and `\t` inside pasted
|
|
48
|
+
* indentation is content the model is supposed to receive. Stray C0/C1 bytes
|
|
49
|
+
* (and DEL) are NOT this function's job -- they are removed by the caller that
|
|
50
|
+
* wants them gone, so one rule has one owner.
|
|
51
|
+
*/
|
|
52
|
+
export declare function stripControlSequences(text: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* True when `text` carries a sequence a KEYBOARD cannot physically produce.
|
|
55
|
+
*
|
|
56
|
+
* Used at the keystroke branch of the stdin filter: a single-line paste that
|
|
57
|
+
* carries ESC fails `classifyChunk`'s two rules (no line break, and
|
|
58
|
+
* `HAS_CONTROL_EXCEPT_TAB`) and would otherwise reach Ink as raw bytes --
|
|
59
|
+
* either typing `[38;2;18;52;86m` with a live ESC into the draft, or throwing
|
|
60
|
+
* an uncaught `TypeError` inside Ink's key parser.
|
|
61
|
+
*
|
|
62
|
+
* THREE THINGS MUST NOT MATCH, and the shapes below are why:
|
|
63
|
+
* - SGR mouse reports (`ESC [ < 0 ; 1 ; 2 m`) are stripped by `splitMouseEvents`
|
|
64
|
+
* BEFORE this test, and `<` is not a parameter byte, so the SGR rule cannot
|
|
65
|
+
* see them anyway.
|
|
66
|
+
* - Arrow / function / Home / End keys (`ESC [ A`, `ESC [ 1 ; 5 D`, `ESC [ 3 ~`,
|
|
67
|
+
* `ESC [ Z`) never end in `m`.
|
|
68
|
+
* - `Alt`+letter (`ESC` followed by one character) has no introducer body, so
|
|
69
|
+
* the string-type rule does not fire.
|
|
70
|
+
*/
|
|
71
|
+
export declare function hasPasteOnlySequence(text: string): boolean;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control-SEQUENCE removal for text that crosses a boundary where a terminal
|
|
3
|
+
* escape must never become content (paste-control-sequence-residue, fix A'-1).
|
|
4
|
+
*
|
|
5
|
+
* WHY A DELETE-THE-WHOLE-SEQUENCE RULE EXISTS. Deleting only the introducer
|
|
6
|
+
* byte turns an escape into visible text: `ESC [ 3 8 ; 2 m` loses its ESC and
|
|
7
|
+
* leaves `[38;2m`, which is a run of digits and semicolons somebody then reads
|
|
8
|
+
* on screen and sends to the model. The bytes were never content; the sequence
|
|
9
|
+
* has to go as a unit.
|
|
10
|
+
*
|
|
11
|
+
* WHY IT IS NOT `tools/terminal-output.ts`'s REGEXES MOVED HERE. Those four
|
|
12
|
+
* regexes answer "what does this look like" for a terminal row that is still
|
|
13
|
+
* being written, and they are fed text whose unfinished tail has ALREADY been
|
|
14
|
+
* cut off and parked in a carry by the caller (`sanitizeChunk`,
|
|
15
|
+
* `toDisplayRow`). Pasted text is one-shot: there is no next chunk, so a
|
|
16
|
+
* truncated sequence needs a rule of its own. Measured against the stream
|
|
17
|
+
* version of the same input, `ESC [ 3 8 ; 2 ; 1 8 ; 5 2 ; 8 6` followed by
|
|
18
|
+
* prose loses the first letter of the prose (`n`), and an unterminated OSC
|
|
19
|
+
* leaves its whole body behind.
|
|
20
|
+
*
|
|
21
|
+
* THE FOUR DECISIONS THIS MODULE ENCODES:
|
|
22
|
+
*
|
|
23
|
+
* 1. A sequence introduced by a real introducer byte (`0x1B`, `0x9B`, `0x9D`,
|
|
24
|
+
* ...) is removed as a unit; bare text that merely LOOKS like the inside of
|
|
25
|
+
* one (`[38;2;18;52;86m` typed or pasted without an ESC) is left exactly as
|
|
26
|
+
* it is, character for character.
|
|
27
|
+
* 2. A string-type sequence (OSC / DCS / SOS / PM / APC) runs to BEL or ST, and
|
|
28
|
+
* an unterminated one is removed UP TO THE END OF ITS LINE. It never crosses
|
|
29
|
+
* `\n` and never guesses at a terminator: a truncated sequence is the most
|
|
30
|
+
* common shape in a clipboard, and the only safe reading of it is "the rest
|
|
31
|
+
* of this line was never text".
|
|
32
|
+
* 3. A truncated CSI loses its introducer and its parameter bytes and NOTHING
|
|
33
|
+
* ELSE. `ESC [ 3 1 m` split mid-parameter is parameters, not prose, but the
|
|
34
|
+
* byte after them is the user's own character and must survive.
|
|
35
|
+
* 4. A C1 CSI (`0x9B`) with no parameter byte at all is treated as the stray
|
|
36
|
+
* control byte it is most likely to be rather than as a sequence, because a
|
|
37
|
+
* lone `0x9B` followed by one letter is otherwise indistinguishable from a
|
|
38
|
+
* non-UTF-8 file's stray byte followed by content -- and consuming that
|
|
39
|
+
* letter would be the very data loss this module exists to prevent.
|
|
40
|
+
*
|
|
41
|
+
* Pure, ASCII-only, React-free, Ink-free, and TOTAL: it never throws and it
|
|
42
|
+
* never reorders or mangles the bytes it keeps. No new dependency.
|
|
43
|
+
*/
|
|
44
|
+
/** ESC -- the introducer every terminal shares. */
|
|
45
|
+
const ESC = 0x1b;
|
|
46
|
+
/** LF. No rule in this module may consume it (decision 2). */
|
|
47
|
+
const LF = 0x0a;
|
|
48
|
+
/** BEL terminates an OSC. */
|
|
49
|
+
const BEL = 0x07;
|
|
50
|
+
/** `[` after ESC is CSI, and `\` after ESC is the two-byte ST. */
|
|
51
|
+
const BRACKET = 0x5b;
|
|
52
|
+
const BACKSLASH = 0x5c;
|
|
53
|
+
/** The ASCII second byte of each string-type sequence (`ESC ]`, `ESC P`, ...). */
|
|
54
|
+
const ESC_DCS = 0x50;
|
|
55
|
+
const ESC_SOS = 0x58;
|
|
56
|
+
const ESC_OSC = 0x5d;
|
|
57
|
+
const ESC_PM = 0x5e;
|
|
58
|
+
const ESC_APC = 0x5f;
|
|
59
|
+
/** C1 introducers, as written by a source that is not UTF-8 clean. */
|
|
60
|
+
const C1_CSI = 0x9b;
|
|
61
|
+
const C1_ST = 0x9c;
|
|
62
|
+
const C1_OSC = 0x9d;
|
|
63
|
+
const C1_DCS = 0x90;
|
|
64
|
+
const C1_SOS = 0x98;
|
|
65
|
+
const C1_PM = 0x9e;
|
|
66
|
+
const C1_APC = 0x9f;
|
|
67
|
+
/** Every byte that can START a sequence this module removes. */
|
|
68
|
+
const ANY_INTRODUCER = /[\u001b\u0090\u0098\u009b\u009d\u009e\u009f]/;
|
|
69
|
+
/**
|
|
70
|
+
* `CSI ... m` (SGR), in both encodings: `ESC [ digits ; m` and the single-byte
|
|
71
|
+
* C1 form `0x9B digits ; m`. NO KEY PRESS PRODUCES ONE, which is what makes it
|
|
72
|
+
* usable as the discriminator for an unframed paste in `stdin-filter.ts`.
|
|
73
|
+
*/
|
|
74
|
+
const SGR_SEQUENCE = /\u001b\[[0-9;]*m|\u009b[0-9;]*m/;
|
|
75
|
+
/**
|
|
76
|
+
* A string-type introducer followed by at least one body byte. An `Alt`+key
|
|
77
|
+
* press is exactly two bytes, so "introducer plus a body" is already outside
|
|
78
|
+
* what a keyboard can emit.
|
|
79
|
+
*/
|
|
80
|
+
const STRING_SEQUENCE = /(?:\u001b[\u0050\u0058\u005d\u005e\u005f]|[\u0090\u0098\u009d\u009e\u009f])[\s\S]/;
|
|
81
|
+
/** CSI final byte: `@` through `~`. */
|
|
82
|
+
function isFinalByte(code) {
|
|
83
|
+
return code >= 0x40 && code <= 0x7e;
|
|
84
|
+
}
|
|
85
|
+
/** CSI parameter byte: `0` through `?`. */
|
|
86
|
+
function isParamByte(code) {
|
|
87
|
+
return code >= 0x30 && code <= 0x3f;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* CSI intermediate byte: `!` through `/`.
|
|
91
|
+
*
|
|
92
|
+
* SPACE (`0x20`) IS DELIBERATELY EXCLUDED (decision 3). The standard allows it,
|
|
93
|
+
* but a pasted sequence cut mid-parameter is followed by ` ` and then prose,
|
|
94
|
+
* and the first letter of that prose is a legal final byte -- so honouring the
|
|
95
|
+
* space would silently eat it. `CSI Ps SP q` (cursor style) is the only shape
|
|
96
|
+
* that gets worse, and no child process writes that to its stdout.
|
|
97
|
+
*/
|
|
98
|
+
function isIntermediateByte(code) {
|
|
99
|
+
return code >= 0x21 && code <= 0x2f;
|
|
100
|
+
}
|
|
101
|
+
/** True for the C1 introducers that open a string, not a CSI. */
|
|
102
|
+
function isC1StringIntroducer(code) {
|
|
103
|
+
return code === C1_OSC || code === C1_DCS || code === C1_SOS || code === C1_PM || code === C1_APC;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* End of the string-type sequence whose body starts at `from`.
|
|
107
|
+
*
|
|
108
|
+
* Stops AT the line break rather than past it, and reports "consumed to here"
|
|
109
|
+
* for a body that never terminates (decision 2).
|
|
110
|
+
*/
|
|
111
|
+
function stringEnd(text, from) {
|
|
112
|
+
let i = from;
|
|
113
|
+
while (i < text.length) {
|
|
114
|
+
const code = text.charCodeAt(i);
|
|
115
|
+
if (code === BEL || code === C1_ST)
|
|
116
|
+
return i + 1;
|
|
117
|
+
if (code === LF)
|
|
118
|
+
return i;
|
|
119
|
+
if (code === ESC && text.charCodeAt(i + 1) === BACKSLASH)
|
|
120
|
+
return i + 2;
|
|
121
|
+
i += 1;
|
|
122
|
+
}
|
|
123
|
+
return i;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* End of the CSI whose parameter run starts at `from`.
|
|
127
|
+
*
|
|
128
|
+
* `requireParams` implements decision 4: with no parameter byte at all, a C1
|
|
129
|
+
* CSI is only the introducer byte, and its "final byte" is content.
|
|
130
|
+
*/
|
|
131
|
+
function csiEnd(text, from, requireParams) {
|
|
132
|
+
let i = from;
|
|
133
|
+
let sawParams = false;
|
|
134
|
+
while (i < text.length && isParamByte(text.charCodeAt(i))) {
|
|
135
|
+
sawParams = true;
|
|
136
|
+
i += 1;
|
|
137
|
+
}
|
|
138
|
+
if (requireParams && !sawParams)
|
|
139
|
+
return from;
|
|
140
|
+
while (i < text.length && isIntermediateByte(text.charCodeAt(i)))
|
|
141
|
+
i += 1;
|
|
142
|
+
if (i < text.length && isFinalByte(text.charCodeAt(i)))
|
|
143
|
+
return i + 1;
|
|
144
|
+
return i;
|
|
145
|
+
}
|
|
146
|
+
/** End of the sequence starting with ESC at `index` (decision 1 / 3). */
|
|
147
|
+
function escapeEnd(text, index) {
|
|
148
|
+
if (index + 1 >= text.length)
|
|
149
|
+
return index + 1;
|
|
150
|
+
const next = text.charCodeAt(index + 1);
|
|
151
|
+
if (next === BRACKET)
|
|
152
|
+
return csiEnd(text, index + 2, false);
|
|
153
|
+
if (next === ESC_OSC || next === ESC_DCS || next === ESC_SOS || next === ESC_PM
|
|
154
|
+
|| next === ESC_APC) {
|
|
155
|
+
return stringEnd(text, index + 2);
|
|
156
|
+
}
|
|
157
|
+
if (next === LF)
|
|
158
|
+
return index + 1;
|
|
159
|
+
return index + 2;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* End of the sequence starting at `index`, or `-1` when no sequence starts
|
|
163
|
+
* there.
|
|
164
|
+
*/
|
|
165
|
+
function sequenceEnd(text, index) {
|
|
166
|
+
const code = text.charCodeAt(index);
|
|
167
|
+
if (code === ESC)
|
|
168
|
+
return escapeEnd(text, index);
|
|
169
|
+
if (code === C1_CSI)
|
|
170
|
+
return csiEnd(text, index + 1, true);
|
|
171
|
+
if (isC1StringIntroducer(code)) {
|
|
172
|
+
return index + 1 >= text.length ? index + 1 : stringEnd(text, index + 1);
|
|
173
|
+
}
|
|
174
|
+
return -1;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Remove every escape/control sequence, keeping all other bytes in order.
|
|
178
|
+
*
|
|
179
|
+
* KEEPS `\t`, `\n` AND `\r`: they are line structure, and `\t` inside pasted
|
|
180
|
+
* indentation is content the model is supposed to receive. Stray C0/C1 bytes
|
|
181
|
+
* (and DEL) are NOT this function's job -- they are removed by the caller that
|
|
182
|
+
* wants them gone, so one rule has one owner.
|
|
183
|
+
*/
|
|
184
|
+
export function stripControlSequences(text) {
|
|
185
|
+
if (!ANY_INTRODUCER.test(text))
|
|
186
|
+
return text;
|
|
187
|
+
let out = '';
|
|
188
|
+
let kept = 0;
|
|
189
|
+
let i = 0;
|
|
190
|
+
while (i < text.length) {
|
|
191
|
+
const end = sequenceEnd(text, i);
|
|
192
|
+
if (end === -1) {
|
|
193
|
+
i += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
out += text.slice(kept, i);
|
|
197
|
+
i = end;
|
|
198
|
+
kept = end;
|
|
199
|
+
}
|
|
200
|
+
return kept === 0 ? text : out + text.slice(kept);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* True when `text` carries a sequence a KEYBOARD cannot physically produce.
|
|
204
|
+
*
|
|
205
|
+
* Used at the keystroke branch of the stdin filter: a single-line paste that
|
|
206
|
+
* carries ESC fails `classifyChunk`'s two rules (no line break, and
|
|
207
|
+
* `HAS_CONTROL_EXCEPT_TAB`) and would otherwise reach Ink as raw bytes --
|
|
208
|
+
* either typing `[38;2;18;52;86m` with a live ESC into the draft, or throwing
|
|
209
|
+
* an uncaught `TypeError` inside Ink's key parser.
|
|
210
|
+
*
|
|
211
|
+
* THREE THINGS MUST NOT MATCH, and the shapes below are why:
|
|
212
|
+
* - SGR mouse reports (`ESC [ < 0 ; 1 ; 2 m`) are stripped by `splitMouseEvents`
|
|
213
|
+
* BEFORE this test, and `<` is not a parameter byte, so the SGR rule cannot
|
|
214
|
+
* see them anyway.
|
|
215
|
+
* - Arrow / function / Home / End keys (`ESC [ A`, `ESC [ 1 ; 5 D`, `ESC [ 3 ~`,
|
|
216
|
+
* `ESC [ Z`) never end in `m`.
|
|
217
|
+
* - `Alt`+letter (`ESC` followed by one character) has no introducer body, so
|
|
218
|
+
* the string-type rule does not fire.
|
|
219
|
+
*/
|
|
220
|
+
export function hasPasteOnlySequence(text) {
|
|
221
|
+
return SGR_SEQUENCE.test(text) || STRING_SEQUENCE.test(text);
|
|
222
|
+
}
|
package/dist/input/limits.d.ts
CHANGED
|
@@ -39,6 +39,26 @@ export declare const PASTE_BURST_MS = 15;
|
|
|
39
39
|
* which is the rule `MAX_PENDING_MOUSE_CHARS` already encodes for mouse reports.
|
|
40
40
|
*/
|
|
41
41
|
export declare const PASTE_ASSEMBLY_MAX_MS = 2000;
|
|
42
|
+
/**
|
|
43
|
+
* How long a paste-marker head that was ALREADY released to the pass-through as
|
|
44
|
+
* ordinary text may still be recognised as the head of a marker (defect 2: the
|
|
45
|
+
* torn-marker rescue in `stdin-filter.ts`).
|
|
46
|
+
*
|
|
47
|
+
* STRUCTURAL, NOT POLICY: no config key exposes it, because it bounds what the
|
|
48
|
+
* byte stream can physically do -- a marker split across two reads -- and not
|
|
49
|
+
* what a user prefers. What it does NOT do is delay a keystroke: the 12 ms
|
|
50
|
+
* pending-flush window in `stdin-filter.ts` still releases a lone ESC exactly as
|
|
51
|
+
* before, and this window only governs whether that release may be spliced back
|
|
52
|
+
* in front of the next chunk so an assembled marker is consumed, not typed.
|
|
53
|
+
*
|
|
54
|
+
* THE FALSE POSITIVE IS THE TRADE-OFF, AND IT IS ACCEPTED. Firing the rescue
|
|
55
|
+
* wrongly needs the user to press Esc and then TYPE the literal characters
|
|
56
|
+
* `[200~` or `[201~` inside this window -- a sequence no keyboard produces, and
|
|
57
|
+
* at worst five characters of lost typing. Paying that buys a fix for visible
|
|
58
|
+
* `[200~` / `[201~` garbage in the draft, which is the failure the whole paste
|
|
59
|
+
* feature exists to prevent.
|
|
60
|
+
*/
|
|
61
|
+
export declare const PASTE_MARKER_RESCAN_TTL_MS = 250;
|
|
42
62
|
/**
|
|
43
63
|
* The inline frame the filter writes into the stream Ink reads (D-3 / I-5).
|
|
44
64
|
*
|
package/dist/input/limits.js
CHANGED
|
@@ -39,6 +39,26 @@ export const PASTE_BURST_MS = 15;
|
|
|
39
39
|
* which is the rule `MAX_PENDING_MOUSE_CHARS` already encodes for mouse reports.
|
|
40
40
|
*/
|
|
41
41
|
export const PASTE_ASSEMBLY_MAX_MS = 2_000;
|
|
42
|
+
/**
|
|
43
|
+
* How long a paste-marker head that was ALREADY released to the pass-through as
|
|
44
|
+
* ordinary text may still be recognised as the head of a marker (defect 2: the
|
|
45
|
+
* torn-marker rescue in `stdin-filter.ts`).
|
|
46
|
+
*
|
|
47
|
+
* STRUCTURAL, NOT POLICY: no config key exposes it, because it bounds what the
|
|
48
|
+
* byte stream can physically do -- a marker split across two reads -- and not
|
|
49
|
+
* what a user prefers. What it does NOT do is delay a keystroke: the 12 ms
|
|
50
|
+
* pending-flush window in `stdin-filter.ts` still releases a lone ESC exactly as
|
|
51
|
+
* before, and this window only governs whether that release may be spliced back
|
|
52
|
+
* in front of the next chunk so an assembled marker is consumed, not typed.
|
|
53
|
+
*
|
|
54
|
+
* THE FALSE POSITIVE IS THE TRADE-OFF, AND IT IS ACCEPTED. Firing the rescue
|
|
55
|
+
* wrongly needs the user to press Esc and then TYPE the literal characters
|
|
56
|
+
* `[200~` or `[201~` inside this window -- a sequence no keyboard produces, and
|
|
57
|
+
* at worst five characters of lost typing. Paying that buys a fix for visible
|
|
58
|
+
* `[200~` / `[201~` garbage in the draft, which is the failure the whole paste
|
|
59
|
+
* feature exists to prevent.
|
|
60
|
+
*/
|
|
61
|
+
export const PASTE_MARKER_RESCAN_TTL_MS = 250;
|
|
42
62
|
/**
|
|
43
63
|
* The inline frame the filter writes into the stream Ink reads (D-3 / I-5).
|
|
44
64
|
*
|