@levu/snap 0.3.7 → 0.3.9
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
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.9] - 2026-02-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Added multiline paste recovery for cases where readline submit flow only exposes the last pasted line.
|
|
12
|
+
- Recovery now uses recent raw paste chunks to reconstruct full multi-line content when the submit payload is unexpectedly single-line.
|
|
13
|
+
- Added regression coverage for the “last line only after multiline paste” scenario.
|
|
14
|
+
|
|
15
|
+
## [0.3.8] - 2026-02-26
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Prevented premature submit during non-bracketed multi-line paste bursts where newline Enter events were treated as user submit before all pasted lines arrived.
|
|
19
|
+
- Added explicit paste-burst detection from raw input stream and suppressed Enter-submit only during the short paste window.
|
|
20
|
+
- Kept bracketed paste behavior intact and avoided false suppression after paste-end marker.
|
|
21
|
+
- Added regression coverage for non-bracketed multi-line paste with delayed second-line arrival.
|
|
22
|
+
|
|
8
23
|
## [0.3.7] - 2026-02-26
|
|
9
24
|
|
|
10
25
|
### Fixed
|
|
@@ -35,6 +35,9 @@ export const createMultilineTextPrompt = () => {
|
|
|
35
35
|
let bracketPasteProbe = '';
|
|
36
36
|
let pendingEnterSubmit = false;
|
|
37
37
|
let pendingEnterSubmitTimer;
|
|
38
|
+
let recentPasteBurstUntil = 0;
|
|
39
|
+
let sawPasteLikeRawInput = false;
|
|
40
|
+
let recentPasteRawBuffer = '';
|
|
38
41
|
const cleanup = () => {
|
|
39
42
|
if (pendingEnterSubmitTimer) {
|
|
40
43
|
clearTimeout(pendingEnterSubmitTimer);
|
|
@@ -113,8 +116,35 @@ export const createMultilineTextPrompt = () => {
|
|
|
113
116
|
const normalizeGhosttyInlineTokens = (raw) => {
|
|
114
117
|
return raw.replace(/(?:\u001b\[13;2u|\[13;2u|\u001b\[27;2;13~|\[27;2;13~|13~|~13)/g, '\n');
|
|
115
118
|
};
|
|
119
|
+
const stripAnsiControls = (raw) => {
|
|
120
|
+
return String(raw || '')
|
|
121
|
+
.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
122
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '');
|
|
123
|
+
};
|
|
124
|
+
const recoverMultilineFromRawPaste = (primary) => {
|
|
125
|
+
if (!sawPasteLikeRawInput)
|
|
126
|
+
return '';
|
|
127
|
+
if (primary.includes('\n'))
|
|
128
|
+
return '';
|
|
129
|
+
if (!recentPasteRawBuffer)
|
|
130
|
+
return '';
|
|
131
|
+
const recoveredLines = stripAnsiControls(recentPasteRawBuffer)
|
|
132
|
+
.replace(/\r/g, '\n')
|
|
133
|
+
.split('\n')
|
|
134
|
+
.map((line) => line.trim())
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
if (recoveredLines.length < 2)
|
|
137
|
+
return '';
|
|
138
|
+
const normalizedPrimary = String(primary || '').trim();
|
|
139
|
+
const recoveredLastLine = recoveredLines[recoveredLines.length - 1] || '';
|
|
140
|
+
if (normalizedPrimary && recoveredLastLine !== normalizedPrimary)
|
|
141
|
+
return '';
|
|
142
|
+
return recoveredLines.join('\n');
|
|
143
|
+
};
|
|
116
144
|
const buildSubmitValue = () => {
|
|
117
|
-
|
|
145
|
+
const primary = normalizeGhosttyInlineTokens(lines.concat(getLiveLine()).join('\n'));
|
|
146
|
+
const recovered = recoverMultilineFromRawPaste(primary);
|
|
147
|
+
return recovered || primary;
|
|
118
148
|
};
|
|
119
149
|
const absorbLine = (line) => {
|
|
120
150
|
if (line.trim() === '') {
|
|
@@ -141,6 +171,8 @@ export const createMultilineTextPrompt = () => {
|
|
|
141
171
|
const BRACKET_PASTE_START = '\u001b[200~';
|
|
142
172
|
const BRACKET_PASTE_END = '\u001b[201~';
|
|
143
173
|
const BRACKET_PASTE_PROBE_MAX = 96;
|
|
174
|
+
const PASTE_BURST_WINDOW_MS = 70;
|
|
175
|
+
const RAW_PASTE_BUFFER_MAX = 8192;
|
|
144
176
|
const updateBracketPasteState = (chunk) => {
|
|
145
177
|
if (!chunk)
|
|
146
178
|
return;
|
|
@@ -160,6 +192,22 @@ export const createMultilineTextPrompt = () => {
|
|
|
160
192
|
}
|
|
161
193
|
}
|
|
162
194
|
};
|
|
195
|
+
const notePossiblePasteBurst = (chunk) => {
|
|
196
|
+
if (!chunk)
|
|
197
|
+
return;
|
|
198
|
+
const normalized = chunk
|
|
199
|
+
.replaceAll(BRACKET_PASTE_START, '')
|
|
200
|
+
.replaceAll(BRACKET_PASTE_END, '');
|
|
201
|
+
if (!normalized)
|
|
202
|
+
return;
|
|
203
|
+
// In raw mode, normal typing usually arrives as single-byte chunks.
|
|
204
|
+
// Multi-byte chunks and newlines are strong signals that input is a paste burst.
|
|
205
|
+
if (normalized.length > 1 || /[\r\n]/.test(normalized)) {
|
|
206
|
+
sawPasteLikeRawInput = true;
|
|
207
|
+
recentPasteRawBuffer = `${recentPasteRawBuffer}${normalized}`.slice(-RAW_PASTE_BUFFER_MAX);
|
|
208
|
+
recentPasteBurstUntil = Math.max(recentPasteBurstUntil, Date.now() + PASTE_BURST_WINDOW_MS);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
163
211
|
showPrompt();
|
|
164
212
|
// Handle paste from clipboard
|
|
165
213
|
const handlePaste = async () => {
|
|
@@ -200,12 +248,19 @@ export const createMultilineTextPrompt = () => {
|
|
|
200
248
|
rl.on('line', (line) => {
|
|
201
249
|
if (cancelled)
|
|
202
250
|
return;
|
|
251
|
+
const now = Date.now();
|
|
203
252
|
if (pendingEnterSubmit) {
|
|
253
|
+
const likelyPasteFlow = now < recentPasteBurstUntil;
|
|
204
254
|
pendingEnterSubmit = false;
|
|
205
255
|
if (pendingEnterSubmitTimer) {
|
|
206
256
|
clearTimeout(pendingEnterSubmitTimer);
|
|
207
257
|
pendingEnterSubmitTimer = undefined;
|
|
208
258
|
}
|
|
259
|
+
if (likelyPasteFlow) {
|
|
260
|
+
absorbLine(line);
|
|
261
|
+
showPrompt();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
209
264
|
absorbLine(line);
|
|
210
265
|
submit(buildSubmitValue());
|
|
211
266
|
return;
|
|
@@ -231,7 +286,6 @@ export const createMultilineTextPrompt = () => {
|
|
|
231
286
|
showPrompt();
|
|
232
287
|
return;
|
|
233
288
|
}
|
|
234
|
-
const now = Date.now();
|
|
235
289
|
// Check for double Enter to submit
|
|
236
290
|
if (line === '' && now - lastEnterTime < DOUBLE_ENTER_TIMEOUT) {
|
|
237
291
|
submit(buildSubmitValue());
|
|
@@ -262,6 +316,7 @@ export const createMultilineTextPrompt = () => {
|
|
|
262
316
|
return;
|
|
263
317
|
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
|
|
264
318
|
updateBracketPasteState(text);
|
|
319
|
+
notePossiblePasteBurst(text);
|
|
265
320
|
};
|
|
266
321
|
input.on('data', dataListener);
|
|
267
322
|
keypressListener = async (str, key) => {
|
|
@@ -319,6 +374,11 @@ export const createMultilineTextPrompt = () => {
|
|
|
319
374
|
insertNewline();
|
|
320
375
|
}
|
|
321
376
|
else if (key.name === 'enter' || key.name === 'return') {
|
|
377
|
+
const now = Date.now();
|
|
378
|
+
const likelyPasteReturn = now < recentPasteBurstUntil;
|
|
379
|
+
if (likelyPasteReturn) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
322
382
|
if (key.shift || key.alt) {
|
|
323
383
|
ignoreNextLineEvent = true;
|
|
324
384
|
// Shift+Enter / Alt+Enter inserts a new line.
|