@levu/snap 0.3.6 → 0.3.8
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,22 @@ 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.8] - 2026-02-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Prevented premature submit during non-bracketed multi-line paste bursts where newline Enter events were treated as user submit before all pasted lines arrived.
|
|
12
|
+
- Added explicit paste-burst detection from raw input stream and suppressed Enter-submit only during the short paste window.
|
|
13
|
+
- Kept bracketed paste behavior intact and avoided false suppression after paste-end marker.
|
|
14
|
+
- Added regression coverage for non-bracketed multi-line paste with delayed second-line arrival.
|
|
15
|
+
|
|
16
|
+
## [0.3.7] - 2026-02-26
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Fixed Enter-submit race in multiline prompt where pasted multi-line input could lose the final line and submit only the first line.
|
|
20
|
+
- Enter submit now waits briefly for the corresponding readline `line` event (with fallback timeout), preserving pending paste buffer content.
|
|
21
|
+
- Prompt rendering now prefers live readline buffer content so pending pasted lines are visible before submit.
|
|
22
|
+
- Added regression coverage for the “first line only after multiline paste” scenario.
|
|
23
|
+
|
|
8
24
|
## [0.3.6] - 2026-02-26
|
|
9
25
|
|
|
10
26
|
### Fixed
|
|
@@ -33,7 +33,14 @@ export const createMultilineTextPrompt = () => {
|
|
|
33
33
|
let expectingGhosttySequenceEcho = false;
|
|
34
34
|
let bracketPasteActive = false;
|
|
35
35
|
let bracketPasteProbe = '';
|
|
36
|
+
let pendingEnterSubmit = false;
|
|
37
|
+
let pendingEnterSubmitTimer;
|
|
38
|
+
let recentPasteBurstUntil = 0;
|
|
36
39
|
const cleanup = () => {
|
|
40
|
+
if (pendingEnterSubmitTimer) {
|
|
41
|
+
clearTimeout(pendingEnterSubmitTimer);
|
|
42
|
+
pendingEnterSubmitTimer = undefined;
|
|
43
|
+
}
|
|
37
44
|
if (keypressListener) {
|
|
38
45
|
input.off('keypress', keypressListener);
|
|
39
46
|
}
|
|
@@ -110,6 +117,19 @@ export const createMultilineTextPrompt = () => {
|
|
|
110
117
|
const buildSubmitValue = () => {
|
|
111
118
|
return normalizeGhosttyInlineTokens(lines.concat(getLiveLine()).join('\n'));
|
|
112
119
|
};
|
|
120
|
+
const absorbLine = (line) => {
|
|
121
|
+
if (line.trim() === '') {
|
|
122
|
+
if (currentLine !== '') {
|
|
123
|
+
lines.push(currentLine);
|
|
124
|
+
currentLine = '';
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (currentLine !== '') {
|
|
129
|
+
lines.push(currentLine);
|
|
130
|
+
}
|
|
131
|
+
currentLine = line;
|
|
132
|
+
};
|
|
113
133
|
const insertNewline = () => {
|
|
114
134
|
lines.push(getLiveLine());
|
|
115
135
|
currentLine = '';
|
|
@@ -117,11 +137,12 @@ export const createMultilineTextPrompt = () => {
|
|
|
117
137
|
output.write(`\n${pc.dim('> ')}`);
|
|
118
138
|
};
|
|
119
139
|
const showPrompt = () => {
|
|
120
|
-
output.write(`\n${pc.dim('> ')}${
|
|
140
|
+
output.write(`\n${pc.dim('> ')}${getLiveLine()}`);
|
|
121
141
|
};
|
|
122
142
|
const BRACKET_PASTE_START = '\u001b[200~';
|
|
123
143
|
const BRACKET_PASTE_END = '\u001b[201~';
|
|
124
144
|
const BRACKET_PASTE_PROBE_MAX = 96;
|
|
145
|
+
const PASTE_BURST_WINDOW_MS = 70;
|
|
125
146
|
const updateBracketPasteState = (chunk) => {
|
|
126
147
|
if (!chunk)
|
|
127
148
|
return;
|
|
@@ -141,6 +162,20 @@ export const createMultilineTextPrompt = () => {
|
|
|
141
162
|
}
|
|
142
163
|
}
|
|
143
164
|
};
|
|
165
|
+
const notePossiblePasteBurst = (chunk) => {
|
|
166
|
+
if (!chunk)
|
|
167
|
+
return;
|
|
168
|
+
const normalized = chunk
|
|
169
|
+
.replaceAll(BRACKET_PASTE_START, '')
|
|
170
|
+
.replaceAll(BRACKET_PASTE_END, '');
|
|
171
|
+
if (!normalized)
|
|
172
|
+
return;
|
|
173
|
+
// In raw mode, normal typing usually arrives as single-byte chunks.
|
|
174
|
+
// Multi-byte chunks and newlines are strong signals that input is a paste burst.
|
|
175
|
+
if (normalized.length > 1 || /[\r\n]/.test(normalized)) {
|
|
176
|
+
recentPasteBurstUntil = Math.max(recentPasteBurstUntil, Date.now() + PASTE_BURST_WINDOW_MS);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
144
179
|
showPrompt();
|
|
145
180
|
// Handle paste from clipboard
|
|
146
181
|
const handlePaste = async () => {
|
|
@@ -181,6 +216,23 @@ export const createMultilineTextPrompt = () => {
|
|
|
181
216
|
rl.on('line', (line) => {
|
|
182
217
|
if (cancelled)
|
|
183
218
|
return;
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
if (pendingEnterSubmit) {
|
|
221
|
+
const likelyPasteFlow = now < recentPasteBurstUntil;
|
|
222
|
+
pendingEnterSubmit = false;
|
|
223
|
+
if (pendingEnterSubmitTimer) {
|
|
224
|
+
clearTimeout(pendingEnterSubmitTimer);
|
|
225
|
+
pendingEnterSubmitTimer = undefined;
|
|
226
|
+
}
|
|
227
|
+
if (likelyPasteFlow) {
|
|
228
|
+
absorbLine(line);
|
|
229
|
+
showPrompt();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
absorbLine(line);
|
|
233
|
+
submit(buildSubmitValue());
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
184
236
|
if (ignoreNextLineEvent) {
|
|
185
237
|
ignoreNextLineEvent = false;
|
|
186
238
|
return;
|
|
@@ -202,27 +254,13 @@ export const createMultilineTextPrompt = () => {
|
|
|
202
254
|
showPrompt();
|
|
203
255
|
return;
|
|
204
256
|
}
|
|
205
|
-
const now = Date.now();
|
|
206
257
|
// Check for double Enter to submit
|
|
207
258
|
if (line === '' && now - lastEnterTime < DOUBLE_ENTER_TIMEOUT) {
|
|
208
259
|
submit(buildSubmitValue());
|
|
209
260
|
return;
|
|
210
261
|
}
|
|
211
262
|
lastEnterTime = now;
|
|
212
|
-
|
|
213
|
-
// Empty line - add to lines
|
|
214
|
-
if (currentLine !== '') {
|
|
215
|
-
lines.push(currentLine);
|
|
216
|
-
currentLine = '';
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
// Non-empty line
|
|
221
|
-
if (currentLine !== '') {
|
|
222
|
-
lines.push(currentLine);
|
|
223
|
-
}
|
|
224
|
-
currentLine = line;
|
|
225
|
-
}
|
|
263
|
+
absorbLine(line);
|
|
226
264
|
showPrompt();
|
|
227
265
|
});
|
|
228
266
|
// Handle SIGINT (Ctrl+C)
|
|
@@ -246,6 +284,7 @@ export const createMultilineTextPrompt = () => {
|
|
|
246
284
|
return;
|
|
247
285
|
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
|
|
248
286
|
updateBracketPasteState(text);
|
|
287
|
+
notePossiblePasteBurst(text);
|
|
249
288
|
};
|
|
250
289
|
input.on('data', dataListener);
|
|
251
290
|
keypressListener = async (str, key) => {
|
|
@@ -303,13 +342,28 @@ export const createMultilineTextPrompt = () => {
|
|
|
303
342
|
insertNewline();
|
|
304
343
|
}
|
|
305
344
|
else if (key.name === 'enter' || key.name === 'return') {
|
|
306
|
-
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
const likelyPasteReturn = now < recentPasteBurstUntil;
|
|
347
|
+
if (likelyPasteReturn) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
307
350
|
if (key.shift || key.alt) {
|
|
351
|
+
ignoreNextLineEvent = true;
|
|
308
352
|
// Shift+Enter / Alt+Enter inserts a new line.
|
|
309
353
|
insertNewline();
|
|
310
354
|
return;
|
|
311
355
|
}
|
|
312
|
-
|
|
356
|
+
pendingEnterSubmit = true;
|
|
357
|
+
if (pendingEnterSubmitTimer) {
|
|
358
|
+
clearTimeout(pendingEnterSubmitTimer);
|
|
359
|
+
}
|
|
360
|
+
pendingEnterSubmitTimer = setTimeout(() => {
|
|
361
|
+
if (!pendingEnterSubmit || cancelled)
|
|
362
|
+
return;
|
|
363
|
+
pendingEnterSubmit = false;
|
|
364
|
+
pendingEnterSubmitTimer = undefined;
|
|
365
|
+
submit(buildSubmitValue());
|
|
366
|
+
}, 20);
|
|
313
367
|
}
|
|
314
368
|
};
|
|
315
369
|
input.on('keypress', keypressListener);
|