@levu/snap 0.3.5 → 0.3.6

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,13 @@ 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.6] - 2026-02-26
9
+
10
+ ### Fixed
11
+ - 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.
12
+ - Improved multiline prompt handling in Ghostty/macOS when pasted newlines are delivered via terminal stream instead of explicit paste shortcuts.
13
+ - Added regression coverage for bracketed multi-line paste to ensure pasted content is preserved until the user explicitly submits.
14
+
8
15
  ## [0.3.5] - 2026-02-26
9
16
 
10
17
  ### Fixed
@@ -28,12 +28,18 @@ 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 = '';
33
36
  const cleanup = () => {
34
37
  if (keypressListener) {
35
38
  input.off('keypress', keypressListener);
36
39
  }
40
+ if (dataListener) {
41
+ input.off?.('data', dataListener);
42
+ }
37
43
  if (rawModeEnabled && input.setRawMode) {
38
44
  input.setRawMode(false);
39
45
  rawModeEnabled = false;
@@ -113,6 +119,28 @@ export const createMultilineTextPrompt = () => {
113
119
  const showPrompt = () => {
114
120
  output.write(`\n${pc.dim('> ')}${currentLine}`);
115
121
  };
122
+ const BRACKET_PASTE_START = '\u001b[200~';
123
+ const BRACKET_PASTE_END = '\u001b[201~';
124
+ const BRACKET_PASTE_PROBE_MAX = 96;
125
+ const updateBracketPasteState = (chunk) => {
126
+ if (!chunk)
127
+ return;
128
+ bracketPasteProbe = `${bracketPasteProbe}${chunk}`.slice(-BRACKET_PASTE_PROBE_MAX);
129
+ if (!bracketPasteActive) {
130
+ const startIndex = bracketPasteProbe.indexOf(BRACKET_PASTE_START);
131
+ if (startIndex >= 0) {
132
+ bracketPasteActive = true;
133
+ bracketPasteProbe = bracketPasteProbe.slice(startIndex + BRACKET_PASTE_START.length);
134
+ }
135
+ }
136
+ if (bracketPasteActive) {
137
+ const endIndex = bracketPasteProbe.indexOf(BRACKET_PASTE_END);
138
+ if (endIndex >= 0) {
139
+ bracketPasteActive = false;
140
+ bracketPasteProbe = bracketPasteProbe.slice(endIndex + BRACKET_PASTE_END.length);
141
+ }
142
+ }
143
+ };
116
144
  showPrompt();
117
145
  // Handle paste from clipboard
118
146
  const handlePaste = async () => {
@@ -213,9 +241,29 @@ export const createMultilineTextPrompt = () => {
213
241
  input.setRawMode(true);
214
242
  rawModeEnabled = true;
215
243
  input.resume();
244
+ dataListener = (chunk) => {
245
+ if (cancelled)
246
+ return;
247
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
248
+ updateBracketPasteState(text);
249
+ };
250
+ input.on('data', dataListener);
216
251
  keypressListener = async (str, key) => {
217
252
  if (cancelled)
218
253
  return;
254
+ if (key?.ctrl && key.name === 'c') {
255
+ doCancel();
256
+ return;
257
+ }
258
+ if (key?.name === 'escape') {
259
+ doCancel();
260
+ return;
261
+ }
262
+ // In bracketed paste mode, treat all incoming keypresses as paste content.
263
+ // This avoids accidental submit on Enter while multi-line paste is flowing.
264
+ if (bracketPasteActive) {
265
+ return;
266
+ }
219
267
  // Some terminals emit Shift+Enter as literal chars "13~" with no key metadata.
220
268
  // Readline may already have appended those chars into rl.line by the time we run.
221
269
  if (!key?.ctrl && !key?.meta && key?.name !== 'enter' && key?.name !== 'return') {
@@ -263,9 +311,6 @@ export const createMultilineTextPrompt = () => {
263
311
  }
264
312
  submit(buildSubmitValue());
265
313
  }
266
- else if (key.name === 'escape') {
267
- doCancel();
268
- }
269
314
  };
270
315
  input.on('keypress', keypressListener);
271
316
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levu/snap",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "snap": "./dist/cli-entry.js"