@levu/snap 0.3.10 → 0.3.11

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.11] - 2026-02-28
9
+
10
+ ### Fixed
11
+ - Added direct handling of bracketed paste payloads from raw input stream to preserve multiline content in Ghostty-like terminals.
12
+ - Kept compatibility with readline `line`-event paste flow while avoiding duplicate line ingestion when raw bracketed payload is available.
13
+ - Added regression coverage for bracketed paste payload capture without `line` events.
14
+
8
15
  ## [0.3.10] - 2026-02-28
9
16
 
10
17
  ### Fixed
@@ -33,6 +33,9 @@ export const createMultilineTextPrompt = () => {
33
33
  let expectingGhosttySequenceEcho = false;
34
34
  let bracketPasteActive = false;
35
35
  let bracketPasteProbe = '';
36
+ let bracketPasteBuffer = '';
37
+ let bracketPasteHasRawPayload = false;
38
+ let suppressLineEventsUntil = 0;
36
39
  let pendingEnterSubmit = false;
37
40
  let pendingEnterSubmitTimer;
38
41
  let recentPasteBurstUntil = 0;
@@ -175,6 +178,21 @@ export const createMultilineTextPrompt = () => {
175
178
  const showPrompt = () => {
176
179
  output.write(`\n${pc.dim('> ')}${getLiveLine()}`);
177
180
  };
181
+ const applyPastedText = (rawPasted) => {
182
+ const normalized = String(rawPasted || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
183
+ if (!normalized)
184
+ return;
185
+ const merged = `${getLiveLine()}${normalized}`;
186
+ const pastedLines = merged.split('\n');
187
+ if (pastedLines.length > 1) {
188
+ lines.push(...pastedLines.slice(0, -1));
189
+ currentLine = pastedLines[pastedLines.length - 1];
190
+ }
191
+ else {
192
+ currentLine = merged;
193
+ }
194
+ rl.line = currentLine;
195
+ };
178
196
  const BRACKET_PASTE_START = '\u001b[200~';
179
197
  const BRACKET_PASTE_END = '\u001b[201~';
180
198
  const BRACKET_PASTE_PROBE_MAX = 96;
@@ -215,6 +233,38 @@ export const createMultilineTextPrompt = () => {
215
233
  recentPasteBurstUntil = Math.max(recentPasteBurstUntil, Date.now() + PASTE_BURST_WINDOW_MS);
216
234
  }
217
235
  };
236
+ const ingestBracketedPasteChunk = (chunk) => {
237
+ if (!chunk)
238
+ return;
239
+ let rest = chunk;
240
+ while (rest.length > 0) {
241
+ if (!bracketPasteActive) {
242
+ const startIndex = rest.indexOf(BRACKET_PASTE_START);
243
+ if (startIndex < 0)
244
+ return;
245
+ bracketPasteActive = true;
246
+ rest = rest.slice(startIndex + BRACKET_PASTE_START.length);
247
+ continue;
248
+ }
249
+ const endIndex = rest.indexOf(BRACKET_PASTE_END);
250
+ if (endIndex < 0) {
251
+ if (rest.length > 0)
252
+ bracketPasteHasRawPayload = true;
253
+ bracketPasteBuffer += rest;
254
+ return;
255
+ }
256
+ if (endIndex > 0)
257
+ bracketPasteHasRawPayload = true;
258
+ bracketPasteBuffer += rest.slice(0, endIndex);
259
+ bracketPasteActive = false;
260
+ applyPastedText(bracketPasteBuffer);
261
+ bracketPasteBuffer = '';
262
+ bracketPasteHasRawPayload = false;
263
+ suppressLineEventsUntil = Math.max(suppressLineEventsUntil, Date.now() + 60);
264
+ showPrompt();
265
+ rest = rest.slice(endIndex + BRACKET_PASTE_END.length);
266
+ }
267
+ };
218
268
  showPrompt();
219
269
  // Handle paste from clipboard
220
270
  const handlePaste = async () => {
@@ -255,6 +305,8 @@ export const createMultilineTextPrompt = () => {
255
305
  rl.on('line', (line) => {
256
306
  if (cancelled)
257
307
  return;
308
+ if ((bracketPasteActive && bracketPasteHasRawPayload) || Date.now() < suppressLineEventsUntil)
309
+ return;
258
310
  const now = Date.now();
259
311
  if (pendingEnterSubmit) {
260
312
  const likelyPasteFlow = now < recentPasteBurstUntil;
@@ -322,6 +374,7 @@ export const createMultilineTextPrompt = () => {
322
374
  if (cancelled)
323
375
  return;
324
376
  const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
377
+ ingestBracketedPasteChunk(text);
325
378
  updateBracketPasteState(text);
326
379
  notePossiblePasteBurst(text);
327
380
  };
@@ -362,17 +415,7 @@ export const createMultilineTextPrompt = () => {
362
415
  if (pasted) {
363
416
  // Clear current line and show pasted content
364
417
  output.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
365
- const pastedLines = pasted.split('\n');
366
- if (pastedLines.length > 1) {
367
- // Multiline paste
368
- lines.push(...pastedLines.slice(0, -1));
369
- currentLine = pastedLines[pastedLines.length - 1];
370
- }
371
- else {
372
- // Single line paste
373
- currentLine += pasted;
374
- }
375
- rl.line = currentLine;
418
+ applyPastedText(pasted);
376
419
  output.write(`${pc.dim('> ')}${currentLine}`);
377
420
  }
378
421
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levu/snap",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "snap": "./dist/cli-entry.js"