@levu/snap 0.3.5 → 0.3.7
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.7] - 2026-02-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Fixed Enter-submit race in multiline prompt where pasted multi-line input could lose the final line and submit only the first line.
|
|
12
|
+
- Enter submit now waits briefly for the corresponding readline `line` event (with fallback timeout), preserving pending paste buffer content.
|
|
13
|
+
- Prompt rendering now prefers live readline buffer content so pending pasted lines are visible before submit.
|
|
14
|
+
- Added regression coverage for the “first line only after multiline paste” scenario.
|
|
15
|
+
|
|
16
|
+
## [0.3.6] - 2026-02-26
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Prevented accidental submit during terminal-native multi-line paste by detecting bracketed paste mode (`ESC[200~ ... ESC[201~`) and suppressing Enter submit while paste is in progress.
|
|
20
|
+
- Improved multiline prompt handling in Ghostty/macOS when pasted newlines are delivered via terminal stream instead of explicit paste shortcuts.
|
|
21
|
+
- Added regression coverage for bracketed multi-line paste to ensure pasted content is preserved until the user explicitly submits.
|
|
22
|
+
|
|
8
23
|
## [0.3.5] - 2026-02-26
|
|
9
24
|
|
|
10
25
|
### Fixed
|
|
@@ -28,12 +28,24 @@ export const createMultilineTextPrompt = () => {
|
|
|
28
28
|
let cancelled = false;
|
|
29
29
|
let rawModeEnabled = false;
|
|
30
30
|
let keypressListener;
|
|
31
|
+
let dataListener;
|
|
31
32
|
let ignoreNextLineEvent = false;
|
|
32
33
|
let expectingGhosttySequenceEcho = false;
|
|
34
|
+
let bracketPasteActive = false;
|
|
35
|
+
let bracketPasteProbe = '';
|
|
36
|
+
let pendingEnterSubmit = false;
|
|
37
|
+
let pendingEnterSubmitTimer;
|
|
33
38
|
const cleanup = () => {
|
|
39
|
+
if (pendingEnterSubmitTimer) {
|
|
40
|
+
clearTimeout(pendingEnterSubmitTimer);
|
|
41
|
+
pendingEnterSubmitTimer = undefined;
|
|
42
|
+
}
|
|
34
43
|
if (keypressListener) {
|
|
35
44
|
input.off('keypress', keypressListener);
|
|
36
45
|
}
|
|
46
|
+
if (dataListener) {
|
|
47
|
+
input.off?.('data', dataListener);
|
|
48
|
+
}
|
|
37
49
|
if (rawModeEnabled && input.setRawMode) {
|
|
38
50
|
input.setRawMode(false);
|
|
39
51
|
rawModeEnabled = false;
|
|
@@ -104,6 +116,19 @@ export const createMultilineTextPrompt = () => {
|
|
|
104
116
|
const buildSubmitValue = () => {
|
|
105
117
|
return normalizeGhosttyInlineTokens(lines.concat(getLiveLine()).join('\n'));
|
|
106
118
|
};
|
|
119
|
+
const absorbLine = (line) => {
|
|
120
|
+
if (line.trim() === '') {
|
|
121
|
+
if (currentLine !== '') {
|
|
122
|
+
lines.push(currentLine);
|
|
123
|
+
currentLine = '';
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (currentLine !== '') {
|
|
128
|
+
lines.push(currentLine);
|
|
129
|
+
}
|
|
130
|
+
currentLine = line;
|
|
131
|
+
};
|
|
107
132
|
const insertNewline = () => {
|
|
108
133
|
lines.push(getLiveLine());
|
|
109
134
|
currentLine = '';
|
|
@@ -111,7 +136,29 @@ export const createMultilineTextPrompt = () => {
|
|
|
111
136
|
output.write(`\n${pc.dim('> ')}`);
|
|
112
137
|
};
|
|
113
138
|
const showPrompt = () => {
|
|
114
|
-
output.write(`\n${pc.dim('> ')}${
|
|
139
|
+
output.write(`\n${pc.dim('> ')}${getLiveLine()}`);
|
|
140
|
+
};
|
|
141
|
+
const BRACKET_PASTE_START = '\u001b[200~';
|
|
142
|
+
const BRACKET_PASTE_END = '\u001b[201~';
|
|
143
|
+
const BRACKET_PASTE_PROBE_MAX = 96;
|
|
144
|
+
const updateBracketPasteState = (chunk) => {
|
|
145
|
+
if (!chunk)
|
|
146
|
+
return;
|
|
147
|
+
bracketPasteProbe = `${bracketPasteProbe}${chunk}`.slice(-BRACKET_PASTE_PROBE_MAX);
|
|
148
|
+
if (!bracketPasteActive) {
|
|
149
|
+
const startIndex = bracketPasteProbe.indexOf(BRACKET_PASTE_START);
|
|
150
|
+
if (startIndex >= 0) {
|
|
151
|
+
bracketPasteActive = true;
|
|
152
|
+
bracketPasteProbe = bracketPasteProbe.slice(startIndex + BRACKET_PASTE_START.length);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (bracketPasteActive) {
|
|
156
|
+
const endIndex = bracketPasteProbe.indexOf(BRACKET_PASTE_END);
|
|
157
|
+
if (endIndex >= 0) {
|
|
158
|
+
bracketPasteActive = false;
|
|
159
|
+
bracketPasteProbe = bracketPasteProbe.slice(endIndex + BRACKET_PASTE_END.length);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
115
162
|
};
|
|
116
163
|
showPrompt();
|
|
117
164
|
// Handle paste from clipboard
|
|
@@ -153,6 +200,16 @@ export const createMultilineTextPrompt = () => {
|
|
|
153
200
|
rl.on('line', (line) => {
|
|
154
201
|
if (cancelled)
|
|
155
202
|
return;
|
|
203
|
+
if (pendingEnterSubmit) {
|
|
204
|
+
pendingEnterSubmit = false;
|
|
205
|
+
if (pendingEnterSubmitTimer) {
|
|
206
|
+
clearTimeout(pendingEnterSubmitTimer);
|
|
207
|
+
pendingEnterSubmitTimer = undefined;
|
|
208
|
+
}
|
|
209
|
+
absorbLine(line);
|
|
210
|
+
submit(buildSubmitValue());
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
156
213
|
if (ignoreNextLineEvent) {
|
|
157
214
|
ignoreNextLineEvent = false;
|
|
158
215
|
return;
|
|
@@ -181,20 +238,7 @@ export const createMultilineTextPrompt = () => {
|
|
|
181
238
|
return;
|
|
182
239
|
}
|
|
183
240
|
lastEnterTime = now;
|
|
184
|
-
|
|
185
|
-
// Empty line - add to lines
|
|
186
|
-
if (currentLine !== '') {
|
|
187
|
-
lines.push(currentLine);
|
|
188
|
-
currentLine = '';
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
// Non-empty line
|
|
193
|
-
if (currentLine !== '') {
|
|
194
|
-
lines.push(currentLine);
|
|
195
|
-
}
|
|
196
|
-
currentLine = line;
|
|
197
|
-
}
|
|
241
|
+
absorbLine(line);
|
|
198
242
|
showPrompt();
|
|
199
243
|
});
|
|
200
244
|
// Handle SIGINT (Ctrl+C)
|
|
@@ -213,9 +257,29 @@ export const createMultilineTextPrompt = () => {
|
|
|
213
257
|
input.setRawMode(true);
|
|
214
258
|
rawModeEnabled = true;
|
|
215
259
|
input.resume();
|
|
260
|
+
dataListener = (chunk) => {
|
|
261
|
+
if (cancelled)
|
|
262
|
+
return;
|
|
263
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
|
|
264
|
+
updateBracketPasteState(text);
|
|
265
|
+
};
|
|
266
|
+
input.on('data', dataListener);
|
|
216
267
|
keypressListener = async (str, key) => {
|
|
217
268
|
if (cancelled)
|
|
218
269
|
return;
|
|
270
|
+
if (key?.ctrl && key.name === 'c') {
|
|
271
|
+
doCancel();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (key?.name === 'escape') {
|
|
275
|
+
doCancel();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// In bracketed paste mode, treat all incoming keypresses as paste content.
|
|
279
|
+
// This avoids accidental submit on Enter while multi-line paste is flowing.
|
|
280
|
+
if (bracketPasteActive) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
219
283
|
// Some terminals emit Shift+Enter as literal chars "13~" with no key metadata.
|
|
220
284
|
// Readline may already have appended those chars into rl.line by the time we run.
|
|
221
285
|
if (!key?.ctrl && !key?.meta && key?.name !== 'enter' && key?.name !== 'return') {
|
|
@@ -255,16 +319,23 @@ export const createMultilineTextPrompt = () => {
|
|
|
255
319
|
insertNewline();
|
|
256
320
|
}
|
|
257
321
|
else if (key.name === 'enter' || key.name === 'return') {
|
|
258
|
-
ignoreNextLineEvent = true;
|
|
259
322
|
if (key.shift || key.alt) {
|
|
323
|
+
ignoreNextLineEvent = true;
|
|
260
324
|
// Shift+Enter / Alt+Enter inserts a new line.
|
|
261
325
|
insertNewline();
|
|
262
326
|
return;
|
|
263
327
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
328
|
+
pendingEnterSubmit = true;
|
|
329
|
+
if (pendingEnterSubmitTimer) {
|
|
330
|
+
clearTimeout(pendingEnterSubmitTimer);
|
|
331
|
+
}
|
|
332
|
+
pendingEnterSubmitTimer = setTimeout(() => {
|
|
333
|
+
if (!pendingEnterSubmit || cancelled)
|
|
334
|
+
return;
|
|
335
|
+
pendingEnterSubmit = false;
|
|
336
|
+
pendingEnterSubmitTimer = undefined;
|
|
337
|
+
submit(buildSubmitValue());
|
|
338
|
+
}, 20);
|
|
268
339
|
}
|
|
269
340
|
};
|
|
270
341
|
input.on('keypress', keypressListener);
|